|
|
<template>
|
|
|
<div class="industry-pest-page student-training-shell">
|
|
|
<main class="industry-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="submitTraining" :on-reset="resetTraining"
|
|
|
:on-export="exportOutcome" />
|
|
|
</section>
|
|
|
|
|
|
<section class="industry-workbench">
|
|
|
<nav class="step-indicator" aria-label="行业需求分析步骤">
|
|
|
<template v-for="(step, index) in trainingSteps" :key="`${step}-${index}`">
|
|
|
<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 class="step-name" :title="step">{{ step }}</strong><span
|
|
|
class="step-action">{{ currentStep === index + 1 ? '当前步骤' : '点击切换' }}
|
|
|
→</span></span>
|
|
|
</button>
|
|
|
<span v-if="index < trainingSteps.length - 1" class="step-connector" />
|
|
|
</template>
|
|
|
</nav>
|
|
|
|
|
|
<section class="industry-step-card">
|
|
|
<template v-if="currentStep === 1">
|
|
|
<div class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step 01</p>
|
|
|
<h3>{{ trainingSteps[0] }}</h3>
|
|
|
</div><span class="step-chip">PEST 分类练习</span>
|
|
|
</div>
|
|
|
<p class="step-desc">请根据案例文档中的案例信息进行 PEST 分析,进行上述各项内容归类,把文档中相应内容填写到以下四类中。</p>
|
|
|
<TrainingStepOneChoiceQuestions :questions="taskConfig?.stepOneQuestions"
|
|
|
v-model="stepOneChoiceAnswers" />
|
|
|
<div class="pest-table-wrap">
|
|
|
<table class="pest-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>序号</th>
|
|
|
<th>PEST类型</th>
|
|
|
<th>信息资讯</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="(row, index) in pestRows" :key="row.key">
|
|
|
<td>{{ index + 1 }}</td>
|
|
|
<td class="type-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 class="section-eyebrow">Step 02</p>
|
|
|
<h3>{{ trainingSteps[1] }}</h3>
|
|
|
</div><span class="step-chip">资料补充</span>
|
|
|
</div>
|
|
|
<div class="research-guide">
|
|
|
<p>请自行收集资料,补充 PEST 完善,每个维度至少补充 1 条。信息收集可参考以下方法:</p>
|
|
|
<ol>
|
|
|
<li>自行通过互联网平台搜索、电商平台(京东/天猫搜索“智能健身镜”)、行业报告网站(艾瑞咨询、前瞻产业研究院等)、新闻资讯(百度/头条搜索健身行业关键词)。</li>
|
|
|
<li>使用 AI 大模型智能搜索功能进行信息收集,参考提示词见实训任务案例文档。</li>
|
|
|
</ol>
|
|
|
<p>请把补充的 PEST 信息资讯填入下表:</p>
|
|
|
</div>
|
|
|
<p v-if="!stepOnePassed" class="step-warning">请先完成并保存第 1 步的 PEST 信息归类,再填写本步骤。</p>
|
|
|
<div class="pest-table-wrap">
|
|
|
<table class="pest-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>序号</th>
|
|
|
<th>PEST类型</th>
|
|
|
<th>补充信息资讯</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="(row, index) in pestRows" :key="row.key">
|
|
|
<td>{{ index + 1 }}</td>
|
|
|
<td class="type-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 class="section-eyebrow">Step 03</p>
|
|
|
<h3>{{ trainingSteps[2] }}</h3>
|
|
|
</div><span class="step-chip">机会与挑战</span>
|
|
|
</div>
|
|
|
<p class="step-desc">基于上述 PEST 信息归类与补充的信息,从每个 PEST 维度分别提炼该产品面临的核心机遇与潜在挑战,并给出应对思路。每个单元格至少填写 1
|
|
|
条,可按换行分条。</p>
|
|
|
<p v-if="!stepTwoPassed" class="step-warning">请先完成并保存第 2 步的 PEST 补充资讯,再填写本步骤。</p>
|
|
|
<div class="analysis-table-wrap">
|
|
|
<table class="analysis-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>PEST维度</th>
|
|
|
<th>核心机遇(至少1条)</th>
|
|
|
<th>潜在挑战(至少1条)</th>
|
|
|
<th>应对思路建议</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="row in pestAnalysisRows" :key="row.key">
|
|
|
<td class="type-cell">{{ row.label }}</td>
|
|
|
<td><el-input v-model.trim="analysisForm[row.key].opportunities" type="textarea"
|
|
|
:autosize="{ minRows: 2 }" :disabled="!stepTwoPassed"
|
|
|
placeholder="例如:政策、需求或技术带来的增长机会" /></td>
|
|
|
<td><el-input v-model.trim="analysisForm[row.key].challenges" type="textarea"
|
|
|
:autosize="{ minRows: 2 }" :disabled="!stepTwoPassed"
|
|
|
placeholder="例如:合规、成本、竞争或使用门槛" /></td>
|
|
|
<td><el-input v-model.trim="analysisForm[row.key].strategies" 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 class="section-eyebrow">Step 04</p>
|
|
|
<h3>{{ trainingSteps[3] }}</h3>
|
|
|
</div><span class="step-chip">综合结论</span>
|
|
|
</div>
|
|
|
<p class="step-desc">基于前述 PEST 分析,完成行业环境评估、市场进入判断与风险总结。</p>
|
|
|
<p v-if="!stepThreePassed" class="step-warning">请先完成并保存第 3 步的机会与挑战分析,再填写本步骤。</p>
|
|
|
<div class="conclusion-form" :class="{ 'is-disabled': !stepThreePassed }">
|
|
|
<section class="conclusion-section">
|
|
|
<h4>(1)总体 PEST 环境评估</h4>
|
|
|
<p>概括当前宏观环境对该产品的总体友好程度,从以下五个等级中选择一个:</p>
|
|
|
<div class="option-row"><el-radio-group v-model="conclusionForm.overallAssessment"
|
|
|
:disabled="!stepThreePassed" class="option-row-group"><el-radio
|
|
|
v-for="item in assessmentOptions" :key="item" :label="item"
|
|
|
class="option-card">{{ item }}</el-radio></el-radio-group></div>
|
|
|
</section>
|
|
|
<section class="conclusion-section">
|
|
|
<h4>(2)市场进入结论</h4>
|
|
|
<p>从以下三个结论中选择一个,并说明理由:</p>
|
|
|
<div class="option-row decision-options"><el-radio-group
|
|
|
v-model="conclusionForm.marketDecision" :disabled="!stepThreePassed"
|
|
|
class="option-row-group"><el-radio v-for="item in decisionOptions" :key="item"
|
|
|
:label="item" class="option-card">{{ item }}</el-radio></el-radio-group>
|
|
|
</div><el-input v-model.trim="conclusionForm.decisionReason" type="textarea"
|
|
|
:autosize="{ minRows: 2 }" :disabled="!stepThreePassed"
|
|
|
placeholder="请说明市场进入结论的理由" />
|
|
|
</section>
|
|
|
<section class="conclusion-section">
|
|
|
<h4>(3)市场进入需要关注的风险点</h4>
|
|
|
<p>请指出 2 个以上的风险点。</p>
|
|
|
<div class="risk-grid"><label>核心风险点 1:<el-input v-model.trim="conclusionForm.riskOne"
|
|
|
type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepThreePassed"
|
|
|
placeholder="填写核心风险点" /></label><label>核心风险点 2:<el-input
|
|
|
v-model.trim="conclusionForm.riskTwo" type="textarea"
|
|
|
:autosize="{ minRows: 2 }" :disabled="!stepThreePassed"
|
|
|
placeholder="填写核心风险点" /></label><label>其他风险点:<el-input
|
|
|
v-model.trim="conclusionForm.otherRisks" type="textarea"
|
|
|
:autosize="{ minRows: 2 }" :disabled="!stepThreePassed"
|
|
|
placeholder="可补充其他风险点" /></label></div>
|
|
|
</section>
|
|
|
<section class="conclusion-section">
|
|
|
<h4>(4)总结 PEST 环境评估结论</h4><el-input v-model.trim="conclusionForm.summary"
|
|
|
type="textarea" :autosize="{ minRows: 3 }" :disabled="!stepThreePassed"
|
|
|
placeholder="总结该产品的 PEST 环境评估结论" />
|
|
|
</section>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<div v-else class="step-placeholder">
|
|
|
<p>第 {{ currentStep }} 步内容待配置</p><span>前序步骤已保存,可从上方步骤栏返回查看。</span>
|
|
|
</div>
|
|
|
<div class="step-navigation">
|
|
|
<div class="step-nav-left">
|
|
|
<button v-if="currentStep > 1" type="button" class="btn-nav btn-ghost" :disabled="saving"
|
|
|
@click="previousStep">
|
|
|
<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">
|
|
|
下一步
|
|
|
<el-icon>
|
|
|
<ArrowRight />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
</section>
|
|
|
</main>
|
|
|
<TrainingAiSidebar study-tip="先从案例中提取宏观环境信息,再按政治、经济、社会、技术四个维度归类和补充。" reference-title="PEST 分类提示"
|
|
|
reference-text="· 政治:政策、法规、认证、税收、数据合规<br />· 经济:市场规模、成本、商业模式、供应链<br />· 社会:人群需求、消费趋势、生活方式与场景<br />· 技术:产品技术、平台能力、设备互联与创新"
|
|
|
:progress-items="progressItems" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, DataAnalysis } from "@element-plus/icons-vue"
|
|
|
import { checkIndustryDemandAnalysisStepOne, checkIndustryDemandAnalysisStepTwo, checkIndustryDemandAnalysisStepThree, checkIndustryDemandAnalysisStepFour, deleteStudentTrainingAnswer, 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 = "industry-demand-analysis"
|
|
|
const { proxy } = getCurrentInstance()
|
|
|
const userStore = useUserStore()
|
|
|
const DEFAULT_STEPS = ["PEST宏观环境分析", "行业机会识别", "需求判断", "分析结论"]
|
|
|
const pestRows = [
|
|
|
{ key: "political", label: "政治环境", placeholder: "填写政策、法规、认证、税收或数据合规等信息" },
|
|
|
{ key: "economic", label: "经济环境", placeholder: "填写市场规模、成本、商业模式或供应链等信息" },
|
|
|
{ key: "social", label: "社会环境", placeholder: "填写用户需求、消费趋势、生活方式或场景变化等信息" },
|
|
|
{ key: "technology", label: "技术环境", placeholder: "填写产品技术、平台能力、设备互联或创新等信息" },
|
|
|
]
|
|
|
const pestAnalysisRows = [
|
|
|
{ key: "political", label: "政治环境(P)" },
|
|
|
{ key: "economic", label: "经济环境(E)" },
|
|
|
{ key: "social", label: "社会环境(S)" },
|
|
|
{ key: "technology", label: "技术环境(T)" },
|
|
|
]
|
|
|
const assessmentOptions = ["高度友好", "较为友好", "中性", "存在风险", "风险较高"]
|
|
|
const decisionOptions = ["建议进入", "谨慎进入", "建议暂缓"]
|
|
|
const defaultTask = {
|
|
|
title: "行业需求分析",
|
|
|
background: "通过 PEST 模型识别影响行业需求的宏观环境因素,为后续产品机会判断提供依据。",
|
|
|
goals: ["掌握 PEST 四维度的分类方法", "提炼案例中的宏观环境信息", "形成行业需求分析的基础材料"],
|
|
|
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 stepOneChoiceAnswers = ref({})
|
|
|
const supplementForm = ref(createForm())
|
|
|
const analysisForm = ref(createAnalysisForm())
|
|
|
const conclusionForm = ref(createConclusionForm())
|
|
|
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(() => pestRows.map((row) => ({ text: `${conclusionForm.value.summary ? "已总结" : analysisForm.value[row.key].opportunities ? "已分析" : supplementForm.value[row.key] ? "已补充" : form.value[row.key] ? "已归类" : "待填写"}:${row.label}`, status: conclusionForm.value.summary || analysisForm.value[row.key].opportunities || 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 { political: "", economic: "", social: "", technology: "" } }
|
|
|
function createAnalysisForm() { return { political: createAnalysisRow(), economic: createAnalysisRow(), social: createAnalysisRow(), technology: createAnalysisRow() } }
|
|
|
function createAnalysisRow() { return { opportunities: "", challenges: "", strategies: "" } }
|
|
|
function createConclusionForm() { return { overallAssessment: "", marketDecision: "", decisionReason: "", riskOne: "", riskTwo: "", otherRisks: "", summary: "" } }
|
|
|
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) { restoreAnalysisOutcome(answer.step3Answer); stepThreePassed.value = true }
|
|
|
if (answer?.step4Answer) restoreConclusionOutcome(answer.step4Answer)
|
|
|
if (Number(answer?.currentStep) > 0) currentStep.value = Number(answer.currentStep)
|
|
|
} catch (error) { /* Keep the local draft when the answer endpoint is unavailable. */ }
|
|
|
}
|
|
|
function restoreOutcome(value, target, field = "pest") { try { const parsed = typeof value === "string" ? JSON.parse(value) : value; target.value = { ...createForm(), ...(parsed?.[field] || parsed || {}) }; if (field === "pest" && parsed?.choiceAnswers && typeof parsed.choiceAnswers === "object") stepOneChoiceAnswers.value = parsed.choiceAnswers } catch (error) { target.value = createForm() } }
|
|
|
function restoreAnalysisOutcome(value) { try { const parsed = typeof value === "string" ? JSON.parse(value) : value; const source = parsed?.analysis || parsed || {}; analysisForm.value = Object.fromEntries(pestAnalysisRows.map((row) => [row.key, { ...createAnalysisRow(), ...(source[row.key] || {}) }])) } catch (error) { analysisForm.value = createAnalysisForm() } }
|
|
|
function restoreConclusionOutcome(value) { try { const parsed = typeof value === "string" ? JSON.parse(value) : value; conclusionForm.value = { ...createConclusionForm(), ...(parsed?.conclusion || parsed || {}) } } catch (error) { conclusionForm.value = createConclusionForm() } }
|
|
|
function buildStepOneOutcome() { return { title: taskTitle.value, pest: { ...form.value }, choiceAnswers: { ...stepOneChoiceAnswers.value } } }
|
|
|
function buildStepTwoOutcome() { return { title: taskTitle.value, supplements: { ...supplementForm.value } } }
|
|
|
function buildStepThreeOutcome() { return { title: taskTitle.value, analysis: JSON.parse(JSON.stringify(analysisForm.value)) } }
|
|
|
function buildStepFourOutcome() { return { title: taskTitle.value, conclusion: { ...conclusionForm.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) { /* Browser draft is optional. */ } }
|
|
|
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"); restoreAnalysisOutcome(draft?.step3); restoreConclusionOutcome(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 checkIndustryDemandAnalysisStepOne(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 || "请完整填写 PEST 四类信息") } finally { saving.value = false } }
|
|
|
async function saveAndGoToStepThree() { if (saving.value || !stepOnePassed.value) return; saving.value = true; try { await checkIndustryDemandAnalysisStepTwo(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 || "请为每个 PEST 维度至少补充 1 条信息") } finally { saving.value = false } }
|
|
|
async function saveAndGoToStepFour() { if (saving.value || !stepTwoPassed.value) return; saving.value = true; try { await checkIndustryDemandAnalysisStepThree({ rows: pestAnalysisRows.map((row) => ({ dimension: row.key, ...analysisForm.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 || "请完成每个 PEST 维度的机遇、挑战和应对思路") } finally { saving.value = false } }
|
|
|
async function submitTraining() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkIndustryDemandAnalysisStepFour(conclusionForm.value); 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 || "请完成 PEST 环境评估结论") } finally { saving.value = false } }
|
|
|
async function saveCurrentStepFour() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkIndustryDemandAnalysisStepFour(conclusionForm.value); await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); clearDraft(); proxy?.$modal?.msgSuccess("第 4 步已保存") } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成 PEST 环境评估结论") } finally { saving.value = false } }
|
|
|
async function saveCurrentProgress(showMessage = true) {
|
|
|
if (saving.value) return false
|
|
|
saving.value = true
|
|
|
try {
|
|
|
const payloads = {
|
|
|
1: async () => { await checkIndustryDemandAnalysisStepOne(form.value); return { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 1 } },
|
|
|
2: async () => { if (!stepOnePassed.value) throw new Error("请先完成第 1 步"); await checkIndustryDemandAnalysisStepTwo(supplementForm.value); return { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: 2 } },
|
|
|
3: async () => { if (!stepTwoPassed.value) throw new Error("请先完成第 2 步"); await checkIndustryDemandAnalysisStepThree({ rows: pestAnalysisRows.map((row) => ({ dimension: row.key, ...analysisForm.value[row.key] })) }); return { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 3 } },
|
|
|
4: async () => { if (!stepThreePassed.value) throw new Error("请先完成第 3 步"); await checkIndustryDemandAnalysisStepFour(conclusionForm.value); return { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4 } },
|
|
|
}
|
|
|
const answer = await payloads[currentStep.value]?.()
|
|
|
if (!answer) return false
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, { ...answer, submitted: false, saveAction: "SAVE" })
|
|
|
if (currentStep.value === 1) stepOnePassed.value = true
|
|
|
if (currentStep.value === 2) stepTwoPassed.value = true
|
|
|
if (currentStep.value === 3) stepThreePassed.value = true
|
|
|
clearDraft()
|
|
|
if (showMessage) proxy?.$modal?.msgSuccess("当前步骤已保存")
|
|
|
return true
|
|
|
} catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写当前步骤内容"); return false } finally { saving.value = false }
|
|
|
}
|
|
|
function previousStep() { if (currentStep.value > 1) currentStep.value -= 1 }
|
|
|
async function nextStep() { if (currentStep.value < trainingSteps.value.length && await saveCurrentProgress(false)) currentStep.value += 1 }
|
|
|
async function resetTraining() { form.value = createForm(); stepOneChoiceAnswers.value = {}; supplementForm.value = createForm(); analysisForm.value = createAnalysisForm(); conclusionForm.value = createConclusionForm(); stepOnePassed.value = false; stepTwoPassed.value = false; stepThreePassed.value = false; currentStep.value = 1; clearDraft(); try { saving.value = true; await deleteStudentTrainingAnswer(TASK_KEY) } catch (error) { /* Local clearing still succeeds. */ } finally { saving.value = false } proxy?.$modal?.msgSuccess("已重新开始本次实训") }
|
|
|
function resetStepOne() { form.value = createForm(); stepOneChoiceAnswers.value = {}; stepOnePassed.value = false; proxy?.$modal?.msgSuccess("已清空第 1 步填写内容") }
|
|
|
function resetStepTwo() { supplementForm.value = createForm(); proxy?.$modal?.msgSuccess("已清空第 2 步填写内容") }
|
|
|
function resetStepThree() { analysisForm.value = createAnalysisForm(); proxy?.$modal?.msgSuccess("已清空第 3 步填写内容") }
|
|
|
function resetStepFour() { conclusionForm.value = createConclusionForm(); 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>
|
|
|
/* ===== 页面级令牌(教育蓝极简主题) ===== */
|
|
|
.industry-pest-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,页面不再重复声明 */
|
|
|
|
|
|
min-width: 0;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
/* 对齐 foundation 的 .training-core:只设 gap,无边框/背景/阴影 */
|
|
|
.industry-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,
|
|
|
.section-eyebrow {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-weight: 700;
|
|
|
font-size: var(--student-fs-caption);
|
|
|
letter-spacing: 0.12em;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
|
|
|
.task-title {
|
|
|
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: -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: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
ul {
|
|
|
display: grid;
|
|
|
gap: 4px;
|
|
|
margin: 0;
|
|
|
padding-left: 18px;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.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;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* step-card 层:透明容器,去掉旧的 border/bg(对齐 foundation 的 .task-card) */
|
|
|
.industry-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 h3 {
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
.step-chip {
|
|
|
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,
|
|
|
.research-guide {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.step-desc {
|
|
|
margin: 14px 0;
|
|
|
}
|
|
|
|
|
|
.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;
|
|
|
}
|
|
|
|
|
|
.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);
|
|
|
}
|
|
|
|
|
|
/* ===== 表格 ===== */
|
|
|
.pest-table-wrap,
|
|
|
.analysis-table-wrap {
|
|
|
overflow-x: auto;
|
|
|
}
|
|
|
|
|
|
.pest-table,
|
|
|
.analysis-table {
|
|
|
width: 100%;
|
|
|
min-width: 720px;
|
|
|
border-collapse: collapse;
|
|
|
}
|
|
|
|
|
|
.pest-table th,
|
|
|
.pest-table td,
|
|
|
.analysis-table th,
|
|
|
.analysis-table td {
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
}
|
|
|
|
|
|
.pest-table th,
|
|
|
.analysis-table th {
|
|
|
padding: 10px 12px;
|
|
|
color: var(--student-text);
|
|
|
background: #fafbfc;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 800;
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
.pest-table th:first-child,
|
|
|
.pest-table td:first-child,
|
|
|
.analysis-table th:first-child,
|
|
|
.analysis-table td:first-child {
|
|
|
width: 8%;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.pest-table th:nth-child(2),
|
|
|
.pest-table .type-cell {
|
|
|
width: 18%;
|
|
|
text-align: center;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.analysis-table th:nth-child(2),
|
|
|
.analysis-table .type-cell {
|
|
|
width: 14%;
|
|
|
text-align: center;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.pest-table textarea,
|
|
|
.analysis-table textarea,
|
|
|
.conclusion-section textarea {
|
|
|
box-sizing: border-box;
|
|
|
display: block;
|
|
|
width: 100%;
|
|
|
min-height: 120px;
|
|
|
resize: vertical;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 6px;
|
|
|
padding: 10px 12px;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.pest-table textarea,
|
|
|
.analysis-table textarea {
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
padding: 8px 10px;
|
|
|
}
|
|
|
|
|
|
.analysis-table textarea {
|
|
|
min-height: 140px;
|
|
|
}
|
|
|
|
|
|
.pest-table textarea:focus,
|
|
|
.analysis-table textarea:focus,
|
|
|
.conclusion-section textarea:focus {
|
|
|
outline: 2px solid rgba(37, 99, 235, 0.2);
|
|
|
outline-offset: -2px;
|
|
|
}
|
|
|
|
|
|
.pest-table textarea:disabled,
|
|
|
.analysis-table textarea:disabled,
|
|
|
.conclusion-section textarea:disabled {
|
|
|
cursor: not-allowed;
|
|
|
color: var(--student-text-secondary);
|
|
|
background: var(--student-accent-soft);
|
|
|
}
|
|
|
|
|
|
/* ===== 结论表单 ===== */
|
|
|
.conclusion-form {
|
|
|
display: grid;
|
|
|
gap: 14px;
|
|
|
}
|
|
|
|
|
|
.conclusion-section {
|
|
|
padding: 14px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.conclusion-section h4 {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h2);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.conclusion-section p {
|
|
|
margin: 6px 0 10px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.option-row {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 8px 14px;
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
.option-card {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: 6px;
|
|
|
min-width: 100px;
|
|
|
padding: 8px 12px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-text);
|
|
|
cursor: pointer;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
|
|
|
font-size: var(--student-fs-body);
|
|
|
}
|
|
|
|
|
|
.option-card:hover {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
}
|
|
|
|
|
|
.option-card input {
|
|
|
accent-color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
.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;
|
|
|
font-weight: 400;
|
|
|
font-size: var(--student-fs-body);
|
|
|
}
|
|
|
|
|
|
/* ===== 底部步骤导航按钮(scoped 基础) ===== */
|
|
|
.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-primary {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid var(--student-accent);
|
|
|
border-radius: 8px;
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.2), 0 2px 8px rgba(37, 99, 235, 0.15);
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
&: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 {
|
|
|
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-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: 18px;
|
|
|
}
|
|
|
|
|
|
&: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 {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid #e5e7eb;
|
|
|
border-radius: 8px;
|
|
|
color: #4b5563;
|
|
|
background: #f9fafb;
|
|
|
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: 18px;
|
|
|
}
|
|
|
|
|
|
&: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-family: var(--student-font-data);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.05em;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.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: 840px) {
|
|
|
.risk-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
.task-header {
|
|
|
flex-direction: column;
|
|
|
align-items: flex-start;
|
|
|
}
|
|
|
|
|
|
.step-chip {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.step-navigation {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.btn-nav {
|
|
|
width: 100%;
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<style lang="scss">
|
|
|
/* ===== 步骤导航:分段连接胶囊(教育蓝主题) ===== */
|
|
|
#app .studentLayout section.app-main:has(.industry-pest-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: 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: 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-name {
|
|
|
overflow: hidden !important;
|
|
|
text-overflow: ellipsis !important;
|
|
|
white-space: normal !important;
|
|
|
line-height: 1.3 !important;
|
|
|
color: inherit !important;
|
|
|
}
|
|
|
|
|
|
.step-action {
|
|
|
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(.industry-pest-page) .step-indicator {
|
|
|
justify-content: flex-start !important;
|
|
|
overflow-x: auto !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.industry-pest-page) .step-indicator .step-tab {
|
|
|
flex: 0 0 140px !important;
|
|
|
max-width: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 顶部任务工具栏换肤 ===== */
|
|
|
.industry-pest-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;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.industry-pest-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;
|
|
|
}
|
|
|
}
|
|
|
</style>
|