You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/demand/competitive-environment-ana...

957 lines
45 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>