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

1427 lines
43 KiB
Vue

This file contains ambiguous Unicode characters!

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

<template>
<div class="industry-demand-page student-training-shell">
<TrainingTaskToolbar />
<main class="training-core">
<section class="task-header">
<h1 class="task-title">{{ taskTitle }}</h1>
</section>
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement"
:show-material="false" />
<section class="task-card">
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName"
:material-url="taskConfig?.materialUrl" :training-data-name="taskConfig?.trainingDataName"
:training-data-url="taskConfig?.trainingDataUrl"
:guide-manual-name="taskConfig?.guideManualName"
:guide-manual-url="taskConfig?.guideManualUrl" :ai-manual-name="taskConfig?.aiManualName"
:ai-manual-url="taskConfig?.aiManualUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
<el-icon>
<Download />
</el-icon>
导出实训成果
</button>
</div>
</div>
<nav v-if="trainingSteps.length" 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 }]"
:aria-label="`切换至第 ${index + 1} 步:${step}`"
:aria-current="currentStep === index + 1 ? 'step' : undefined"
@click="changeStep(index + 1)">
<span class="step-marker" aria-hidden="true">{{ 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
aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="index < trainingSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section class="step-card industry-demand-step-card">
<div v-if="activeStepName" class="step-title-row">
<div>
<p class="section-eyebrow">Step {{ String(currentStep).padStart(2, "0") }}</p>
<h3>{{ activeStepName }}</h3>
</div>
</div>
<div class="background-block">
<h4>背景信息</h4>
<p v-for="line in backgroundLines" :key="line">{{ line }}</p>
</div>
<p class="task-desc">请根据以上信息进行PEST分析,进行上述各项内容归类。</p>
<div class="pest-grid">
<article v-for="item in pestItems" :key="item.key" class="pest-card">
<div class="pest-card__title">{{ item.label }}</div>
<textarea v-model="form.pest[item.key]" class="pest-input"
:placeholder="item.placeholder" />
</article>
</div>
<label class="field-block">
<span>通过PEST模型对智能健身镜进行分析,总结该行业核心机遇与挑战,给出合适的战略决策。</span>
<textarea v-model="form.strategy" class="long-textarea" placeholder="填写PEST综合分析、行业机遇挑战和战略决策" />
</label>
<div class="comparison-block">
<p class="comparison-title">机会与挑战对比:</p>
<div class="comparison-table">
<div class="comparison-row comparison-head">
<div>维度</div>
<div>机会点</div>
<div>风险点</div>
</div>
<div v-for="row in form.comparisons" :key="row.dimension" class="comparison-row">
<div class="dimension-cell">{{ row.dimension }}</div>
<textarea v-model="row.opportunity" :placeholder="row.opportunityHint" />
<textarea v-model="row.risk" :placeholder="row.riskHint" />
</div>
</div>
</div>
<label class="field-block">
<span>分析结论:</span>
<textarea v-model="form.conclusion" class="long-textarea"
placeholder="填写最终分析结论、适合进入的细分方向和后续产品建议" />
</label>
<div v-if="trainingSteps.length" class="step-actions">
<el-button class="step-action-btn step-action-btn--previous" :disabled="currentStep === 1"
@click="previousStep">
<el-icon>
<ArrowLeft />
</el-icon>
上一步
</el-button>
<el-button class="step-action-btn step-action-btn--save" :loading="saving"
@click="saveCurrentProgress">
<el-icon>
<CircleCheck />
</el-icon>
保存当前进度
</el-button>
<el-button class="step-action-btn step-action-btn--next"
:disabled="currentStep === trainingSteps.length" @click="nextStep">
下一步
<el-icon>
<ArrowRight />
</el-icon>
</el-button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving"
@click="submitTask">提交任务</el-button>
<el-button class="training-action training-action--reset"
@click="resetTraining">重新实训</el-button>
</div>
</section>
</section>
</main>
<TrainingAiSidebar :study-tip="`当前任务建议:先把背景信息拆到政治、经济、社会、技术四类,再总结机会、风险与战略决策。`" :progress-items="progressItems" />
</div>
</template>
<script setup>
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue"
import { getTrainingTaskByKey } from "@/api/trainingTask"
import { parseTrainingArray } from "@/views/training/taskKeyMap"
import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"
import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue"
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"
const TASK_KEY = "industry-demand-analysis"
const { proxy } = getCurrentInstance()
const taskConfig = ref(null)
const saving = ref(false)
const currentStep = ref(1)
const defaultGoals = [
"掌握PEST模型在行业需求分析中的应用方法",
"能够从政策、经济、社会、技术四个维度识别行业机会与风险",
"形成适合产品开发和市场进入的战略建议",
]
const defaultRequirement = "PEST分析用于从宏观环境拆解行业需求,帮助判断产品机会、外部约束和市场进入策略。"
const defaultBackground = "以智能健身镜行业为例,结合政策标准、市场规模、消费趋势和技术条件,完成行业需求分析与战略判断。"
const backgroundLines = [
"中国“健康中国2030”规划推动家庭健身设备普及,对符合GB/T 38644-2020标准的企业给予15%税收减免。",
"欧盟CE认证新增EN 62368-1安全标准,并强制要求碳足迹披露(SPI倡议)。",
"美国FCC认证与关税政策(如25%对华加征关税)驱动供应链向越南、墨西哥转移,利用RCEP规则规避成本。",
"欧盟《数字健身法案》要求用户数据本地化存储,中国《数据安全法》强制端侧数据处理,避免隐私泄露风险。",
"2024年全球市场规模达3.51亿美元,预计2031年突破5亿美元(CAGR 5.1%)。",
"成本结构优化:中国供应链成本较北美低37%,珠三角地区形成显示面板、传感器、ODM组装一体化产业链。",
"“硬件+订阅”主导:头部厂商ARPU值达18美元/月(如数智引力),商用场景(酒店、健身房)渗透率超60%(如Tempo)。",
"关税冲击应对:康冠科技采用“中国核心部件+墨西哥组装”模式,降低关税影响。",
"居家健身刚需化:疫情后62%用户倾向家庭场景训练,智能健身镜租约使用频次达4.7次。",
"内容需求精细化:Z世代青睐个性化课程(如亲子互动、广场舞),推动课程库扩充至200+品类。",
"都市人群对“精准健康管理”需求激增,设备需集成体脂、骨骼点追踪(精度±0.5°)及生理指标监测功能。",
"动作捕捉:3D结构光摄像头+多模态传感器,实现128个骨骼点实时追踪(商汤科技算法精度98.6%)。",
"交互体验:防眩光AG玻璃(BOE)、双频WiFi6保障4K流畅稳定性,响应延迟<0.5秒。",
"SDK开放平台(如康冠科技接入200+健身APP)构建软硬件闭环,支持与穿戴设备(心率带、手表)数据同步。",
]
const pestItems = [
{ key: "political", label: "政治环境", placeholder: "归类政策、认证、关税、数据合规等因素" },
{ key: "economic", label: "经济环境", placeholder: "归类市场规模、成本、商业模式、供应链等因素" },
{ key: "social", label: "社会环境", placeholder: "归类用户需求、消费趋势、健康管理和场景变化" },
{ key: "technology", label: "技术环境", placeholder: "归类动作捕捉、交互体验、SDK开放和设备互联能力" },
]
function createComparisons() {
return [
{
dimension: "政策",
opportunity: "",
risk: "",
opportunityHint: "中国税收减免、欧盟碳足迹政策催生绿色技术需求",
riskHint: "认证成本上升",
},
{
dimension: "经济",
opportunity: "",
risk: "",
opportunityHint: "家庭场景ARPU值年增xx,商用场景留存率提升xx",
riskHint: "关税致北美市场出货量降18%",
},
{
dimension: "技术",
opportunity: "",
risk: "",
opportunityHint: "端侧AI降低隐私风险,模块化设计使维修成本降xx",
riskHint: "通用大模型直接调用致内容失控",
},
{
dimension: "社会",
opportunity: "",
risk: "",
opportunityHint: "银发经济(太极课程)、亲子经济(体感游戏)打开增量市场",
riskHint: "高价订阅制引发抵触",
},
]
}
function createForm() {
return {
pest: {
political: "",
economic: "",
social: "",
technology: "",
},
strategy: "",
comparisons: createComparisons(),
conclusion: "",
}
}
const form = ref(createForm())
const taskTitle = computed(() => taskConfig.value?.taskName || "行业需求分析")
const taskBackground = computed(() => taskConfig.value?.background || defaultBackground)
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals))
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement)
const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, []))
const activeStepName = computed(() => trainingSteps.value[currentStep.value - 1] || trainingSteps.value[0] || "")
watch(trainingSteps, (steps) => {
if (steps.length && currentStep.value > steps.length) {
currentStep.value = steps.length
}
})
const progressItems = computed(() => [
{ text: "进行中:PEST四维度归类", status: "doing" },
{ text: "进行中:机会与风险对比", status: "doing" },
{ text: form.value.conclusion ? "已填写:分析结论" : "待完成:分析结论", status: form.value.conclusion ? "done" : "" },
])
onMounted(async () => {
await loadTaskConfig()
await loadSavedAnswer()
})
async function loadTaskConfig() {
try {
const res = await getTrainingTaskByKey(TASK_KEY)
taskConfig.value = res?.data || null
} catch (error) {
taskConfig.value = null
}
}
async function loadSavedAnswer() {
try {
const res = await getStudentTrainingAnswer(TASK_KEY)
const answer = res?.data
if (answer?.currentStep && Number(answer.currentStep) > 0) {
currentStep.value = Number(answer.currentStep)
}
if (answer?.step1Answer) {
applySavedAnswer(answer.step1Answer)
}
} catch (error) {
// Keep the page usable even when the answer endpoint is unavailable.
}
}
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 : fallback
} catch (error) {
const list = String(value)
.split(/[,,|]/)
.map((item) => item.trim())
.filter(Boolean)
return list.length ? list : fallback
}
}
function applySavedAnswer(value) {
try {
const parsed = JSON.parse(value)
if (!parsed || typeof parsed !== "object") return
const nextForm = createForm()
nextForm.pest = {
...nextForm.pest,
...(parsed.pest || {}),
}
nextForm.strategy = parsed.strategy || ""
nextForm.conclusion = parsed.conclusion || ""
if (Array.isArray(parsed.comparisons)) {
const savedByDimension = parsed.comparisons.reduce((map, row) => {
if (row?.dimension) {
map[row.dimension] = row
}
return map
}, {})
nextForm.comparisons = createComparisons().map((row) => ({
...row,
opportunity: savedByDimension[row.dimension]?.opportunity || "",
risk: savedByDimension[row.dimension]?.risk || "",
}))
}
form.value = nextForm
} catch (error) {
// Ignore malformed historical answers.
}
}
function buildOutcome() {
return {
title: taskTitle.value,
pest: form.value.pest,
strategy: form.value.strategy,
comparisons: form.value.comparisons.map((row) => ({
dimension: row.dimension,
opportunity: row.opportunity,
risk: row.risk,
})),
conclusion: form.value.conclusion,
}
}
function buildAnswerPayload(saveAction = "SAVE") {
return {
step1Answer: JSON.stringify(buildOutcome()),
currentStep: currentStep.value,
submitted: saveAction === "SUBMIT",
saveAction,
}
}
async function saveAnswer(saveAction = "SAVE", silent = false) {
if (saving.value) return
saving.value = true
try {
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(saveAction))
if (!silent) {
proxy?.$modal?.msgSuccess(saveAction === "SUBMIT" ? "提交成功" : "已保存")
}
} catch (error) {
if (!silent) {
proxy?.$modal?.msgError?.(saveAction === "SUBMIT" ? "提交失败" : "保存失败")
}
} finally {
saving.value = false
}
}
function saveCurrentProgress() {
return saveAnswer("SAVE")
}
function changeStep(targetStep) {
const maxStep = trainingSteps.value.length
if (!maxStep) return
const next = Math.min(Math.max(Number(targetStep) || 1, 1), maxStep)
if (next === currentStep.value) return
currentStep.value = next
void saveAnswer("SAVE", true)
}
function previousStep() {
changeStep(currentStep.value - 1)
}
function nextStep() {
changeStep(currentStep.value + 1)
}
function submitTask() {
return saveAnswer("SUBMIT")
}
async function resetTraining() {
form.value = createForm()
await saveAnswer("RESET")
}
function exportOutcome() {
const blob = new Blob([JSON.stringify(buildOutcome(), null, 2)], { type: "application/json;charset=utf-8" })
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = url
link.download = `${taskTitle.value}-实训成果.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
proxy?.$modal?.msgSuccess("导出成功")
}
</script>
<style lang="scss" scoped>
.industry-demand-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;
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;
}
.training-core {
display: grid;
gap: 10px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
/* ===== 任务头:独立白卡 + 左侧教育蓝竖条 ===== */
.task-header {
position: relative;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 10px;
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-title {
margin: 0;
color: var(--student-text);
font-size: var(--student-fs-display);
font-weight: 700;
line-height: 1.35;
}
.goal-bg,
.task-card,
.step-card {
margin: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
display: grid;
gap: 10px;
}
.goal-bg {
p,
li {
color: #374151;
font-size: var(--student-fs-body);
line-height: 1.8;
}
p,
ol {
margin: 0;
}
ol {
padding-left: 18px;
}
}
.subtitle-icon {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
color: var(--student-text);
font-weight: 800;
}
.subtitle-icon--spaced {
margin-top: 12px;
}
.workbench-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 18px;
margin-bottom: 22px;
padding-bottom: 18px;
border-bottom: 1px solid var(--student-panel-border);
h3 {
margin: 4px 0 8px;
color: var(--student-text);
font-size: var(--student-fs-h1);
font-weight: 800;
line-height: 1.3;
}
}
.workbench-actions {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
margin-left: auto;
justify-content: flex-end;
gap: 10px;
}
.section-eyebrow {
margin: 0;
color: #2563eb;
font-size: var(--student-fs-caption);
font-weight: 800;
line-height: 1.4;
}
.workbench-description {
margin: 0;
color: #374151;
font-size: var(--student-fs-body);
font-weight: 400;
line-height: 1.8;
}
.outline-action {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
height: 36px;
padding: 0 14px;
border: 1px solid #bfdbfe;
border-radius: 8px;
color: var(--student-text);
background: #eff6ff;
font-size: var(--student-fs-button);
font-weight: 800;
&:hover:not(.is-disabled) {
transform: translateY(-1px);
border-color: #2563eb;
background: #dbeafe;
}
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
border: 1px solid var(--student-panel-border);
border-radius: 18px;
background:
linear-gradient(90deg, #eef5fc, #eef5fc),
#f0f6fd;
box-shadow: inset 0 1px 0 rgba(37, 99, 235, 0.08);
overflow-x: auto;
scrollbar-width: thin;
}
.step-tab {
display: flex;
flex: 1 0 145px;
align-items: center;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid var(--student-panel-border);
border-radius: 8px;
color: #6b7280;
background: #f0f6fd;
box-shadow: inset 0 1px 0 rgba(37, 99, 235, 0.04);
font-family: inherit;
font-size: var(--student-fs-body);
font-weight: 800;
cursor: pointer;
text-align: left;
transition: transform 0.2s ease, color 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
&:hover {
border-color: #2563eb;
color: var(--student-text);
background: #eff6ff;
box-shadow: 0 7px 18px rgba(30, 60, 100, 0.10), inset 0 1px 0 rgba(37, 99, 235, 0.16);
transform: translateY(-2px);
.step-action {
color: #1d4ed8;
}
}
&:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 4px;
}
&.active {
border-color: #2563eb;
color: var(--student-text);
background: linear-gradient(115deg, rgba(37, 99, 235, 0.14), rgba(37, 99, 235, 0.08));
box-shadow: 0 0 18px rgba(37, 99, 235, 0.22), inset 0 1px 0 rgba(37, 99, 235, 0.2);
.step-marker {
border-color: #2563eb;
color: #ffffff;
background: linear-gradient(135deg, #2563eb, #1d4ed8);
box-shadow: 0 0 0 5px rgba(37, 99, 235, 0.14), 0 0 18px rgba(37, 99, 235, 0.4);
}
}
&.complete {
color: #374151;
.step-marker {
border-color: #2563eb;
color: #ffffff;
background: #2563eb;
box-shadow: 0 0 12px #2563eb;
}
}
}
.step-marker {
display: inline-flex;
flex: 0 0 38px;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border: 1px solid #bfdbfe;
border-radius: 50%;
color: #6b7280;
background: #f0f6fd;
box-shadow: inset 0 0 12px rgba(37, 99, 235, 0.08);
font-size: var(--student-fs-caption);
font-weight: 900;
letter-spacing: 0.04em;
transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.step-copy {
display: grid;
gap: 4px;
min-width: 0;
margin-left: 10px;
}
.step-name {
display: -webkit-box;
overflow: hidden;
color: inherit;
font-size: var(--student-fs-body);
font-weight: 800;
line-height: 1.25;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.step-action {
color: #1d4ed8;
font-size: var(--student-fs-caption);
font-weight: 800;
letter-spacing: 0.06em;
line-height: 1;
transition: color 0.2s ease;
}
.step-connector {
position: relative;
flex: 0 1 40px;
min-width: 18px;
height: 2px;
margin: 0 8px;
overflow: visible;
background: var(--student-panel-border);
}
.step-connector::after {
position: absolute;
top: 50%;
right: -1px;
width: 6px;
height: 6px;
border-top: 1px solid #2563eb;
border-right: 1px solid #2563eb;
content: "";
transform: translateY(-50%) rotate(45deg);
}
.step-tab.complete+.step-connector {
background: #2563eb;
box-shadow: 0 0 9px rgba(37, 99, 235, 0.3);
&::after {
border-color: #2563eb;
}
}
.step-card {
margin-bottom: 0;
}
.step-title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 18px;
margin-bottom: 14px;
h3 {
margin: 4px 0 0;
color: var(--student-text);
font-size: var(--student-fs-h2);
font-weight: 900;
}
}
.background-block {
padding: 16px;
border: 1px solid var(--student-panel-border);
border-radius: 22px;
background: #f8fbfe;
h4 {
margin: 0 0 12px;
color: #374151;
font-size: var(--student-fs-body);
font-weight: 800;
}
p {
margin: 0 0 8px;
color: #374151;
font-size: var(--student-fs-body);
line-height: 1.72;
}
p:last-child {
margin-bottom: 0;
}
}
.task-desc {
margin: 22px 0 18px;
color: #374151;
font-size: var(--student-fs-body);
line-height: 1.8;
}
.pest-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin: 20px 0;
}
.pest-card {
min-height: 196px;
padding: 16px;
border: 1px solid var(--student-panel-border);
border-radius: 20px;
background: #f8fbfe;
}
.pest-card__title {
min-height: 22px;
margin-bottom: 12px;
color: var(--student-text);
font-size: var(--student-fs-body);
font-weight: 800;
}
.pest-input,
.long-textarea,
.comparison-row textarea {
width: 100%;
border: 1px solid #c9d8e8;
outline: none;
resize: vertical;
color: #374151;
background: #ffffff;
font-size: var(--student-fs-body);
line-height: 1.65;
transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
&::placeholder {
color: #6b7280;
opacity: 0.9;
}
&:focus {
border-color: #2563eb;
background: #ffffff;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.13);
}
}
.pest-input {
min-height: 128px;
padding: 12px 14px;
border-radius: 14px;
}
.field-block {
display: block;
margin-top: 22px;
span {
display: block;
margin-bottom: 10px;
color: #374151;
font-size: var(--student-fs-body);
line-height: 1.8;
}
}
.long-textarea {
min-height: 150px;
padding: 14px 16px;
border-radius: 14px;
}
.comparison-block {
margin-top: 26px;
}
.comparison-title {
margin: 0 0 12px;
color: #374151;
font-size: var(--student-fs-body);
font-weight: 800;
}
.comparison-table {
overflow: hidden;
border: 1px solid var(--student-panel-border);
border-radius: 22px;
background: #f8fbfe;
}
.comparison-row {
display: grid;
grid-template-columns: 140px minmax(0, 1fr) minmax(0, 1fr);
border-top: 1px solid var(--student-panel-border);
>div,
>textarea {
min-height: 76px;
border: 0;
border-right: 1px solid var(--student-panel-border);
border-radius: 0;
}
>textarea {
padding: 16px;
text-align: center;
}
> :last-child {
border-right: 0;
}
}
.comparison-head {
border-top: 0;
color: var(--student-text);
background: #dbeafe;
font-weight: 900;
div {
display: flex;
align-items: center;
justify-content: center;
min-height: 48px;
}
}
.dimension-cell {
display: flex;
align-items: center;
justify-content: center;
color: var(--student-text);
font-weight: 800;
}
.training-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 52px;
margin-top: 36px;
}
.training-actions :deep(.training-action) {
min-width: 172px;
height: 36px;
min-height: 36px;
padding: 0 14px;
border: 1px solid #2563eb;
border-radius: 8px;
color: #374151;
background: #eff6ff;
box-shadow: none;
font-size: var(--student-fs-h1);
font-weight: 900;
line-height: 1;
letter-spacing: 0;
text-shadow: none;
}
.training-actions :deep(.training-action span) {
display: inline-flex;
align-items: center;
color: inherit;
font-size: inherit;
line-height: 1;
}
.training-actions :deep(.training-action--submit),
.training-actions :deep(.training-action--reset),
.training-actions :deep(.training-action--export) {
border-color: #2563eb;
color: #374151;
background: #eff6ff;
}
.training-actions :deep(.training-action:hover:not(.is-disabled)) {
border-color: #1d4ed8;
color: var(--student-text);
background: #dbeafe;
}
.step-actions {
display: flex;
justify-content: center;
gap: 14px;
margin-top: 22px;
}
.step-actions :deep(.step-action-btn) {
min-height: 36px;
height: 36px;
padding: 0 14px;
border-radius: 8px;
font-size: var(--student-fs-button);
font-weight: 800;
}
.step-actions :deep(.step-action-btn--previous) {
border-color: #bfdbfe;
color: var(--student-text);
background: #eff6ff;
}
.step-actions :deep(.step-action-btn--save),
.step-actions :deep(.step-action-btn--next) {
border-color: #2563eb;
color: #ffffff;
background: linear-gradient(95deg, #2563eb, #1d4ed8);
}
.step-actions :deep(.step-action-btn:hover:not(.is-disabled)) {
transform: translateY(-1px);
}
.step-actions :deep(.step-action-btn.is-disabled) {
opacity: 0.45;
cursor: not-allowed;
}
@media (max-width: 1100px) {
.industry-demand-page {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.pest-grid {
grid-template-columns: 1fr;
margin: 20px 0;
}
.industry-demand-page {
padding: 14px 14px 18px;
}
.training-core {
padding: 18px;
border-radius: 24px;
}
.workbench-head,
.workbench-actions,
.step-title-row {
flex-direction: column;
align-items: stretch;
}
.workbench-actions :deep(.material-download),
.workbench-actions .outline-action {
width: 100%;
}
.step-indicator {
width: 100%;
margin: 0 0 32px;
padding: 16px;
overflow: visible;
flex-direction: column;
align-items: stretch;
border-radius: 20px;
}
.step-tab {
flex: 0 0 auto;
width: 100%;
min-width: 0;
}
.step-connector {
flex: 0 0 20px;
align-self: flex-start;
width: 2px;
min-width: 0;
height: 20px;
margin: 4px 0 4px 18px;
}
.step-connector::after {
top: auto;
right: auto;
bottom: -1px;
left: 50%;
transform: translateX(-50%) rotate(135deg);
}
.step-actions,
.training-actions {
flex-direction: column;
align-items: stretch;
gap: 14px;
}
.step-actions :deep(.step-action-btn),
.training-actions :deep(.training-action) {
width: 100%;
}
.comparison-row {
grid-template-columns: 92px minmax(0, 1fr) minmax(0, 1fr);
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>
<style lang="scss">
/* ============================================================
* 画布背景
* ============================================================ */
#app .studentLayout .app-main:has(.industry-demand-page) {
background: #ffffff !important;
}
/* ============================================================
* 步骤导航:分段连接胶囊
* ============================================================ */
#app .studentLayout section.app-main:has(.industry-demand-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 {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: center !important;
gap: 8px !important;
min-width: 0 !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;
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 {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
min-width: 22px !important;
height: 22px !important;
padding: 0 5px !important;
border-radius: 6px !important;
border: none !important;
background: #f3f4f6 !important;
color: #6b7280 !important;
box-shadow: none !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;
margin-left: 0 !important;
}
.step-name {
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !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-demand-page) .step-indicator {
justify-content: flex-start !important;
overflow-x: auto !important;
}
#app .studentLayout section.app-main:has(.industry-demand-page) .step-indicator .step-tab {
flex: 0 0 140px !important;
max-width: none !important;
}
}
/* ============================================================
* 共用组件换肤:TrainingMaterialButton + outline-action → 教育蓝
* ============================================================ */
.industry-demand-page .workbench-actions .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;
}
}
}
#app .studentLayout section.app-main:has(.industry-demand-page) .workbench-actions>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(.industry-demand-page) .workbench-actions>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;
}
/* ============================================================
* step-actions + training-actions(本页用 el-button :deep 覆盖)
* ============================================================ */
#app .studentLayout section.app-main:has(.industry-demand-page) .step-actions .el-button,
#app .studentLayout section.app-main:has(.industry-demand-page) .training-actions .el-button {
height: 34px !important;
min-height: 34px !important;
border-radius: 8px !important;
font-weight: 700 !important;
font-size: var(--student-fs-body) !important;
border: 1px solid rgba(37, 99, 235, 0.35) !important;
background: #ffffff !important;
color: #2563eb !important;
padding: 0 14px !important;
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease, color 0.16s ease !important;
box-shadow: none !important;
}
#app .studentLayout section.app-main:has(.industry-demand-page) .step-actions .el-button:hover:not(.is-disabled),
#app .studentLayout section.app-main:has(.industry-demand-page) .training-actions .el-button:hover:not(.is-disabled) {
transform: translateY(-1px) !important;
border-color: #2563eb !important;
background: #eff6ff !important;
color: #2563eb !important;
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12) !important;
}
#app .studentLayout section.app-main:has(.industry-demand-page) .step-actions .step-action-btn--save.el-button,
#app .studentLayout section.app-main:has(.industry-demand-page) .step-actions .step-action-btn--next.el-button,
#app .studentLayout section.app-main:has(.industry-demand-page) .training-actions .training-action--submit.el-button {
border-color: #2563eb !important;
background: #2563eb !important;
color: #ffffff !important;
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28) !important;
}
#app .studentLayout section.app-main:has(.industry-demand-page) .step-actions .step-action-btn--save.el-button:hover:not(.is-disabled),
#app .studentLayout section.app-main:has(.industry-demand-page) .step-actions .step-action-btn--next.el-button:hover:not(.is-disabled),
#app .studentLayout section.app-main:has(.industry-demand-page) .training-actions .training-action--submit.el-button:hover:not(.is-disabled) {
border-color: #1d4ed8 !important;
background: #1d4ed8 !important;
color: #ffffff !important;
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32) !important;
}
#app .studentLayout section.app-main:has(.industry-demand-page) .training-actions .training-action--reset.el-button {
border-color: #e5e7eb !important;
background: #f3f4f6 !important;
color: #374151 !important;
box-shadow: none !important;
}
#app .studentLayout section.app-main:has(.industry-demand-page) .training-actions .training-action--reset.el-button:hover:not(.is-disabled) {
border-color: #2563eb !important;
background: #eff6ff !important;
color: #2563eb !important;
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12) !important;
}
#app .studentLayout section.app-main:has(.industry-demand-page) .step-actions .el-button.is-disabled,
#app .studentLayout section.app-main:has(.industry-demand-page) .training-actions .el-button.is-disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
transform: none !important;
}
</style>