|
|
<template>
|
|
|
<div class="competitive-analysis-page student-training-shell">
|
|
|
<main class="analysis-core">
|
|
|
<section class="task-header">
|
|
|
<div class="task-head-main">
|
|
|
<h1 class="task-title">{{ taskTitle }}</h1>
|
|
|
<p class="task-brief">
|
|
|
<span class="task-brief-text">{{ taskBackground }}</span>
|
|
|
<button type="button" class="task-brief-toggle" @click="briefOpen = !briefOpen">
|
|
|
{{ briefOpen ? "收起" : "展开目标与要求" }}
|
|
|
</button>
|
|
|
<span v-if="briefOpen" class="task-brief-more">
|
|
|
<b>实训目标</b>
|
|
|
<ul><li v-for="goal in taskGoals" :key="goal">{{ goal }}</li></ul>
|
|
|
<b>实训要求</b>
|
|
|
<p>{{ taskRequirement }}</p>
|
|
|
</span>
|
|
|
</p>
|
|
|
</div>
|
|
|
<div class="analysis-toolbar">
|
|
|
<TrainingMaterialButton
|
|
|
:material-name="taskConfig?.materialName"
|
|
|
:material-url="taskConfig?.materialUrl"
|
|
|
:training-data-name="taskConfig?.trainingDataName"
|
|
|
:training-data-url="taskConfig?.trainingDataUrl"
|
|
|
:guide-manual-name="taskConfig?.guideManualName"
|
|
|
:guide-manual-url="taskConfig?.guideManualUrl"
|
|
|
:ai-manual-name="taskConfig?.aiManualName"
|
|
|
:ai-manual-url="taskConfig?.aiManualUrl"
|
|
|
/>
|
|
|
<button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon> 导出实训成果</button>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="analysis-workbench">
|
|
|
<nav class="step-indicator" aria-label="竞争环境分析步骤"><template v-for="(step, index) in trainingSteps" :key="`${index}-${step}`"><button type="button" :class="['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]" @click="switchStep(index + 1)"><span class="step-marker">{{ String(index + 1).padStart(2, '0') }}</span><span class="step-copy"><strong>{{ step }}</strong><small>{{ currentStep === index + 1 ? '当前步骤' : '点击切换' }} →</small></span></button><i v-if="index < trainingSteps.length - 1" class="step-connector" /></template></nav>
|
|
|
<section class="step-card">
|
|
|
<template v-if="currentStep === 1">
|
|
|
<div class="step-title-row"><div><p>Step 01</p><h2>{{ trainingSteps[0] }}</h2></div><span>SWOT 分类练习</span></div>
|
|
|
<p class="step-desc">请根据案例文档中信息进行 SWOT 分析,进行各项内容归类,把文档中相应内容填写到以下四类中。</p>
|
|
|
<div class="swot-table-wrap"><table class="swot-table"><thead><tr><th>序号</th><th>SWOT维度</th><th>关键信息要点</th></tr></thead><tbody><tr v-for="(row, index) in swotRows" :key="row.key"><td>{{ index + 1 }}</td><td class="dimension-cell">{{ row.label }}</td><td><el-input v-model.trim="form[row.key]" type="textarea" :autosize="{ minRows: 2 }" :placeholder="row.placeholder" /></td></tr></tbody></table></div>
|
|
|
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving" @click="saveAndGoToStepTwo">保存并进入第 2 步</button><button type="button" class="btn" :disabled="saving" @click="resetTraining"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
|
|
|
</template>
|
|
|
<template v-else-if="currentStep === 2">
|
|
|
<div class="step-title-row"><div><p>Step 02</p><h2>{{ trainingSteps[1] }}</h2></div><span>资料补充</span></div>
|
|
|
<div class="research-guide"><p>请自行收集资料,补充完善 SWOT 信息,每个维度至少补充 1 条。信息收集可参考以下方法:</p><ol><li>自行通过互联网平台收集;可在电商平台(京东/天猫)搜索“智能健身镜”,浏览竞品用户评价,从中发现竞品的优劣势和用户未满足的需求。</li><li>使用 AI 大模型智能搜索功能进行信息收集,参考提示词见实训任务案例文档。</li></ol><p>请把补充的 SWOT 信息资讯填入下表:</p></div>
|
|
|
<p v-if="!stepOnePassed" class="step-warning">请先完成并保存第 1 步的 SWOT 信息归类,再填写本步骤。</p>
|
|
|
<div class="swot-table-wrap"><table class="swot-table"><thead><tr><th>序号</th><th>SWOT维度</th><th>补充信息要点</th></tr></thead><tbody><tr v-for="(row, index) in swotRows" :key="row.key"><td>{{ index + 1 }}</td><td class="dimension-cell">{{ row.label }}</td><td><el-input v-model.trim="supplementForm[row.key]" type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepOnePassed" :placeholder="row.placeholder" /></td></tr></tbody></table></div>
|
|
|
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepOnePassed" @click="saveAndGoToStepThree">保存并进入第 3 步</button><button type="button" class="btn" :disabled="saving || !stepOnePassed" @click="resetStepTwo"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
|
|
|
</template>
|
|
|
<template v-else-if="currentStep === 3">
|
|
|
<div class="step-title-row"><div><p>Step 03</p><h2>{{ trainingSteps[2] }}</h2></div><span>SWOT 策略矩阵</span></div>
|
|
|
<p class="step-desc">结合 SWOT 因素矩阵,将内部因素与外部因素交叉组合,分别提出四种策略。每类策略至少提出 1 条。</p>
|
|
|
<p v-if="!stepTwoPassed" class="step-warning">请先完成并保存第 2 步的 SWOT 补充信息,再填写本步骤。</p>
|
|
|
<div class="strategy-table-wrap"><table class="strategy-table"><thead><tr><th>策略类型</th><th>组合逻辑(自动生成)</th><th>具体策略建议</th><th>预期效果</th></tr></thead><tbody><tr v-for="row in strategyDefinitions" :key="row.key"><td class="strategy-type">{{ row.label }}</td><td class="logic-cell"><p v-for="item in getCombinedLogic(row)" :key="item.label"><strong>{{ item.label }}</strong>{{ item.value || '暂无已保存信息' }}</p></td><td><el-input v-model.trim="strategyForm[row.key].recommendation" type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepTwoPassed" placeholder="填写具体策略建议" /></td><td><el-input v-model.trim="strategyForm[row.key].expectedEffect" type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepTwoPassed" placeholder="填写预期效果" /></td></tr></tbody></table></div>
|
|
|
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepTwoPassed" @click="saveAndGoToStepFour">保存并进入第 4 步</button><button type="button" class="btn" :disabled="saving || !stepTwoPassed" @click="resetStepThree"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
|
|
|
</template>
|
|
|
<template v-else-if="currentStep === 4">
|
|
|
<div class="step-title-row"><div><p>Step 04</p><h2>{{ trainingSteps[3] }}</h2></div><span>最终决策</span></div>
|
|
|
<p class="step-desc">结合前述 SWOT 分析,完成策略优先级排序、风险识别与竞争环境总结。</p>
|
|
|
<p v-if="!stepThreePassed" class="step-warning">请先完成并保存第 3 步的四类 SWOT 策略,再完成最终决策。</p>
|
|
|
<section class="decision-section"><h3>(1)优先策略排序</h3><p class="section-tip">策略内容由第 3 步填写的“具体策略建议”自动带入;请完成优先级排序并说明理由。</p><div class="priority-table-wrap"><table class="priority-table"><thead><tr><th>优先级排序</th><th>策略</th><th>理由</th></tr></thead><tbody><tr v-for="row in strategyDefinitions" :key="row.key"><td><el-select v-model="finalForm.priorities[row.key].priority" :disabled="!stepThreePassed" :aria-label="`${row.label} 的优先级排序`" placeholder="请选择" style="width:100%"><el-option :value="null" label="请选择" /><el-option v-for="rank in 4" :key="rank" :value="rank" :label="`${rank}`" /></el-select></td><td class="priority-strategy"><strong>{{ row.label }}</strong><p>{{ strategyForm[row.key].recommendation || '请先在第 3 步填写该策略建议' }}</p></td><td><el-input v-model.trim="finalForm.priorities[row.key].reason" type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepThreePassed" placeholder="说明该策略的排序理由" /></td></tr></tbody></table></div></section>
|
|
|
<section class="decision-section"><h3>(2)需关注的风险点</h3><p>请至少指出 2 个风险点。</p><div class="risk-grid"><label><span>核心风险点 1</span><el-input v-model.trim="finalForm.coreRiskOne" type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>核心风险点 2</span><el-input v-model.trim="finalForm.coreRiskTwo" type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>其他风险点</span><el-input v-model.trim="finalForm.otherRisk" type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepThreePassed" placeholder="可补充其他风险点" /></label></div></section>
|
|
|
<section class="decision-section"><h3>(3)总结 SWOT 竞争环境评估结论</h3><el-input v-model.trim="finalForm.conclusion" class="conclusion-textarea" type="textarea" :autosize="{ minRows: 3 }" :disabled="!stepThreePassed" placeholder="综合说明优先策略、风险应对方向及最终结论" /></section>
|
|
|
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepThreePassed" @click="saveAndCompleteTraining">保存并完成实训</button><button type="button" class="btn" :disabled="saving || !stepThreePassed" @click="resetStepFour"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
|
|
|
</template>
|
|
|
<div v-else class="step-placeholder"><p>第 {{ currentStep }} 步内容待配置</p><span>第 1 步 SWOT 分类已保存,可从上方步骤栏返回查看。</span></div>
|
|
|
</section>
|
|
|
</section>
|
|
|
</main>
|
|
|
<TrainingAiSidebar study-tip="先辨别内部条件与外部环境,再归类为优势、劣势、机会和威胁。" reference-title="SWOT 分类提示" reference-text="· 优势:企业或产品已有的资源、能力、渠道与差异化<br />· 劣势:内部能力、成本、体验、资源或覆盖不足<br />· 机会:外部市场增长、政策变化、消费趋势与技术窗口<br />· 威胁:竞争加剧、替代品、成本风险、政策限制与需求变化" :progress-items="progressItems" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { Download, RefreshLeft, TrendCharts } from "@element-plus/icons-vue";
|
|
|
import { checkCompetitiveEnvironmentAnalysisStepOne, checkCompetitiveEnvironmentAnalysisStepTwo, checkCompetitiveEnvironmentAnalysisStepThree, checkCompetitiveEnvironmentAnalysisStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask";
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap";
|
|
|
import useUserStore from "@/store/modules/user";
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
|
|
|
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
|
|
|
|
|
|
const TASK_KEY = "competitive-environment-analysis";
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
const userStore = useUserStore();
|
|
|
const DEFAULT_STEPS = ["SWOT竞争环境分析", "竞品调研与比较", "竞争策略设计", "竞争环境结论"];
|
|
|
const swotRows = [
|
|
|
{ key: "strengths", label: "优势(S)", placeholder: "填写案例中的资源、能力、渠道、产品或服务优势" },
|
|
|
{ key: "weaknesses", label: "劣势(W)", placeholder: "填写案例中的内部短板、成本、体验或资源不足" },
|
|
|
{ key: "opportunities", label: "机会(O)", placeholder: "填写案例中的市场、政策、消费趋势或技术机会" },
|
|
|
{ key: "threats", label: "威胁(T)", placeholder: "填写案例中的竞争、替代、成本、政策或需求风险" },
|
|
|
];
|
|
|
const strategyDefinitions = [
|
|
|
{ key: "so", label: "SO策略(优势×机会)", first: "strengths", firstLabel: "优势", second: "opportunities", secondLabel: "机会" },
|
|
|
{ key: "wo", label: "WO策略(劣势×机会)", first: "weaknesses", firstLabel: "劣势", second: "opportunities", secondLabel: "机会" },
|
|
|
{ key: "st", label: "ST策略(优势×威胁)", first: "strengths", firstLabel: "优势", second: "threats", secondLabel: "威胁" },
|
|
|
{ key: "wt", label: "WT策略(劣势×威胁)", first: "weaknesses", firstLabel: "劣势", second: "threats", secondLabel: "威胁" },
|
|
|
];
|
|
|
const defaultTask = { title: "竞争环境分析", background: "通过 SWOT 模型归类案例资料中的竞争环境信息,为后续竞品比较与竞争策略判断提供依据。", goals: ["掌握 SWOT 四维度的分类方法", "提炼案例中的竞争环境关键信息", "形成竞争分析的基础材料"], requirement: "请阅读案例资料,将相关信息归类为优势、劣势、机会和威胁。" };
|
|
|
const taskConfig = ref(null); const briefOpen = ref(false); const currentStep = ref(1); const saving = ref(false); const draftReady = ref(false); const stepOnePassed = ref(false); const stepTwoPassed = ref(false); const stepThreePassed = ref(false); const form = ref(createForm()); const supplementForm = ref(createForm()); const strategyForm = ref(createStrategyForm()); const finalForm = ref(createFinalForm());
|
|
|
const taskTitle = computed(() => taskConfig.value?.taskName || defaultTask.title); const taskBackground = computed(() => taskConfig.value?.background || defaultTask.background); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultTask.goals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultTask.requirement); const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, DEFAULT_STEPS)); const progressItems = computed(() => swotRows.map((row) => ({ text: `${strategyForm.value.so.recommendation ? "已制定策略" : supplementForm.value[row.key] ? "已补充" : form.value[row.key] ? "已归类" : "待填写"}:${row.label}`, status: strategyForm.value.so.recommendation || supplementForm.value[row.key] || form.value[row.key] ? "done" : "" })));
|
|
|
onMounted(async () => { await loadTaskConfig(); loadDraft(); await loadSavedAnswer(); draftReady.value = true; }); watch(() => JSON.stringify(buildDraft()), persistDraft);
|
|
|
function createForm() { return { strengths: "", weaknesses: "", opportunities: "", threats: "" }; }
|
|
|
function createStrategyForm() { return { so: createStrategyRow(), wo: createStrategyRow(), st: createStrategyRow(), wt: createStrategyRow() }; }
|
|
|
function createStrategyRow() { return { recommendation: "", expectedEffect: "" }; }
|
|
|
function createFinalForm() { return { priorities: Object.fromEntries(strategyDefinitions.map((row) => [row.key, { priority: null, reason: "" }])), coreRiskOne: "", coreRiskTwo: "", otherRisk: "", conclusion: "" }; }
|
|
|
function parseArray(value, fallback) { if (Array.isArray(value)) return value.length ? value : fallback; if (!value) return fallback; try { const parsed = JSON.parse(value); return Array.isArray(parsed) && parsed.length ? parsed.map((item) => String(item?.name ?? item).trim()).filter(Boolean) : fallback; } catch (error) { const list = String(value).split(/[,,|]/).map((item) => item.trim()).filter(Boolean); return list.length ? list : fallback; } }
|
|
|
async function loadTaskConfig() { try { taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || null; } catch (error) { taskConfig.value = null; } }
|
|
|
async function loadSavedAnswer() { try { const answer = (await getStudentTrainingAnswer(TASK_KEY))?.data; if (answer?.step1Answer) { restoreOutcome(answer.step1Answer, form); stepOnePassed.value = true; clearDraft(); } if (answer?.step2Answer) { restoreOutcome(answer.step2Answer, supplementForm, "supplements"); stepTwoPassed.value = true; } if (answer?.step3Answer) { restoreStrategyOutcome(answer.step3Answer); stepThreePassed.value = true; } if (answer?.step4Answer) restoreFinalOutcome(answer.step4Answer); if (Number(answer?.currentStep) > 0) currentStep.value = Number(answer.currentStep); } catch (error) { /* Keep local draft when unavailable. */ } }
|
|
|
function restoreOutcome(value, target, field = "swot") { try { const parsed = typeof value === "string" ? JSON.parse(value) : value; target.value = { ...createForm(), ...(parsed?.[field] || parsed || {}) }; } catch (error) { target.value = createForm(); } }
|
|
|
function restoreStrategyOutcome(value) { try { const parsed = typeof value === "string" ? JSON.parse(value) : value; const source = parsed?.strategies || parsed || {}; strategyForm.value = Object.fromEntries(strategyDefinitions.map((row) => [row.key, { ...createStrategyRow(), ...(source[row.key] || {}) }])); } catch (error) { strategyForm.value = createStrategyForm(); } }
|
|
|
function restoreFinalOutcome(value) { try { const parsed = typeof value === "string" ? JSON.parse(value) : value; const source = parsed?.decision || parsed || {}; const priorities = source.priorities || {}; finalForm.value = { ...createFinalForm(), ...source, priorities: Object.fromEntries(strategyDefinitions.map((row) => [row.key, { priority: null, reason: "", ...(priorities[row.key] || {}) }])) }; } catch (error) { finalForm.value = createFinalForm(); } }
|
|
|
function getCombinedLogic(row) { return [{ label: `${row.firstLabel}(第1步)· `, value: form.value[row.first] }, { label: `${row.firstLabel}(第2步补充)· `, value: supplementForm.value[row.first] }, { label: `${row.secondLabel}(第1步)· `, value: form.value[row.second] }, { label: `${row.secondLabel}(第2步补充)· `, value: supplementForm.value[row.second] }]; }
|
|
|
function buildStepOneOutcome() { return { title: taskTitle.value, swot: { ...form.value } }; } function buildStepTwoOutcome() { return { title: taskTitle.value, supplements: { ...supplementForm.value } }; } function buildStepThreeOutcome() { return { title: taskTitle.value, strategies: JSON.parse(JSON.stringify(strategyForm.value)) }; } function buildStepFourOutcome() { return { title: taskTitle.value, decision: JSON.parse(JSON.stringify(finalForm.value)) }; } function buildDraft() { return { step1: buildStepOneOutcome(), step2: buildStepTwoOutcome(), step3: buildStepThreeOutcome(), step4: buildStepFourOutcome(), currentStep: currentStep.value }; } function getLocalDraftKey() { const info = userStore.userInfo || {}; return `${TASK_KEY}:draft:${info.userId || info.user_id || info.username || info.userName || "anonymous"}`; } function persistDraft() { if (!draftReady.value) return; try { localStorage.setItem(getLocalDraftKey(), JSON.stringify(buildDraft())); } catch (error) { /* Optional browser draft. */ } } function loadDraft() { try { const raw = localStorage.getItem(getLocalDraftKey()); if (raw) { const draft = JSON.parse(raw); restoreOutcome(draft?.step1 || draft, form); restoreOutcome(draft?.step2, supplementForm, "supplements"); restoreStrategyOutcome(draft?.step3); restoreFinalOutcome(draft?.step4); if (Number(draft?.currentStep) > 0) currentStep.value = Number(draft.currentStep); } } catch (error) { clearDraft(); } } function clearDraft() { try { localStorage.removeItem(getLocalDraftKey()); } catch (error) { /* Ignore storage failures. */ } }
|
|
|
function switchStep(step) { if (step === 2 && !stepOnePassed.value) { proxy?.$modal?.msgWarning("请先完成并保存第 1 步"); return; } if (step === 3 && !stepTwoPassed.value) { proxy?.$modal?.msgWarning("请先完成并保存第 2 步"); return; } if (step === 4 && !stepThreePassed.value) { proxy?.$modal?.msgWarning("请先完成并保存第 3 步"); return; } currentStep.value = step; }
|
|
|
async function saveAndGoToStepTwo() { if (saving.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepOne(form.value); await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 2, submitted: false, saveAction: "SAVE" }); stepOnePassed.value = true; clearDraft(); currentStep.value = 2; proxy?.$modal?.msgSuccess("已保存,已进入第 2 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写 SWOT 四类信息"); } finally { saving.value = false; } }
|
|
|
async function saveAndGoToStepThree() { if (saving.value || !stepOnePassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepTwo(supplementForm.value); await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: 3, submitted: false, saveAction: "SAVE" }); stepTwoPassed.value = true; clearDraft(); currentStep.value = 3; proxy?.$modal?.msgSuccess("已保存,已进入第 3 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请为每个 SWOT 维度至少补充 1 条信息"); } finally { saving.value = false; } }
|
|
|
async function saveAndGoToStepFour() { if (saving.value || !stepTwoPassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepThree({ strategies: strategyDefinitions.map((row) => ({ strategyType: row.key, ...strategyForm.value[row.key] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); stepThreePassed.value = true; clearDraft(); currentStep.value = 4; proxy?.$modal?.msgSuccess("已保存,已进入第 4 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成四类 SWOT 策略建议和预期效果"); } finally { saving.value = false; } }
|
|
|
async function saveAndCompleteTraining() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepFour({ priorities: strategyDefinitions.map((row) => ({ strategyType: row.key, ...finalForm.value.priorities[row.key] })), coreRiskOne: finalForm.value.coreRiskOne, coreRiskTwo: finalForm.value.coreRiskTwo, otherRisk: finalForm.value.otherRisk, conclusion: finalForm.value.conclusion }); await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: true, saveAction: "SUBMIT" }); clearDraft(); proxy?.$modal?.msgSuccess("已保存并完成实训"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写策略优先级、至少 2 个核心风险点和总结 SWOT 竞争环境评估结论"); } finally { saving.value = false; } }
|
|
|
async function resetTraining() { form.value = createForm(); supplementForm.value = createForm(); strategyForm.value = createStrategyForm(); finalForm.value = createFinalForm(); stepOnePassed.value = false; stepTwoPassed.value = false; stepThreePassed.value = false; currentStep.value = 1; clearDraft(); try { saving.value = true; await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 1, submitted: false, saveAction: "RESET" }); } catch (error) { /* Local clearing still succeeds. */ } finally { saving.value = false; } proxy?.$modal?.msgSuccess("已清空填写内容"); }
|
|
|
function resetStepTwo() { supplementForm.value = createForm(); proxy?.$modal?.msgSuccess("已清空第 2 步填写内容"); }
|
|
|
function resetStepThree() { strategyForm.value = createStrategyForm(); proxy?.$modal?.msgSuccess("已清空第 3 步填写内容"); }
|
|
|
function resetStepFour() { finalForm.value = createFinalForm(); proxy?.$modal?.msgSuccess("已清空第 4 步填写内容"); }
|
|
|
function exportOutcome() { const blob = new Blob([JSON.stringify(buildDraft(), null, 2)], { type: "application/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "竞争环境分析-实训成果.json"; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); proxy?.$modal?.msgSuccess("导出成功"); }
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
/* ===== 页面级令牌(教育蓝极简主题) ===== */
|
|
|
.competitive-analysis-page {
|
|
|
--student-accent: #2563eb;
|
|
|
--student-accent-deep: #1d4ed8;
|
|
|
--student-accent-light: #3b82f6;
|
|
|
--student-accent-soft: #eff6ff;
|
|
|
--student-danger: #ef4444;
|
|
|
--student-success: #2ba471;
|
|
|
--student-layer-1: #1d4ed8;
|
|
|
--student-layer-2: #3b82f6;
|
|
|
--student-layer-3: #93c5fd;
|
|
|
--student-panel-border: #e5e7eb;
|
|
|
--student-card-bg: #ffffff;
|
|
|
--student-panel-bg: #fafbfc;
|
|
|
--student-text: #1f2937;
|
|
|
--student-text-secondary: #6b7280;
|
|
|
--el-color-primary: #2563eb;
|
|
|
--el-color-primary-light-3: #60a5fa;
|
|
|
--el-color-primary-light-5: #93c5fd;
|
|
|
--el-color-primary-light-7: #bfdbfe;
|
|
|
--el-color-primary-light-8: #dbeafe;
|
|
|
--el-color-primary-light-9: #eff6ff;
|
|
|
--el-color-primary-dark-2: #1d4ed8;
|
|
|
--el-input-hover-border-color: #3b82f6;
|
|
|
--el-input-focus-border-color: #2563eb;
|
|
|
--el-input-focus-ring-color: rgba(59, 130, 246, 0.15);
|
|
|
--el-textarea-hover-border-color: #3b82f6;
|
|
|
--el-textarea-focus-border-color: #2563eb;
|
|
|
/* 字号梯度保留原样 */
|
|
|
--student-fs-display: 17px;
|
|
|
--student-fs-h1: 15px;
|
|
|
--student-fs-h2: 14px;
|
|
|
--student-fs-h3: 13px;
|
|
|
--student-fs-body: 13px;
|
|
|
--student-fs-body-sm: 12px;
|
|
|
--student-fs-caption: 11px;
|
|
|
--student-fs-button: 13px;
|
|
|
--student-fs-button-lg: 13px;
|
|
|
--student-fs-data: 14px;
|
|
|
|
|
|
position: relative;
|
|
|
z-index: 2;
|
|
|
display: grid;
|
|
|
grid-template-columns: minmax(720px, 1fr) 340px;
|
|
|
gap: 24px;
|
|
|
width: 100%;
|
|
|
max-width: 100%;
|
|
|
min-height: calc(100vh - 106px);
|
|
|
margin: 0;
|
|
|
padding: 24px 24px 32px;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
/* 对齐 foundation 的 .training-core:只设 gap,无边框/背景/阴影 */
|
|
|
.analysis-core {
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
align-content: start;
|
|
|
min-width: 0;
|
|
|
border: 0;
|
|
|
background: transparent;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
/* 任务头:白卡 + 左侧教育蓝竖条 + 12px 圆角 */
|
|
|
.task-header {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
margin: 0;
|
|
|
padding: 18px 20px 18px 24px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: var(--student-card-bg);
|
|
|
overflow: hidden;
|
|
|
|
|
|
&::before {
|
|
|
content: "";
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
bottom: 0;
|
|
|
left: 0;
|
|
|
width: 4px;
|
|
|
background: var(--student-accent);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.task-badge,
|
|
|
.step-title-row p {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-weight: 700;
|
|
|
font-size: 11px;
|
|
|
letter-spacing: 0.12em;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
|
|
|
.task-header h1 {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-display);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
/* task-header 内部主列(对齐 foundation 的 .task-head-main) */
|
|
|
.task-head-main {
|
|
|
display: grid;
|
|
|
justify-items: start;
|
|
|
gap: 8px;
|
|
|
min-width: 0;
|
|
|
flex: 1 1 auto;
|
|
|
}
|
|
|
|
|
|
.task-brief {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: 12px;
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.task-brief-text {
|
|
|
display: -webkit-box;
|
|
|
overflow: hidden;
|
|
|
-webkit-box-orient: vertical;
|
|
|
-webkit-line-clamp: 1;
|
|
|
}
|
|
|
|
|
|
.task-brief-toggle {
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
color: var(--student-accent-deep);
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover { text-decoration: underline; }
|
|
|
}
|
|
|
|
|
|
.task-brief-more {
|
|
|
display: grid;
|
|
|
gap: 6px;
|
|
|
margin-top: 8px;
|
|
|
padding-top: 8px;
|
|
|
border-top: 1px solid var(--student-panel-border);
|
|
|
|
|
|
b { color: var(--student-text); font-size: 12px; }
|
|
|
ul { display: grid; gap: 4px; margin: 0; padding-left: 18px; }
|
|
|
p { margin: 0; }
|
|
|
}
|
|
|
|
|
|
.analysis-toolbar {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
flex: 0 0 auto;
|
|
|
justify-content: flex-end;
|
|
|
}
|
|
|
|
|
|
/* workbench 层:透明容器 */
|
|
|
.analysis-workbench {
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
/* step-card 层:透明容器 */
|
|
|
.step-card {
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
.step-title-row {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: flex-start;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.step-title-row p { margin: 0; }
|
|
|
|
|
|
.step-title-row h2 {
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
.step-title-row > span {
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 6px;
|
|
|
padding: 3px 10px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: 11px;
|
|
|
font-weight: 600;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.step-desc {
|
|
|
margin: 14px 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.research-guide {
|
|
|
margin: 14px 0 12px;
|
|
|
padding: 12px 14px;
|
|
|
border-left: 3px solid var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.research-guide p { margin: 0 0 6px; }
|
|
|
.research-guide ol { margin: 0 0 6px; padding-left: 20px; }
|
|
|
.research-guide li + li { margin-top: 4px; }
|
|
|
|
|
|
.step-warning {
|
|
|
margin: 10px 0;
|
|
|
color: #b45309;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
/* ===== 表格 ===== */
|
|
|
.swot-table-wrap,
|
|
|
.strategy-table-wrap,
|
|
|
.priority-table-wrap { overflow-x: auto; }
|
|
|
|
|
|
.swot-table,
|
|
|
.strategy-table,
|
|
|
.priority-table {
|
|
|
width: 100%;
|
|
|
border-collapse: collapse;
|
|
|
}
|
|
|
|
|
|
.swot-table { min-width: 720px; }
|
|
|
.strategy-table { min-width: 980px; }
|
|
|
.priority-table { min-width: 820px; }
|
|
|
|
|
|
.swot-table th,
|
|
|
.swot-table td,
|
|
|
.strategy-table th,
|
|
|
.strategy-table td,
|
|
|
.priority-table th,
|
|
|
.priority-table td {
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
}
|
|
|
|
|
|
.swot-table th,
|
|
|
.strategy-table th,
|
|
|
.priority-table th {
|
|
|
padding: 10px 12px;
|
|
|
color: var(--student-text);
|
|
|
background: #fafbfc;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 800;
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
.swot-table th:first-child,
|
|
|
.swot-table td:first-child {
|
|
|
width: 8%;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.swot-table th:nth-child(2),
|
|
|
.dimension-cell {
|
|
|
width: 18%;
|
|
|
text-align: center;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.strategy-table th:first-child,
|
|
|
.strategy-table .strategy-type {
|
|
|
width: 18%;
|
|
|
text-align: center;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.strategy-table th:nth-child(2) { width: 28%; }
|
|
|
|
|
|
.strategy-table .logic-cell {
|
|
|
padding: 10px 12px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.strategy-table .logic-cell p { margin: 0 0 6px; }
|
|
|
.strategy-table .logic-cell p:last-child { margin-bottom: 0; }
|
|
|
|
|
|
.strategy-table .logic-cell strong {
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.priority-table th:first-child,
|
|
|
.priority-table td:first-child {
|
|
|
width: 16%;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.priority-table th:nth-child(2) { width: 42%; }
|
|
|
|
|
|
.swot-table textarea,
|
|
|
.strategy-table textarea,
|
|
|
.priority-table textarea,
|
|
|
.risk-grid textarea,
|
|
|
.conclusion-textarea {
|
|
|
box-sizing: border-box;
|
|
|
display: block;
|
|
|
width: 100%;
|
|
|
min-height: 112px;
|
|
|
resize: vertical;
|
|
|
border: 0;
|
|
|
padding: 8px 10px;
|
|
|
color: var(--student-text);
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.swot-table textarea:focus,
|
|
|
.strategy-table textarea:focus,
|
|
|
.priority-table textarea:focus,
|
|
|
.risk-grid textarea:focus,
|
|
|
.conclusion-textarea:focus {
|
|
|
outline: 2px solid rgba(37, 99, 235, 0.15);
|
|
|
outline-offset: -2px;
|
|
|
}
|
|
|
|
|
|
.swot-table textarea:disabled,
|
|
|
.strategy-table textarea:disabled,
|
|
|
.priority-table textarea:disabled,
|
|
|
.risk-grid textarea:disabled,
|
|
|
.conclusion-textarea:disabled {
|
|
|
cursor: not-allowed;
|
|
|
color: var(--student-text-secondary);
|
|
|
background: var(--student-accent-soft);
|
|
|
}
|
|
|
|
|
|
/* ===== 决策区 ===== */
|
|
|
.decision-section {
|
|
|
margin-top: 20px;
|
|
|
padding: 14px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.decision-section h3 {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h2);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.decision-section > p {
|
|
|
margin: 6px 0 10px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.section-tip {
|
|
|
color: var(--student-accent) !important;
|
|
|
font-size: var(--student-fs-body-sm) !important;
|
|
|
}
|
|
|
|
|
|
.priority-table select {
|
|
|
min-width: 90px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 6px;
|
|
|
padding: 6px 8px;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.priority-strategy {
|
|
|
padding: 10px 12px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.priority-strategy strong {
|
|
|
color: var(--student-text);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.priority-strategy p {
|
|
|
margin: 4px 0 0;
|
|
|
white-space: pre-line;
|
|
|
}
|
|
|
|
|
|
.priority-table textarea { min-height: 96px; }
|
|
|
|
|
|
.risk-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.risk-grid label {
|
|
|
display: grid;
|
|
|
gap: 6px;
|
|
|
color: var(--student-text);
|
|
|
font-weight: 600;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.risk-grid textarea {
|
|
|
min-height: 120px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 6px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.conclusion-textarea {
|
|
|
min-height: 160px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 6px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
/* ===== 底部按钮 ===== */
|
|
|
.task-actions {
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
gap: 10px;
|
|
|
margin-top: 18px;
|
|
|
}
|
|
|
|
|
|
.btn {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
text-shadow: none;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, transform 0.16s ease, box-shadow 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.btn:hover {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-accent);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
|
|
|
.btn.btn-primary {
|
|
|
border-color: var(--student-accent);
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font-weight: 700;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28);
|
|
|
}
|
|
|
|
|
|
.btn.btn-primary:hover {
|
|
|
background: var(--student-accent-deep);
|
|
|
border-color: var(--student-accent-deep);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32);
|
|
|
}
|
|
|
|
|
|
.btn:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
transform: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.outline-action {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
font-size: 13px;
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
text-shadow: none;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.outline-action:hover {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
.step-placeholder {
|
|
|
padding: 60px 20px;
|
|
|
text-align: center;
|
|
|
color: var(--student-text-secondary);
|
|
|
}
|
|
|
|
|
|
.step-placeholder p {
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h2);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
/* ===== 响应式 ===== */
|
|
|
@media (max-width: 720px) {
|
|
|
.task-header { flex-direction: column; align-items: flex-start; }
|
|
|
.workbench-head { justify-content: flex-start; flex-wrap: wrap; }
|
|
|
.step-title-row > span { display: none; }
|
|
|
.risk-grid { grid-template-columns: 1fr; }
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<style lang="scss">
|
|
|
/* ===== 步骤导航:分段连接胶囊(教育蓝主题) ===== */
|
|
|
#app .studentLayout section.app-main:has(.competitive-analysis-page) .step-indicator {
|
|
|
display: flex !important;
|
|
|
flex-wrap: nowrap !important;
|
|
|
align-items: stretch !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 0 !important;
|
|
|
width: 100% !important;
|
|
|
margin: 0 0 20px !important;
|
|
|
padding: 0 !important;
|
|
|
border: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: transparent !important;
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
.step-connector { display: none !important; }
|
|
|
|
|
|
.step-tab {
|
|
|
flex: 1 1 0 !important;
|
|
|
max-width: 240px !important;
|
|
|
min-width: 0 !important;
|
|
|
display: flex !important;
|
|
|
flex-direction: row !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 8px !important;
|
|
|
height: auto !important;
|
|
|
min-height: 40px !important;
|
|
|
margin: 0 !important;
|
|
|
padding: 8px 14px !important;
|
|
|
border: 1px solid #e5e7eb !important;
|
|
|
border-right: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1.3 !important;
|
|
|
letter-spacing: 0 !important;
|
|
|
white-space: normal !important;
|
|
|
text-align: center !important;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease !important;
|
|
|
|
|
|
&:first-child { border-radius: 8px 0 0 8px !important; }
|
|
|
&:last-child { border-radius: 0 8px 8px 0 !important; border-right: 1px solid #e5e7eb !important; }
|
|
|
|
|
|
.step-marker {
|
|
|
position: static !important;
|
|
|
flex: 0 0 auto !important;
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
min-width: 22px !important;
|
|
|
height: 22px !important;
|
|
|
padding: 0 5px !important;
|
|
|
border: none !important;
|
|
|
border-radius: 6px !important;
|
|
|
background: #f3f4f6 !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-family: var(--student-font-data), ui-monospace, "SF Mono", Consolas, monospace !important;
|
|
|
font-size: 12px !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: all 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
.step-copy {
|
|
|
display: flex !important;
|
|
|
flex-direction: column !important;
|
|
|
gap: 0 !important;
|
|
|
min-width: 0 !important;
|
|
|
}
|
|
|
|
|
|
.step-copy strong {
|
|
|
overflow: hidden !important;
|
|
|
text-overflow: ellipsis !important;
|
|
|
white-space: normal !important;
|
|
|
line-height: 1.3 !important;
|
|
|
color: inherit !important;
|
|
|
font-weight: 600 !important;
|
|
|
font-size: 13px !important;
|
|
|
}
|
|
|
|
|
|
.step-copy small { display: none !important; }
|
|
|
|
|
|
&:hover:not(.active):not(.complete) {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
color: #2563eb !important;
|
|
|
z-index: 1 !important;
|
|
|
.step-marker { background: #2563eb !important; color: #ffffff !important; }
|
|
|
}
|
|
|
|
|
|
&.complete:not(.active) {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #374151 !important;
|
|
|
.step-marker { background: rgba(43, 164, 113, 0.12) !important; color: #16915f !important; }
|
|
|
&:hover {
|
|
|
border-color: #2ba471 !important;
|
|
|
background: #f0fdf6 !important;
|
|
|
z-index: 1 !important;
|
|
|
.step-marker { background: #2ba471 !important; color: #ffffff !important; }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.active {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
font-weight: 700 !important;
|
|
|
z-index: 2 !important;
|
|
|
.step-marker { background: #ffffff !important; color: #2563eb !important; }
|
|
|
&:hover {
|
|
|
background: #1d4ed8 !important;
|
|
|
border-color: #1d4ed8 !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
#app .studentLayout section.app-main:has(.competitive-analysis-page) .step-indicator {
|
|
|
justify-content: flex-start !important;
|
|
|
overflow-x: auto !important;
|
|
|
}
|
|
|
#app .studentLayout section.app-main:has(.competitive-analysis-page) .step-indicator .step-tab {
|
|
|
flex: 0 0 140px !important;
|
|
|
max-width: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 顶部任务工具栏换肤 ===== */
|
|
|
.competitive-analysis-page .task-toolbar-button.el-button {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 6px !important;
|
|
|
min-height: 34px !important;
|
|
|
height: 34px !important;
|
|
|
padding: 0 12px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
|
|
|
&:hover:not(.is-disabled) {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
transform: translateY(-1px) !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page .task-toolbar-button--submit {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.35), 0 3px 10px rgba(37, 99, 235, 0.22) !important;
|
|
|
&:hover:not(.is-disabled) {
|
|
|
background: #1d4ed8 !important;
|
|
|
box-shadow: 0 2px 4px rgba(37, 99, 235, 0.4), 0 5px 14px rgba(29, 78, 216, 0.28) !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 顶部工具栏:TrainingMaterialButton 资料按钮 → 教育蓝
|
|
|
* ============================================================ */
|
|
|
.competitive-analysis-page .analysis-toolbar .material-button.el-button {
|
|
|
height: 34px !important;
|
|
|
min-height: 34px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
|
|
|
&.is-plain {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #9ca3af !important;
|
|
|
&:hover {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #9ca3af !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 顶部导出按钮(原生 <button.outline-action>)
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.competitive-analysis-page) .analysis-toolbar > button.outline-action {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 6px !important;
|
|
|
height: 34px !important;
|
|
|
min-height: 34px !important;
|
|
|
padding: 0 14px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.competitive-analysis-page) .analysis-toolbar > button.outline-action:hover {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
transform: none !important;
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 步骤操作按钮:task-actions > button.btn
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.competitive-analysis-page) .task-actions {
|
|
|
gap: 14px !important;
|
|
|
margin-top: 20px !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.competitive-analysis-page) .task-actions > button {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 8px !important;
|
|
|
height: 34px !important;
|
|
|
min-height: 34px !important;
|
|
|
padding: 0 14px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.competitive-analysis-page) .task-actions > button:hover:not(:disabled) {
|
|
|
transform: translateY(-1px) !important;
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.competitive-analysis-page) .task-actions > button.btn-primary {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
background: #2563eb !important;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.competitive-analysis-page) .task-actions > button.btn-primary:hover:not(:disabled) {
|
|
|
border-color: #1d4ed8 !important;
|
|
|
color: #ffffff !important;
|
|
|
background: #1d4ed8 !important;
|
|
|
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32) !important;
|
|
|
}
|
|
|
</style>
|