|
|
<template>
|
|
|
<div class="competitive-analysis-page student-training-shell">
|
|
|
<main class="analysis-core">
|
|
|
<section class="task-header"><div class="task-badge"><el-icon><TrendCharts /></el-icon> 核心实训 · 市场洞察与需求分析</div><h1>{{ taskTitle }}</h1></section>
|
|
|
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
|
|
|
<section class="analysis-workbench">
|
|
|
<div class="workbench-head"><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /><button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon> 导出实训成果</button></div>
|
|
|
<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><textarea v-model.trim="form[row.key]" :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><textarea v-model.trim="supplementForm[row.key]" :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><textarea v-model.trim="strategyForm[row.key].recommendation" :disabled="!stepTwoPassed" placeholder="填写具体策略建议" /></td><td><textarea v-model.trim="strategyForm[row.key].expectedEffect" :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>请选择该产品进入目标市场的总体竞争判断:</p><div class="radio-options"><label v-for="option in evaluationOptions" :key="option"><input v-model="finalForm.overallEvaluation" :disabled="!stepThreePassed" type="radio" name="competitive-evaluation" :value="option" /><span>{{ option }}</span></label></div></section>
|
|
|
<section class="decision-section"><h3>(2)优先策略排序</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><select v-model.number="finalForm.priorities[row.key].priority" :disabled="!stepThreePassed" :aria-label="`${row.label} 的优先级排序`"><option :value="null">请选择</option><option v-for="rank in 4" :key="rank" :value="rank">{{ rank }}</option></select></td><td class="priority-strategy"><strong>{{ row.label }}</strong><p>{{ strategyForm[row.key].recommendation || '请先在第 3 步填写该策略建议' }}</p></td><td><textarea v-model.trim="finalForm.priorities[row.key].reason" :disabled="!stepThreePassed" placeholder="说明该策略的排序理由" /></td></tr></tbody></table></div></section>
|
|
|
<section class="decision-section"><h3>(3)需关注的风险点</h3><p>请至少指出 2 个风险点。</p><div class="risk-grid"><label><span>核心风险点 1</span><textarea v-model.trim="finalForm.coreRiskOne" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>核心风险点 2</span><textarea v-model.trim="finalForm.coreRiskTwo" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>其他风险点</span><textarea v-model.trim="finalForm.otherRisk" :disabled="!stepThreePassed" placeholder="可补充其他风险点" /></label></div></section>
|
|
|
<section class="decision-section"><h3>(4)总结 SWOT 竞争环境评估结论</h3><textarea v-model.trim="finalForm.conclusion" class="conclusion-textarea" :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 useUserStore from "@/store/modules/user";
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
|
|
|
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
|
|
|
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.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 evaluationOptions = ["优势明显,建议快速进入", "机会大于挑战,建议积极布局", "挑战与机会并存,需谨慎推进", "劣势突出,建议暂缓进入"];
|
|
|
const taskConfig = ref(null); 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(() => parseArray(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 { overallEvaluation: "", 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({ overallEvaluation: finalForm.value.overallEvaluation, 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 || "请完成总体判断、策略排序、风险点和总结结论"); } 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 { display: grid; grid-template-columns: minmax(720px,1fr) 340px; gap: 24px; min-height: calc(100vh - 106px); padding: 24px; color: #e8f7ff; background: #06111d; }.analysis-core { min-width: 0; border: 1px solid rgba(0,180,255,.25); background: rgba(8,18,28,.6); box-shadow: 0 22px 48px rgba(0,0,0,.22); }.task-header { padding: 22px 28px 4px; }.task-badge,.step-title-row p { color: #65dcff; font-weight: 700; }.task-header h1 { margin: 10px 0 0; color: #fff; font-size: 28px; }.analysis-workbench { margin: 24px; padding: 22px; border: 1px solid rgba(33,194,255,.35); border-radius: 20px; background: rgba(1,24,39,.72); }.workbench-head { display: flex; justify-content: flex-end; gap: 12px; }.outline-action,.btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; border: 1px solid #2cc9ff; border-radius: 999px; padding: 10px 18px; color: #dff8ff; background: rgba(8,87,121,.55); cursor: pointer; }.step-indicator { display: flex; align-items: center; gap: 12px; margin: 28px 0; padding: 12px; border: 1px solid rgba(30,190,255,.28); border-radius: 22px; }.step-tab { display: flex; align-items: center; flex: 1; gap: 10px; min-width: 0; padding: 10px; border: 0; border-radius: 16px; color: #96c6d8; background: transparent; text-align: left; cursor: pointer; }.step-tab.active { color: #fff; background: linear-gradient(110deg,#148fc6,#0879aa); }.step-marker { display: grid; place-items: center; flex: 0 0 42px; width: 42px; height: 42px; border: 1px solid #4ed7ff; border-radius: 50%; font-weight: 800; }.step-copy { display: grid; gap: 4px; min-width: 0; }.step-copy strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.step-copy small { color: #5ec9ed; }.step-connector { flex: 0 0 25px; height: 2px; background: rgba(83,204,255,.4); }.step-card { padding: 24px; border: 1px solid rgba(31,185,249,.32); border-radius: 18px; background: rgba(1,21,34,.65); }.step-title-row { display: flex; justify-content: space-between; align-items: flex-start; }.step-title-row p { margin: 0; }.step-title-row h2 { margin: 5px 0 0; color: #fff; font-size: 24px; }.step-title-row > span { border: 1px solid #38caff; border-radius: 999px; padding: 7px 12px; color: #9aeaff; }.step-desc { margin: 24px 0; color: #c5e4ef; font-size: 16px; line-height: 1.8; }.research-guide { margin: 22px 0 18px; padding: 16px 18px; border-left: 3px solid #35cfff; color: #c5e4ef; background: rgba(13,95,129,.16); line-height: 1.8; }.research-guide p { margin: 0 0 8px; }.research-guide ol { margin: 0 0 8px; padding-left: 24px; }.research-guide li + li { margin-top: 6px; }.step-warning { margin: 12px 0; color: #ffd58e; }.swot-table-wrap,.strategy-table-wrap,.priority-table-wrap { overflow-x: auto; }.swot-table { width: 100%; min-width: 720px; border-collapse: collapse; }.swot-table th,.swot-table td,.strategy-table th,.strategy-table td,.priority-table th,.priority-table td { border: 1px solid rgba(139,213,241,.45); }.swot-table th,.strategy-table th,.priority-table th { padding: 14px; color: #fff; background: #079ed9; font-size: 16px; }.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; }.swot-table textarea,.strategy-table textarea,.priority-table textarea,.risk-grid textarea,.conclusion-textarea { box-sizing: border-box; display: block; width: 100%; min-height: 136px; resize: vertical; border: 0; padding: 16px; color: #e9faff; background: transparent; font: inherit; line-height: 1.7; }.swot-table textarea:focus,.strategy-table textarea:focus,.priority-table textarea:focus,.risk-grid textarea:focus,.conclusion-textarea:focus { outline: 2px solid rgba(57,207,255,.4); 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: #8aa8b5; background: rgba(255,255,255,.03); }.strategy-table { width: 100%; min-width: 980px; border-collapse: collapse; }.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: 12px 14px; color: #cdebf5; line-height: 1.7; }.strategy-table .logic-cell p { margin: 0 0 10px; }.strategy-table .logic-cell p:last-child { margin-bottom: 0; }.strategy-table .logic-cell strong { color: #68dcff; font-size: 13px; }.decision-section { margin-top: 30px; padding: 20px; border: 1px solid rgba(66,203,252,.24); border-radius: 14px; background: linear-gradient(130deg,rgba(10,55,79,.32),rgba(2,20,34,.24)); }.decision-section h3 { margin: 0; color: #fff; font-size: 18px; }.decision-section > p { margin: 12px 0 16px; color: #c5e4ef; line-height: 1.7; }.section-tip { color: #82dcfa !important; }.radio-options { display: flex; flex-wrap: wrap; gap: 14px 30px; }.radio-options label { display: inline-flex; align-items: center; gap: 8px; color: #dff6ff; cursor: pointer; }.radio-options input { width: 17px; height: 17px; accent-color: #16bcf0; }.priority-table { width: 100%; min-width: 820px; border-collapse: collapse; }.priority-table th:first-child,.priority-table td:first-child { width: 16%; text-align: center; }.priority-table th:nth-child(2) { width: 42%; }.priority-table select { min-width: 90px; border: 1px solid rgba(102,218,255,.55); border-radius: 6px; padding: 8px 10px; color: #e9faff; background: #09293c; font: inherit; }.priority-strategy { padding: 14px 16px; color: #d9f2fb; line-height: 1.65; }.priority-strategy strong { color: #69dcff; }.priority-strategy p { margin: 7px 0 0; white-space: pre-line; }.priority-table textarea { min-height: 112px; }.risk-grid { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 18px; }.risk-grid label { display: grid; gap: 10px; color: #e7f8ff; font-weight: 700; }.risk-grid textarea { min-height: 154px; border: 1px solid rgba(139,213,241,.45); background: rgba(0,0,0,.08); }.conclusion-textarea { min-height: 190px; border: 1px solid rgba(139,213,241,.45); background: rgba(0,0,0,.08); }.task-actions { display: flex; justify-content: center; gap: 12px; margin-top: 26px; }.btn-primary { border-color: #36d9ff; background: linear-gradient(110deg,#0eaae4,#0572ac); font-weight: 700; }.btn:disabled { cursor: not-allowed; opacity: .6; }.step-placeholder { padding: 90px 20px; text-align: center; color: #a8cfdd; }.step-placeholder p { color: #fff; font-size: 20px; } @media (max-width:1280px) { .competitive-analysis-page { grid-template-columns: 1fr; }.step-indicator { overflow-x: auto; }.step-tab { min-width: 190px; } } @media (max-width:720px) { .competitive-analysis-page { padding: 12px; }.analysis-workbench { margin: 12px; padding: 14px; }.task-header { padding: 18px; }.workbench-head { justify-content: flex-start; flex-wrap: wrap; }.step-title-row > span { display: none; }.risk-grid { grid-template-columns: 1fr; } }
|
|
|
</style>
|