|
|
<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>
|
|
|
</section>
|
|
|
|
|
|
<section class="task-action-strip">
|
|
|
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="saveAndCompleteTraining" :on-reset="resetStepFour"
|
|
|
:on-export="exportOutcome" />
|
|
|
</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 分析,判断它在 SWOT 分析中应当归属于哪一类。</p>
|
|
|
<TrainingStepOneChoiceQuestions :questions="taskConfig?.stepOneQuestions"
|
|
|
v-model="stepOneChoiceAnswers" />
|
|
|
<!-- 表格 UI 已注销:第一步只保留知识自测,原 SWOT 表格保留代码以便日后恢复
|
|
|
<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>
|
|
|
-->
|
|
|
</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>
|
|
|
</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>
|
|
|
</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">结合以上分析,完成以下决策问题:总体竞争态势评估、策略优先级排序、风险识别与竞争环境总结。</p>
|
|
|
<p v-if="!stepThreePassed" class="step-warning">请先完成并保存第 3 步的四类 SWOT 策略,再完成最终决策。</p>
|
|
|
<section class="decision-section">
|
|
|
<div class="section-heading"><span>01</span>
|
|
|
<div>
|
|
|
<h3>案例公司进入该市场的总体竞争态势评估</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<el-radio-group v-model="finalForm.overallAssessment" class="assessment-options"
|
|
|
:disabled="!stepThreePassed">
|
|
|
<el-radio v-for="option in overallAssessmentOptions" :key="option" :label="option" />
|
|
|
</el-radio-group>
|
|
|
</section>
|
|
|
<section class="decision-section">
|
|
|
<div class="section-heading"><span>02</span>
|
|
|
<div>
|
|
|
<h3>优先策略排序</h3>
|
|
|
<p>策略内容由第 3 步填写的“具体策略建议”自动带入;请完成优先级排序并说明理由。</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<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">
|
|
|
<div class="section-heading"><span>03</span>
|
|
|
<div>
|
|
|
<h3>需关注的风险点</h3>
|
|
|
<p>请至少指出 2 个风险点。</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<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">
|
|
|
<div class="section-heading"><span>04</span>
|
|
|
<div>
|
|
|
<h3>总结 SWOT 竞争环境评估结论</h3>
|
|
|
</div>
|
|
|
</div><el-input v-model.trim="finalForm.conclusion" class="conclusion-textarea"
|
|
|
type="textarea" :autosize="{ minRows: 3 }" :disabled="!stepThreePassed"
|
|
|
placeholder="综合说明优先策略、风险应对方向及最终结论" />
|
|
|
</section>
|
|
|
</template>
|
|
|
<div v-else class="step-placeholder">
|
|
|
<p>第 {{ currentStep }} 步内容待配置</p><span>第 1 步 SWOT 分类已保存,可从上方步骤栏返回查看。</span>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<div class="step-navigation">
|
|
|
<div class="step-nav-left">
|
|
|
<button v-if="currentStep > 1" type="button" class="btn-nav btn-ghost" @click="prevStep">
|
|
|
<el-icon>
|
|
|
<ArrowLeft />
|
|
|
</el-icon>
|
|
|
上一步
|
|
|
</button>
|
|
|
<button type="button" class="btn-nav btn-save" :disabled="saving" @click="saveCurrentProgress">
|
|
|
<el-icon>
|
|
|
<CircleCheck />
|
|
|
</el-icon>
|
|
|
保存
|
|
|
</button>
|
|
|
</div>
|
|
|
<div class="step-nav-right">
|
|
|
<span class="step-progress">Step {{ String(currentStep).padStart(2, "0") }} / {{
|
|
|
String(trainingSteps.length).padStart(2, "0") }}</span>
|
|
|
<button v-if="currentStep < trainingSteps.length" type="button" class="btn-nav btn-primary"
|
|
|
:disabled="saving" @click="nextStep">
|
|
|
{{ saving ? "处理中…" : "下一步" }}
|
|
|
<el-icon>
|
|
|
<ArrowRight />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
</main>
|
|
|
<TrainingAiSidebar study-tip="先辨别内部条件与外部环境,再归类为优势、劣势、机会和威胁。" reference-title="SWOT 分类提示"
|
|
|
reference-text="· 优势:企业或产品已有的资源、能力、渠道与差异化<br />· 劣势:内部能力、成本、体验、资源或覆盖不足<br />· 机会:外部市场增长、政策变化、消费趋势与技术窗口<br />· 威胁:竞争加剧、替代品、成本风险、政策限制与需求变化"
|
|
|
:progress-items="progressItems" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, 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 TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"
|
|
|
import TrainingStepOneChoiceQuestions from "@/views/components/TrainingStepOneChoiceQuestions.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 overallAssessmentOptions = ["优势明显,建议快速进入", "机会大于挑战,建议积极布局", "挑战与机会并存,需谨慎推进", "劣势突出,建议暂缓进入"]
|
|
|
const defaultTask = { title: "竞争环境分析", background: "通过 SWOT 模型归类案例资料中的竞争环境信息,为后续竞品比较与竞争策略判断提供依据。", goals: ["掌握 SWOT 四维度的分类方法", "提炼案例中的竞争环境关键信息", "形成竞争分析的基础材料"], requirement: "请阅读案例资料,将相关信息归类为优势、劣势、机会和威胁。" }
|
|
|
const taskConfig = ref(null); const briefOpen = ref(true); 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 stepOneChoiceAnswers = ref({})
|
|
|
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: "" }])), overallAssessment: "", 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 || {}) }; if (field === "swot" && parsed?.choiceAnswers && typeof parsed.choiceAnswers === "object") stepOneChoiceAnswers.value = parsed.choiceAnswers } 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 }, choiceAnswers: { ...stepOneChoiceAnswers.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 }
|
|
|
function prevStep() { if (currentStep.value > 1) currentStep.value -= 1 }
|
|
|
async function saveCurrentProgress() { if (saving.value) return; saving.value = true; try { const step = currentStep.value; const payload = { currentStep: step, submitted: false, saveAction: "SAVE" }; if (step === 1) payload.step1Answer = JSON.stringify(buildStepOneOutcome()); else if (step === 2) payload.step2Answer = JSON.stringify(buildStepTwoOutcome()); else if (step === 3) payload.step3Answer = JSON.stringify(buildStepThreeOutcome()); else if (step === 4) payload.step4Answer = JSON.stringify(buildStepFourOutcome()); await saveStudentTrainingAnswer(TASK_KEY, payload); proxy?.$modal?.msgSuccess("已保存") } catch (error) { proxy?.$modal?.msgError?.(error?.message || "保存失败,请稍后重试") } finally { saving.value = false } }
|
|
|
async function nextStep() { if (currentStep.value === 1) return saveAndGoToStepTwo(); if (currentStep.value === 2) return saveAndGoToStepThree(); if (currentStep.value === 3) return saveAndGoToStepFour() }
|
|
|
async function saveAndGoToStepTwo() { if (saving.value) return; /* 表格 UI 已注销:跳过 checkCompetitiveEnvironmentAnalysisStepOne 表格校验,只校验知识自测 answers */ if (!Object.keys(stepOneChoiceAnswers.value || {}).length) { proxy?.$modal?.msgWarning("请先完成知识自测"); return }; saving.value = true; try { 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 || "保存失败,请稍后重试") } 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; if (!finalForm.value.overallAssessment) { proxy?.$modal?.msgError?.("请先完成“案例公司进入该市场的总体竞争态势评估”选择题"); 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(); stepOneChoiceAnswers.value = {}; 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-training-shell,页面不再重复声明 */
|
|
|
|
|
|
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: var(--student-fs-caption);
|
|
|
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: var(--student-fs-body-sm);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.task-brief-text {
|
|
|
display: block;
|
|
|
}
|
|
|
|
|
|
.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: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
.task-action-strip {
|
|
|
margin: 0;
|
|
|
padding: 10px 16px;
|
|
|
|
|
|
:deep(.training-task-toolbar) {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
justify-content: flex-end;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 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: var(--student-fs-caption);
|
|
|
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,
|
|
|
/* ===== 总体竞争态势评估选择题 ===== */
|
|
|
.assessment-options {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 8px 28px;
|
|
|
}
|
|
|
|
|
|
.assessment-options .el-radio {
|
|
|
height: 28px;
|
|
|
margin-right: 0;
|
|
|
}
|
|
|
|
|
|
.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;
|
|
|
}
|
|
|
|
|
|
.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;
|
|
|
}
|
|
|
|
|
|
/* ===== 底部导航:工具栏式 ===== */
|
|
|
.step-navigation {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 10px;
|
|
|
margin-top: 16px;
|
|
|
padding: 12px 16px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
flex-wrap: nowrap;
|
|
|
}
|
|
|
|
|
|
.step-nav-right {
|
|
|
margin-left: auto;
|
|
|
}
|
|
|
|
|
|
.btn-nav {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border-radius: 8px;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: var(--student-fs-h2);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-primary {
|
|
|
border: 1px solid var(--student-accent);
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.2), 0 2px 8px rgba(37, 99, 235, 0.15);
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent-deep);
|
|
|
background: var(--student-accent-deep);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.3), 0 4px 12px rgba(29, 78, 216, 0.22);
|
|
|
}
|
|
|
|
|
|
&:active:not(:disabled) {
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-ghost {
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-save {
|
|
|
border: 1px solid #e5e7eb;
|
|
|
color: #4b5563;
|
|
|
background: #f9fafb;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: #d1d5db;
|
|
|
background: #f3f4f6;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-primary:disabled,
|
|
|
.btn-ghost:disabled,
|
|
|
.btn-save:disabled,
|
|
|
.btn-nav:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
transform: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.step-progress {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.05em;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.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: var(--student-fs-body);
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
.step-navigation {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.btn-nav {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.risk-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 步骤内小节标题(蓝底序号,统一规范) ===== */
|
|
|
.section-heading {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
gap: 12px;
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
>span {
|
|
|
display: grid;
|
|
|
flex: 0 0 28px;
|
|
|
width: 28px;
|
|
|
height: 28px;
|
|
|
place-items: center;
|
|
|
border-radius: 6px;
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent, #2563eb);
|
|
|
font-size: var(--student-fs-body, 14px);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
h3 {
|
|
|
margin: 0;
|
|
|
color: var(--student-text, #1f2937);
|
|
|
font-size: var(--student-fs-h1, 18px);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text-secondary, #6b7280);
|
|
|
font-size: var(--student-fs-body, 14px);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 共享组件 TrainingStepOneChoiceQuestions 浅色化
|
|
|
* 组件本体为深色且禁止修改,此处用拍平的 :deep() 选择器覆盖
|
|
|
* 选择器必须保留完整类链,确保特异性压过组件 scoped 规则
|
|
|
* 复刻 industry-demand-analysis.vue 同款样式
|
|
|
* ============================================================ */
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions) {
|
|
|
margin: 16px 0;
|
|
|
padding: 16px 18px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: var(--student-card-bg);
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions header) {
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
padding-bottom: 12px;
|
|
|
border-bottom: 1px solid #f0f1f3;
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions header span) {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
padding: 3px 10px;
|
|
|
border-radius: 6px;
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-accent);
|
|
|
font-size: 13px;
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.05em;
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions header p) {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-question) {
|
|
|
padding: 14px 0 2px;
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-question + .training-step-one-question) {
|
|
|
border-top: 1px dashed #e5e7eb;
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-question h4) {
|
|
|
margin: 0 0 10px;
|
|
|
color: var(--student-text);
|
|
|
font-size: 15px;
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-question h4 b) {
|
|
|
color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions .training-step-one-question .el-radio-group) {
|
|
|
display: grid;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions .training-step-one-question .el-radio.is-bordered) {
|
|
|
width: 100%;
|
|
|
height: auto;
|
|
|
min-height: 38px;
|
|
|
margin: 0;
|
|
|
padding: 9px 12px;
|
|
|
color: var(--student-text);
|
|
|
line-height: 1.5;
|
|
|
border: 1px solid #e5e7eb;
|
|
|
border-radius: 8px;
|
|
|
background: #ffffff;
|
|
|
transition: border-color 0.15s ease, background-color 0.15s ease;
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions .training-step-one-question .el-radio.is-bordered:hover) {
|
|
|
border-color: var(--student-accent-light);
|
|
|
background: var(--student-accent-soft);
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions .training-step-one-question .el-radio.is-bordered.is-checked) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions .training-step-one-question .el-radio__label) {
|
|
|
color: var(--student-text);
|
|
|
white-space: normal;
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions .training-step-one-question .el-radio__input.is-checked + .el-radio__label) {
|
|
|
color: var(--student-text);
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions .training-step-one-question .el-radio__input.is-checked .el-radio__inner) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
.competitive-analysis-page :deep(.training-step-one-questions .training-step-one-question strong) {
|
|
|
color: var(--student-accent);
|
|
|
}
|
|
|
</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 {
|
|
|
|
|
|
|
|
|
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: var(--student-fs-body) !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1.3 !important;
|
|
|
letter-spacing: 0 !important;
|
|
|
white-space: nowrap !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: var(--student-fs-body-sm) !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: nowrap !important;
|
|
|
line-height: 1.3 !important;
|
|
|
color: inherit !important;
|
|
|
font-weight: 600 !important;
|
|
|
font-size: var(--student-fs-body) !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: var(--student-fs-body) !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: var(--student-fs-body) !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: var(--student-fs-body) !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;
|
|
|
}
|
|
|
</style>
|