feat: 统一实训页面布局与步骤导航

dev-QQq
chenyuan 1 week ago
parent c18c9bf6b8
commit f07d09635f

@ -180,13 +180,24 @@
### 5.2 桌面端尺寸与布局
- 步骤栏采用**流程式导航**:步骤卡、圆形编号节点、步骤间连线与箭头共同构成流程;不得退回为只有文字的胶囊标签。
#### 截图视觉基准(标准步骤条)
标准步骤条必须呈现为一条完整的深色胶囊流程轨道。每一步从左到右依次为两位编号圆点、步骤名称和状态提示,步骤之间保留青蓝色连线与右向箭头。默认和已完成步骤直接置于整条轨道上,**不得**再绘制独立的深色矩形卡片、边框或背景块;只有当前步骤使用完整的蓝色渐变胶囊、带光晕的蓝色编号圆点和“当前步骤”提示。已完成步骤仅将编号圆点和前置连线改为青绿色,编号不得被勾选图标替代。
```text
(01) 步骤名称 / 点击切换 → → [ (02) 步骤名称 / 当前步骤 ] → (03) 步骤名称 / 点击切换 →
```
实现必须保留 `.step-marker`、`.step-copy`(含 `.step-name``.step-action`)以及 `.step-connector` 四类元素;不得通过隐藏编号、状态文案或连接箭头把流程步骤条简化为纯文字胶囊导航。
- 为避免与工作台内容区过于局促,桌面端步骤栏可相对内容区向左右各扩展 `12px``width: calc(100% + 24px)`、`margin-left/right: -12px`。扩展范围只能位于主工作台内,不得突破主工作台边框。
- 容器内边距 `18px 22px`,圆角 `18px`,边框 `1px solid rgba(0,180,255,.3)`;使用深青蓝渐变叠加深色背景与轻微内高光。
- 每个步骤项使用 `flex: 1 0 145px` 分配宽度,最小高度 `66px`,内边距 `8px 10px`,圆角 `8px`,默认带低对比可点击边框和深色卡片背景。`145px` 是统一的最小可用宽度,不能因步骤名称不同而改为按名称定宽。
- 步骤项左侧为 `38px` 圆形编号节点;节点始终显示两位步骤编号(如 `01`**已完成步骤不得替换为勾选符号**。已完成态只能通过节点和连线颜色表达。
- 容器内边距 `10px 18px`,圆角 `999px`,边框 `1px solid rgba(0,180,255,.3)`;使用深青蓝背景与轻微内高光,作为所有步骤共用的一条轨道
- 每个步骤项使用 `flex: 1 1 0` 分配宽度,最小宽度 `150px`、最小高度 `72px`、内边距 `8px 12px`、圆角 `999px`。默认背景和描边均透明;仅当前步骤使用蓝色渐变背景与青色描边。步骤名称不同不得改变统一的最小可用宽度
- 步骤项左侧为 `56px` 圆形编号节点;节点始终显示两位步骤编号(如 `01`**已完成步骤不得替换为勾选符号**。已完成态只能通过节点和连线颜色表达。
- 步骤项依次显示“步骤名称”和操作提示;不得显示 `STAGE` 或其他英文阶段眉题。未激活项常驻显示“点击切换 →”,当前项显示“当前步骤”。
- 步骤项之间使用低饱和青蓝 `2px` 连线与右箭头表达先后关系;已完成区段可提高亮度,连线不承担点击区域。
- **桌面端步骤间距(硬性值):** 单段连接线使用 `flex: 0 1 40px`、`min-width: 18px`、`height: 2px`、`margin: 0 8px`;箭头使用 `6px × 6px` 的右向描边。不得沿用历史的 `64px` 连接线或 `0 14px` 两侧外边距,避免步骤卡之间出现过宽空白。
- **桌面端步骤间距(硬性值):** 单段连接线使用 `flex: 0 1 72px`、`min-width: 40px`、`height: 2px`、`margin: 0 12px`;箭头使用 `8px × 8px` 的右向描边。连线位于整条轨道内,用于区分透明的默认步骤与当前步骤
- 步骤名称由配置提供,最多显示两行,超出后使用省略号截断;必须保留完整名称的悬停提示(如 `title`)和可访问名称。不得因长名称压缩编号节点、操作提示或相邻步骤卡。
- 当全部步骤的最小宽度无法容纳时,步骤栏允许横向滚动;禁止把步骤卡、文字或连线压缩变形。
@ -194,11 +205,11 @@
| 状态 | 文字 | 背景 | 描边与效果 |
| --- | --- | --- | --- |
| 默认/待完成 | `#9ab3d0` | `rgba(3,27,43,.52)` | `rgba(68,141,177,.35)`;显示“点击切换 →” |
| Hover可点击 | `#ffffff` | `rgba(8,92,131,.52)` | `rgba(89,223,255,.82)`;卡片上移 `2px`、强化阴影与“点击切换”颜色 |
| 当前/激活 | `#ffffff` | 深青蓝渐变 | `rgba(89,226,255,.85)`;节点使用蓝色渐变与青色光晕,提示为“当前步骤” |
| 已完成(如业务提供) | `#dffbff` | 保持步骤卡底色 | 编号节点和已完成连线使用青绿色;编号仍可见,不显示勾选 |
| 禁用(如业务提供) | `#8299aa` | 深色卡片底色 | `opacity: .45`;无指针手势,且不显示“点击切换” |
| 默认/待完成 | `#9ab3d0` | 透明 | 无独立描边;显示“点击切换 →” |
| Hover可点击 | `#ffffff` | 透明或轻微青蓝半透明 | 仅以胶囊圆角高亮反馈;上移 `2px`、强化“点击切换”颜色 |
| 当前/激活 | `#ffffff` | `#0eaae4 → #0572ac` 蓝色渐变胶囊 | `#36d9ff` 描边;节点使用蓝色渐变与青色光晕,提示为“当前步骤” |
| 已完成(如业务提供) | `#dffbff` | 透明 | 编号节点和已完成连线使用青绿色;编号仍可见,不显示勾选 |
| 禁用(如业务提供) | `#8299aa` | 透明 | `opacity: .45`;无指针手势,且不显示“点击切换” |
- 过渡仅用于 `transform`、颜色、背景、描边和阴影,时长 `0.2s`;不得做闪烁、跳动或自动轮播动画。
- 激活状态只表现当前业务状态,不根据名称、索引或固定第一个项目推断。

@ -1,8 +1,8 @@
.student-training-shell {
--student-step-height: 46px;
--student-step-font: 14px;
--student-action-height: 52px;
--student-action-font: 20px;
--student-action-height: 42px;
--student-action-font: 16px;
--student-upload-height: 40px;
--student-upload-font: 14px;
--student-aux-height: 42px;
@ -290,11 +290,11 @@
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
min-width: 144px !important;
min-width: 112px !important;
width: auto !important;
height: var(--student-action-height) !important;
min-height: var(--student-action-height) !important;
padding: 0 20px !important;
padding: 0 16px !important;
border: 1px solid #2c83aa !important;
border-radius: var(--student-radius-sm) !important;
color: #dff7ff !important;
@ -319,6 +319,82 @@
background: rgba(18, 85, 116, 0.98) !important;
}
/* 新产品调查等页面使用 task-actions而非 training-action。统一底部提交/重置按钮规格。 */
.student-training-shell .task-actions {
display: flex !important;
flex-wrap: wrap !important;
justify-content: center !important;
align-items: center !important;
gap: 28px !important;
margin-top: 28px !important;
}
.student-training-shell .task-actions > button {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
min-width: 112px !important;
width: auto !important;
height: var(--student-action-height) !important;
min-height: var(--student-action-height) !important;
padding: 0 16px !important;
font-size: var(--student-action-font) !important;
font-weight: 800 !important;
line-height: 1 !important;
}
.student-training-shell .taskfooter {
display: flex !important;
flex-wrap: wrap !important;
justify-content: center !important;
align-items: center !important;
gap: 28px !important;
margin-top: 28px !important;
}
.student-training-shell .taskfooter .el-button {
min-width: 112px !important;
width: auto !important;
height: var(--student-action-height) !important;
min-height: var(--student-action-height) !important;
padding: 0 16px !important;
font-size: var(--student-action-font) !important;
font-weight: 800 !important;
line-height: 1 !important;
}
/* 底部提交/重置操作统一采用实训页的胶囊按钮样式。 */
.student-training-shell .training-actions,
.student-training-shell .task-actions,
.student-training-shell .taskfooter {
gap: 28px !important;
margin-top: 28px !important;
}
.student-training-shell .training-actions .el-button.training-action,
.student-training-shell .training-actions button.training-action,
.student-training-shell .task-actions > button,
.student-training-shell .taskfooter .el-button {
border: 1px solid #2cc9ff !important;
border-radius: var(--student-radius-pill) !important;
color: #dff8ff !important;
background: rgba(8, 87, 121, 0.55) !important;
font-weight: 700 !important;
}
.student-training-shell .training-actions .training-action--submit,
.student-training-shell .task-actions > .btn-submit,
.student-training-shell .task-actions > .btn-primary,
.student-training-shell .taskfooter .el-button:first-child {
border-color: #36d9ff !important;
color: #ffffff !important;
background: linear-gradient(110deg, #0eaae4, #0572ac) !important;
}
.student-training-shell .task-actions > button .el-icon {
display: inline-flex !important;
}
.student-training-shell .el-button.ghost-action,
.student-training-shell .el-button.outline-action,
.student-training-shell .workbench-actions .el-button.material-download,

@ -42,12 +42,6 @@
</template>
</div>
</template>
<button v-if="isStudentRole" type="button" :class="['student-report-button', { active: route.path === '/report/index' }]" @click="goStudentNav('/report/index')">
<span class="student-course-icon">
<component :is="ElementPlusIconsVue.DocumentChecked" />
</span>
<span>生成实训报告</span>
</button>
</nav>
</el-scrollbar>
</template>

@ -1,26 +1,34 @@
<template>
<main class="supplier-page" v-loading="loading">
<div class="supplier-page student-training-shell" v-loading="loading">
<main class="training-core">
<section class="workspace-shell">
<header class="workspace-header">
<div>
<p class="eyebrow">SUPPLIER CHANNEL EVALUATION</p>
<h1>供应渠道评估</h1>
<h1>{{ taskTitle }}</h1>
<p>分别从试产与量产场景评估供应渠道形成可执行的采购与风险管理建议</p>
</div>
<span class="task-badge">实训任务</span>
</header>
<TrainingTaskBrief
:background="taskBackground"
:goals="taskGoals"
:requirements="taskRequirement"
:show-material="false"
/>
<nav class="step-nav" aria-label="">
<button :class="['step-tab', { active: currentStep === 1 }]" @click="currentStep = 1">
<span>01</span><b>设置评估权重</b>
<button :class="['step-tab', { active: currentStep === 1, complete: currentStep > 1 }]" :aria-current="currentStep === 1 ? 'step' : undefined" @click="currentStep = 1">
<span class="step-marker">01</span><span class="step-copy"><b>设置评估权重</b><small>{{ currentStep === 1 ? '当前步骤 →' : '点击切换 →' }}</small></span>
</button>
<i></i>
<button :class="['step-tab', { active: currentStep === 2 }]" @click="openStepTwo">
<span>02</span><b>渠道评分与对比</b>
<button :class="['step-tab', { active: currentStep === 2, complete: currentStep > 2 }]" :aria-current="currentStep === 2 ? 'step' : undefined" @click="openStepTwo">
<span class="step-marker">02</span><span class="step-copy"><b>渠道评分与对比</b><small>{{ currentStep === 2 ? '当前步骤 →' : '点击切换 →' }}</small></span>
</button>
<i></i>
<button :class="['step-tab', { active: currentStep === 3 }]" @click="openStepThree">
<span>03</span><b>采购建议与风险</b>
<button :class="['step-tab', { active: currentStep === 3 }]" :aria-current="currentStep === 3 ? 'step' : undefined" @click="openStepThree">
<span class="step-marker">03</span><span class="step-copy"><b>采购建议与风险</b><small>{{ currentStep === 3 ? '当前步骤 →' : '点击切换 →' }}</small></span>
</button>
</nav>
@ -167,13 +175,28 @@
</footer>
</template>
</section>
</main>
</main>
<TrainingAiSidebar
:task-key="TASK_KEY"
:has-saved-answer="hasSavedAnswer"
:progress-items="progressItems"
:study-tip="currentStudyTip"
reference-title="供应渠道评估提示"
reference-text="· 试产阶段重点关注交付周期与采购灵活性<br />· 量产阶段重点关注采购成本与质量可靠性<br />· 风险建议应包含明确的应对措施"
assist-text="建议先区分试产与量产的评估侧重点,再基于评分结果形成采购建议和供应风险应对方案。"
/>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from "vue";
import { CircleCheck, Delete, Plus } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import {
getStudentTrainingAnswer,
saveStudentTrainingAnswer,
@ -187,6 +210,25 @@ const loading = ref(false);
const currentStep = ref(1);
const stepOneSaved = ref(false);
const stepTwoSaved = ref(false);
const taskConfig = ref({});
const taskTitle = computed(() => taskConfig.value?.taskName || "供应渠道评估");
const taskBackground = computed(() => taskConfig.value?.background || "供应渠道会直接影响产品的采购成本、交付稳定性与质量风险,需要分别针对试产和量产场景建立评估标准。 ");
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives ?? taskConfig.value?.goals, ["根据试产与量产场景设置供应渠道评估权重", "完成渠道评分、采购建议与供应风险分析"]));
const taskRequirement = computed(() => taskConfig.value?.requirements || taskConfig.value?.requirement || "完成各步骤校验并保存,渠道评分、采购建议和供应风险应与案例资料及评估结果保持一致。");
const hasSavedAnswer = computed(() => stepOneSaved.value || stepTwoSaved.value || Boolean(procurementRecommendation.value.trim()));
const currentStudyTip = computed(() => {
const tips = {
1: "先区分试产与量产的采购重点,确保每个场景的权重合计为 100%。",
2: "逐项填写渠道评分,重点检查渠道名称和加权评分是否与已保存权重一致。",
3: "根据排序结果形成采购建议,并为每类供应风险制定可执行的应对措施。",
};
return tips[currentStep.value] || tips[1];
});
const progressItems = computed(() => [
{ text: "设置评估权重", status: stepOneSaved.value ? "done" : currentStep.value === 1 ? "doing" : "" },
{ text: "渠道评分与对比", status: stepTwoSaved.value ? "done" : currentStep.value === 2 ? "doing" : "" },
{ text: "采购建议与风险", status: currentStep.value === 3 ? "doing" : "" },
]);
const rows = ref([
{ key: "procurementCost", name: "采购成本", description: "元器件/模组的单台采购总成本,含运费、税费", smallBatchWeight: 20, largeBatchWeight: 35 },
@ -458,6 +500,11 @@ function removeRiskRow() {
onMounted(async () => {
loading.value = true;
try {
taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || {};
} catch (_) {
taskConfig.value = {};
}
try {
const answer = await getStudentTrainingAnswer(TASK_KEY);
stepOneSaved.value = applyStepOne(answer);
@ -474,20 +521,22 @@ onMounted(async () => {
</script>
<style scoped>
.supplier-page { min-height: 100%; padding: 24px; color: #dce9f2; background: radial-gradient(circle at top right, #123e58 0, #071723 38%, #050e16 100%); }
.workspace-shell { max-width: 1520px; margin: 24px auto 0; padding: 28px; border: 1px solid rgba(69, 180, 227, .35); border-radius: 18px; background: rgba(7, 27, 41, .86); box-shadow: 0 20px 60px rgba(0, 0, 0, .24); }
.workspace-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 30px; }
.supplier-page { min-height: 100%; padding: 24px; color: #dce9f2; background: #06111d; }
.workspace-shell { box-sizing: border-box; width: min(100%, 1520px); margin: 0 auto; padding: 28px; border: 1px solid rgba(0, 180, 255, .25); border-radius: 36px; background: rgba(8, 18, 28, .6); box-shadow: inset 0 1px 0 rgba(169, 229, 255, .08), 0 22px 48px rgba(0, 0, 0, .22); }
.supplier-page .training-core { box-sizing: border-box; min-width: 0; width: 100%; margin: 0; padding: 28px; border: 1px solid rgba(0, 180, 255, .25); border-radius: 36px; background: rgba(8, 18, 28, .6); box-shadow: inset 0 1px 0 rgba(169, 229, 255, .08), 0 22px 48px rgba(0, 0, 0, .22); }
.supplier-page .workspace-shell { width: 100%; max-width: none; margin: 0; padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; }
.workspace-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 24px; }
.eyebrow, .step-caption { margin: 0 0 7px; color: #63dcff; font-weight: 700; letter-spacing: .1em; }
.workspace-header h1, .intro-card h2 { margin: 0; color: #fff; }
.workspace-header h1, .intro-card h2 { margin: 0; color: #fff; }.workspace-header h1 { margin-top: 12px; font-size: 30px; font-weight: 900; line-height: 1.3; }
.workspace-header p:not(.eyebrow), .intro-card > p:last-child { margin: 10px 0 0; color: #9db4c4; line-height: 1.75; }
.task-badge { padding: 8px 14px; border: 1px solid #3dc4ec; border-radius: 99px; color: #5fdbff; white-space: nowrap; }
.step-nav { display: grid; grid-template-columns: 1fr 44px 1fr 44px 1fr; align-items: center; margin-bottom: 28px; }
.step-nav i { height: 2px; background: linear-gradient(90deg, #2b9cc4, #174963); }
.step-tab { display: flex; align-items: center; gap: 11px; padding: 13px 16px; border: 1px solid #21556e; border-radius: 12px; color: #afc8d7; background: #092231; cursor: pointer; text-align: left; }
.step-tab span { display: grid; width: 30px; height: 30px; place-items: center; border: 1px solid #397a96; border-radius: 50%; font-weight: 700; }
.step-tab.active { color: white; border-color: #36c5f5; background: linear-gradient(90deg, #0b84b3, #126181); box-shadow: inset 0 0 24px rgba(77, 213, 255, .22); }
.step-tab.active span { border-color: #9af0ff; background: #0b98ce; }
.intro-card, .analysis-panel { padding: 24px; border: 1px solid rgba(71, 167, 209, .35); border-radius: 14px; background: rgba(3, 20, 31, .72); }
.task-badge { display: inline-flex; align-items: center; min-height: 28px; padding: 4px 14px; border: 1px solid rgba(99, 217, 255, .55); border-radius: 999px; color: #dff5ff; background: rgba(17, 126, 178, .36); white-space: nowrap; }
.step-nav { display: grid; grid-template-columns: minmax(0, 1fr) 40px minmax(0, 1fr) 40px minmax(0, 1fr); align-items: center; gap: 8px; margin: 0 0 30px; padding: 18px 22px; border: 1px solid rgba(0, 180, 255, .3); border-radius: 22px; background: rgba(1, 18, 31, .62); }
.step-nav i { position: relative; height: 2px; background: rgba(86, 198, 235, .48); }.step-nav i::after { position: absolute; top: 50%; right: 0; width: 7px; height: 7px; border-top: 1px solid #73b1ce; border-right: 1px solid #73b1ce; content: ''; transform: translateY(-50%) rotate(45deg); }
.step-tab { display: flex; align-items: center; min-width: 0; min-height: 66px; gap: 10px; padding: 8px 12px; border: 1px solid rgba(68, 141, 177, .35); border-radius: 12px; color: #9ab3d0; background: rgba(3, 27, 43, .52); cursor: pointer; font: inherit; text-align: left; transition: .2s ease; }
.step-tab:hover { border-color: rgba(89, 223, 255, .82); color: #fff; background: rgba(8, 92, 131, .52); transform: translateY(-1px); }.step-tab:focus-visible { outline: 2px solid #72e8ff; outline-offset: 3px; }
.step-marker { display: grid; flex: 0 0 38px; width: 38px; height: 38px; place-items: center; border: 1px solid rgba(101, 176, 208, .6); border-radius: 50%; color: #a6c4d9; background: rgba(3, 25, 40, .8); font-size: 12px; font-weight: 900; }.step-copy { display: grid; min-width: 0; gap: 4px; }.step-copy b { overflow: hidden; font-size: 14px; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; }.step-copy small { color: #66b6d4; font-size: 10px; font-weight: 800; letter-spacing: .06em; }
.step-tab.active { color: white; border-color: #53d8fb; background: linear-gradient(135deg, rgba(7, 156, 207, .85), rgba(13, 105, 161, .92)); box-shadow: inset 0 0 24px rgba(77, 213, 255, .22); }.step-tab.active .step-marker { border-color: #72e8ff; color: #fff; background: linear-gradient(135deg, #08b7e8, #1375d0); box-shadow: 0 0 0 5px rgba(25, 179, 237, .14), 0 0 18px rgba(46, 208, 255, .58); }.step-tab.complete { color: #dffbff; }.step-tab.complete .step-marker { border-color: rgba(84, 230, 221, .85); color: #062b37; background: #6ef0e1; }
.intro-card, .analysis-panel { padding: 24px; border: 1px solid rgba(82, 174, 226, .42); border-radius: 26px; background: rgba(0, 25, 42, .64); }
.intro-card.compact { margin-bottom: 20px; }
.weights-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; margin-top: 20px; }
.weight-card, .score-section { padding: 20px; border: 1px solid rgba(71, 167, 209, .28); border-radius: 14px; background: rgba(4, 23, 36, .68); }

@ -13,31 +13,43 @@
:show-material="false"
/>
<nav class="step-navigation" aria-label="">
<button type="button" class="step-nav-item" :class="{ active: currentStep === 1 }" @click="currentStep = 1">
<span>01</span> BOM 物料选型
<section class="task-card hardware-workbench">
<div class="workbench-head hardware-workbench-head">
<div>
<p class="section-eyebrow">BOM cost workspace</p>
<h2>智能硬件成本估算</h2>
</div>
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
</div>
<nav class="step-indicator" aria-label="">
<button type="button" :class="['step-tab', { active: currentStep === 1, complete: currentStep > 1 }]" aria-label=" 1 BOM " :aria-current="currentStep === 1 ? 'step' : undefined" @click="currentStep = 1">
<span class="step-marker" aria-hidden="true">01</span>
<span class="step-copy"><strong class="step-name" title="BOM 物料选型">BOM 物料选型</strong><span class="step-action">{{ currentStep === 1 ? '当前步骤' : '点击切换' }} <span aria-hidden="true"></span></span></span>
</button>
<i class="step-connector"></i>
<button type="button" class="step-nav-item" :class="{ active: currentStep === 2 }" @click="openStepTwo">
<span>02</span> 批量成本核算
<span class="step-connector" aria-hidden="true" />
<button type="button" :class="['step-tab', { active: currentStep === 2, complete: currentStep > 2 }]" aria-label=" 2 " :aria-current="currentStep === 2 ? 'step' : undefined" @click="openStepTwo">
<span class="step-marker" aria-hidden="true">02</span>
<span class="step-copy"><strong class="step-name" title="批量成本核算">批量成本核算</strong><span class="step-action">{{ currentStep === 2 ? '当前步骤' : '点击切换' }} <span aria-hidden="true"></span></span></span>
</button>
<i class="step-connector"></i>
<button type="button" class="step-nav-item" :class="{ active: currentStep === 3 }" @click="openStepThree">
<span>03</span> 制造成本核算
<span class="step-connector" aria-hidden="true" />
<button type="button" :class="['step-tab', { active: currentStep === 3, complete: currentStep > 3 }]" aria-label=" 3 " :aria-current="currentStep === 3 ? 'step' : undefined" @click="openStepThree">
<span class="step-marker" aria-hidden="true">03</span>
<span class="step-copy"><strong class="step-name" title="制造成本核算">制造成本核算</strong><span class="step-action">{{ currentStep === 3 ? '当前步骤' : '点击切换' }} <span aria-hidden="true"></span></span></span>
</button>
<i class="step-connector"></i>
<button type="button" class="step-nav-item" :class="{ active: currentStep === 4 }" @click="openStepFour">
<span>04</span> 成本优化决策
<span class="step-connector" aria-hidden="true" />
<button type="button" :class="['step-tab', { active: currentStep === 4, complete: currentStep > 4 }]" aria-label=" 4 " :aria-current="currentStep === 4 ? 'step' : undefined" @click="openStepFour">
<span class="step-marker" aria-hidden="true">04</span>
<span class="step-copy"><strong class="step-name" title="成本优化决策">成本优化决策</strong><span class="step-action">{{ currentStep === 4 ? '当前步骤' : '点击切换' }} <span aria-hidden="true"></span></span></span>
</button>
</nav>
<section v-if="currentStep === 1" class="task-card">
<section v-if="currentStep === 1" class="step-card hardware-step-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Step 01 · BOM selection</p>
<h2>核心元器件选型组合</h2>
</div>
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
</div>
<section class="cost-brief">
@ -90,7 +102,7 @@
</section>
</section>
<section v-else-if="currentStep === 2" class="task-card">
<section v-else-if="currentStep === 2" class="step-card hardware-step-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Step 02 · Volume costing</p>
@ -153,7 +165,7 @@
</section>
</section>
<section v-else-if="currentStep === 3" class="task-card">
<section v-else-if="currentStep === 3" class="step-card hardware-step-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Step 03 · Manufacturing costing</p>
@ -212,7 +224,7 @@
</section>
</section>
<section v-else class="task-card">
<section v-else class="step-card hardware-step-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Step 04 · Cost optimisation</p>
@ -281,12 +293,24 @@
</div>
</section>
</section>
</section>
</main>
<TrainingAiSidebar
:task-key="TASK_KEY"
:has-saved-answer="hasSavedAnswer"
:progress-items="progressItems"
:study-tip="currentStudyTip"
reference-title="硬件成本核算提示"
reference-text="· BOM 金额 = 单价 × 数量<br />· 小批量和大批量成本需分别核算<br />· 优化方案应说明可执行的降本措施"
assist-text="建议先检查物料数量、单价和金额关系,再比较批量成本差异并形成可执行的成本优化结论。"
/>
</div>
</template>
<script setup>
import { computed, getCurrentInstance, onMounted, reactive, ref } from "vue";
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";
@ -328,8 +352,24 @@ const stepFour = reactive({
const taskTitle = computed(() => taskConfig.value?.taskName || "智能硬件成本估算");
const taskBackground = computed(() => taskConfig.value?.background || "围绕智能硬件产品的核心元器件进行 BOM 选型与成本核算。");
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.goals, ["理解元器件性能、成本与产品定位之间的平衡", "完成可追溯的 BOM 选型明细"]));
const taskRequirement = computed(() => taskConfig.value?.requirement || "基于案例资料中的元器件选型库完成 BOM 物料明细,并通过系统校验后保存。");
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives ?? taskConfig.value?.goals, ["理解元器件性能、成本与产品定位之间的平衡", "完成可追溯的 BOM 选型明细"]));
const taskRequirement = computed(() => taskConfig.value?.requirements || taskConfig.value?.requirement || "基于案例资料中的元器件选型库完成 BOM 物料明细,并通过系统校验后保存。");
const hasSavedAnswer = computed(() => savedStepOneItems.value.length > 0 || stepTwoSaved.value || stepThreeSaved.value || Boolean(stepFour.optimizationPlan || stepFour.smallBatchChart.url || stepFour.largeBatchChart.url));
const currentStudyTip = computed(() => {
const tips = {
1: "先完成 BOM 物料选型,确保物料名称、规格、单价和数量之间的关系准确。",
2: "分别核算小批量和大批量的物料成本,重点比较批量差异对单件成本的影响。",
3: "结合制造环节成本与 BOM 成本,核算不同批量下的完整单件成本。",
4: "根据成本核算结果,给出有依据、可执行的硬件成本优化方案。",
};
return tips[currentStep.value] || tips[1];
});
const progressItems = computed(() => [
{ text: "BOM 物料选型", status: savedStepOneItems.value.length ? "done" : currentStep.value === 1 ? "doing" : "" },
{ text: "物料 BOM 成本核算", status: stepTwoSaved.value ? "done" : currentStep.value === 2 ? "doing" : "" },
{ text: "制造成本核算", status: stepThreeSaved.value ? "done" : currentStep.value === 3 ? "doing" : "" },
{ text: "成本优化决策", status: currentStep.value === 4 ? "doing" : "" },
]);
const canAdd = computed(() => rows.value.length < MAX_ROWS);
const canDelete = computed(() => rows.value.length > 1);
const totalAmount = computed(() => rows.value.reduce((total, row) => total + rowAmount(row), 0));
@ -759,4 +799,185 @@ onMounted(async () => {
.manufacturing-table{min-width:820px}.manufacturing-table th:nth-child(1){width:8%}.manufacturing-table th:nth-child(2){width:32%}.manufacturing-table th:nth-child(3),.manufacturing-table th:nth-child(4){width:30%}.reference-amount{text-align:center;color:#a6d8ee;font-size:16px;font-weight:800;background:rgba(8,71,102,.3)!important}
.step-four-summary{margin:0 0 18px}.chart-upload-section,.optimization-section{margin-top:18px}.chart-upload-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.chart-upload-card{padding:18px;border:1px solid rgba(75,185,230,.27);border-radius:13px;background:rgba(3,29,45,.68)}.chart-upload-card h4{margin:0;color:#e9f9ff;font-size:17px}.chart-upload-card p{min-height:40px;margin:7px 0 15px;color:#95b9ca;font-size:13px;line-height:1.65}.chart-upload-trigger{display:flex;align-items:center;justify-content:center;min-height:42px;border:1px dashed rgba(102,221,255,.58);border-radius:8px;color:#c8f5ff;font-weight:800;cursor:pointer;background:rgba(12,112,158,.18);transition:.18s ease}.chart-upload-trigger:hover{border-color:#75e4ff;background:rgba(15,144,195,.31)}.chart-upload-trigger.uploading{pointer-events:none;opacity:.65}.chart-upload-trigger input{display:none}.chart-preview{display:grid;grid-template-columns:104px 1fr auto;gap:12px;align-items:center;margin-top:14px;padding:10px;border:1px solid rgba(105,190,222,.26);border-radius:9px;background:rgba(2,19,31,.58)}.chart-preview :deep(.el-image){width:104px;height:72px;border-radius:6px;cursor:zoom-in;background:#061827}.chart-preview>div{display:grid;gap:4px;min-width:0}.chart-preview span{overflow:hidden;color:#cbe9f5;font-size:13px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.chart-preview small{color:#8fb4c7;font-size:12px}.chart-preview button{border:0;color:#ffc6ce;font-size:13px;cursor:pointer;background:transparent}.decision-hint{margin:8px 0 0;color:#91b8ca;font-size:13px}.reference-downloads{display:flex;flex-wrap:wrap;gap:10px;margin-top:18px}.reference-downloads .btn-nav{display:inline-flex;align-items:center;gap:7px}.reference-downloads .btn-nav::before{content:'↓';font-size:16px}.reference-downloads .btn-nav:hover{color:#e4faff}
@media(max-width:780px){.chart-upload-grid{grid-template-columns:1fr}.chart-preview{grid-template-columns:86px 1fr}.chart-preview :deep(.el-image){width:86px;height:62px}.chart-preview button{grid-column:2;text-align:left;padding:0}.reference-downloads .btn-nav{flex:1;justify-content:center}}
/* 与 foundation/new-product-survey 保持同一套步骤条局部细节;公共规则仍由 student-training.scss 提供。 */
/* AI 侧栏启用后,主内容必须收敛到所属网格列,避免固定宽度侵入侧栏区域。 */
.hardware-cost-page .training-core {
box-sizing: border-box;
min-width: 0;
width: 100%;
margin: 0;
padding: 28px;
border: 1px solid rgba(0, 180, 255, 0.25);
border-radius: 36px;
background: rgba(8, 18, 28, 0.6);
box-shadow: inset 0 1px 0 rgba(169, 229, 255, 0.08), 0 22px 48px rgba(0, 0, 0, 0.22);
}
.hardware-cost-page .task-header {
margin-bottom: 24px;
}
.hardware-cost-page .task-badge {
display: inline-flex;
align-items: center;
min-height: 28px;
margin: 0;
padding: 4px 14px;
border: 1px solid rgba(99, 217, 255, 0.55);
border-radius: 999px;
color: #dff5ff;
background: rgba(17, 126, 178, 0.36);
}
.hardware-cost-page .task-title {
margin: 12px 0 0;
color: #ffffff;
font-size: 30px;
font-weight: 900;
line-height: 1.3;
}
.hardware-cost-page .hardware-workbench {
margin-top: 28px;
padding: 18px 22px 22px;
border: 1px solid rgba(82, 174, 226, 0.42);
border-radius: 26px;
background: rgba(0, 25, 42, 0.64);
}
.hardware-cost-page .hardware-workbench-head {
margin-bottom: 22px;
padding-bottom: 18px;
border-bottom: 1px solid rgba(0, 180, 255, 0.2);
}
.hardware-cost-page .hardware-workbench-head h2 {
margin: 5px 0 0;
color: #ffffff;
font-size: 24px;
line-height: 1.3;
}
.hardware-cost-page .hardware-workbench > .step-indicator {
margin: 0 0 30px !important;
}
.hardware-cost-page .hardware-step-card {
padding: 24px;
border: 1px solid rgba(82, 174, 226, 0.42);
border-radius: 26px;
background: rgba(0, 25, 42, 0.64);
}
.step-marker {
display: inline-flex;
flex: 0 0 38px;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border: 1px solid rgba(101, 176, 208, 0.6);
border-radius: 50%;
color: #a6c4d9;
background: rgba(3, 25, 40, 0.8);
box-shadow: inset 0 0 12px rgba(38, 174, 233, 0.08);
font-size: 12px;
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; min-width: 0; gap: 4px; margin-left: 10px; }
.step-name { display: -webkit-box; overflow: hidden; color: inherit; font-size: 14px; font-weight: 800; line-height: 1.25; text-overflow: ellipsis; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
.step-action { color: #66b6d4; font-size: 10px; font-weight: 800; letter-spacing: 0.06em; line-height: 1; transition: color 0.2s ease; }
.step-tab:hover {
border-color: rgba(89, 223, 255, 0.82);
color: #ffffff;
background: rgba(8, 92, 131, 0.52);
box-shadow: 0 7px 18px rgba(0, 0, 0, 0.24), inset 0 1px 0 rgba(152, 239, 255, 0.16);
transform: translateY(-2px);
}
.step-tab:hover .step-action { color: #9bf4ff; }
.step-tab:focus-visible { outline: 2px solid #72e8ff; outline-offset: 4px; }
.step-tab.active .step-marker {
border-color: #72e8ff;
color: #ffffff;
background: linear-gradient(135deg, #08b7e8, #1375d0);
box-shadow: 0 0 0 5px rgba(25, 179, 237, 0.14), 0 0 18px rgba(46, 208, 255, 0.58);
}
.step-tab.complete { color: #dffbff; }
.step-tab.complete .step-marker {
border-color: rgba(84, 230, 221, 0.85);
color: #062b37;
background: #6ef0e1;
box-shadow: 0 0 12px rgba(79, 231, 218, 0.35);
}
.step-connector {
position: relative;
flex: 0 1 40px;
min-width: 18px;
height: 2px;
margin: 0 8px;
overflow: visible;
background: rgba(98, 169, 201, 0.34);
}
.step-connector::after {
position: absolute;
top: 50%;
right: -1px;
width: 6px;
height: 6px;
border-top: 1px solid #73b1ce;
border-right: 1px solid #73b1ce;
content: '';
transform: translateY(-50%) rotate(45deg);
}
.step-tab.complete + .step-connector {
background: linear-gradient(90deg, #6ef0e1, #1cb8e9);
box-shadow: 0 0 9px rgba(53, 218, 235, 0.46);
}
.step-tab.complete + .step-connector::after { border-color: #63e6f1; }
@media (max-width: 900px) {
.step-indicator {
flex-direction: column;
align-items: stretch;
width: 100%;
margin: 0 0 32px;
padding: 16px;
border-radius: 20px;
overflow-x: visible;
}
.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);
}
}
</style>

@ -18,10 +18,21 @@
</header>
<nav class="step-tabs" aria-label="">
<button type="button" :class="{ active: activeStep === 1 }" @click="activeStep = 1">01 权重设置</button>
<button type="button" :class="{ active: activeStep === 2 }" @click="activeStep = 2">02 渠道数据</button>
<button type="button" :class="{ active: activeStep === 3 }" @click="openStepThree">03 </button>
<button type="button" :class="{ active: activeStep === 4 }" @click="openStepFour">04 </button>
<button type="button" :class="['step-tab', { active: activeStep === 1, complete: activeStep > 1 }]" :aria-current="activeStep === 1 ? 'step' : undefined" @click="activeStep = 1">
<span class="step-marker">01</span><span class="step-copy"><b>权重设置</b><small>{{ activeStep === 1 ? '当前步骤 →' : '点击切换 →' }}</small></span>
</button>
<span class="step-connector" aria-hidden="true" />
<button type="button" :class="['step-tab', { active: activeStep === 2, complete: activeStep > 2 }]" :aria-current="activeStep === 2 ? 'step' : undefined" @click="activeStep = 2">
<span class="step-marker">02</span><span class="step-copy"><b>渠道数据</b><small>{{ activeStep === 2 ? '当前步骤 →' : '点击切换 →' }}</small></span>
</button>
<span class="step-connector" aria-hidden="true" />
<button type="button" :class="['step-tab', { active: activeStep === 3, complete: activeStep > 3 }]" :aria-current="activeStep === 3 ? 'step' : undefined" @click="openStepThree">
<span class="step-marker">03</span><span class="step-copy"><b>归一化评分</b><small>{{ activeStep === 3 ? '当前步骤 →' : '点击切换 →' }}</small></span>
</button>
<span class="step-connector" aria-hidden="true" />
<button type="button" :class="['step-tab', { active: activeStep === 4 }]" :aria-current="activeStep === 4 ? 'step' : undefined" @click="openStepFour">
<span class="step-marker">04</span><span class="step-copy"><b>渠道排名</b><small>{{ activeStep === 4 ? '当前步骤 →' : '点击切换 →' }}</small></span>
</button>
</nav>
<section v-if="activeStep === 1">
@ -223,4 +234,7 @@ onMounted(async () => { try { const result = await getTrainingTaskByKey(TASK_KEY
<style scoped lang="scss">
.release-channel-page{display:grid;grid-template-columns:minmax(720px,1fr) 340px;gap:24px;min-height:calc(100vh - 76px);padding:24px;color:#eef5ff;background:#06111d}.training-core{min-width:0;padding:28px;border:1px solid rgba(0,180,255,.25);border-radius:30px;background:rgba(8,18,28,.6)}.task-header{margin-bottom:24px}.task-badge{display:inline-flex;gap:8px;align-items:center;padding:5px 14px;border:1px solid rgba(99,217,255,.55);border-radius:999px;color:#dff5ff;background:rgba(17,126,178,.36);font-size:12px;font-weight:700}.task-header h1{margin:12px 0 0;color:#fff;font-size:30px}.task-card{margin-top:28px;padding:24px;border:1px solid rgba(82,174,226,.42);border-radius:24px;background:rgba(0,25,42,.64)}.workbench-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding-bottom:18px;border-bottom:1px solid rgba(0,180,255,.2)}.section-eyebrow{margin:0;color:#71dfff;font-size:12px;font-weight:800}.workbench-head h2{margin:4px 0 0;color:#fff;font-size:24px}.step-tabs{display:flex;gap:10px;margin:20px 0}.step-tabs button{min-height:36px;padding:0 15px;border:1px solid rgba(99,183,222,.42);border-radius:8px;color:#a8cde0;background:rgba(3,27,43,.56);font:inherit;font-size:13px;font-weight:800;cursor:pointer}.step-tabs button.active{border-color:#48d5ff;color:#fff;background:rgba(9,125,177,.7)}.lead,.scoring-guide{margin:22px 0;color:#e2f3ff;font-size:16px;line-height:1.9}.scoring-guide{padding:16px 18px;border-left:3px solid #48d5ff;border-radius:0 12px 12px 0;background:rgba(16,85,121,.2)}.table-actions{display:flex;justify-content:flex-end;gap:12px;margin:0 0 16px}.table-actions :deep(.el-button){min-width:110px;height:42px;font-weight:800}.table-wrap{overflow-x:auto;border:1px solid rgba(99,183,222,.46);border-radius:14px}.table-wrap table{width:100%;border-collapse:collapse}.weight-table{min-width:900px}.channel-table{min-width:1320px}.scoring-table{min-width:1780px}.ranking-table{min-width:880px}.table-wrap th,.table-wrap td{padding:12px;border-right:1px solid rgba(99,183,222,.35);border-bottom:1px solid rgba(99,183,222,.35);vertical-align:middle}.table-wrap th{white-space:nowrap;color:#effaff;background:#0c9dd6;font-size:14px}.table-wrap td{background:rgba(2,21,34,.58)}.weight-table tbody td:nth-child(1){width:22%}.weight-table tbody td:nth-child(2){width:43%}.weight-table tbody td:nth-child(3){width:23%}.channel-table td{min-width:134px}.channel-table td:first-child{min-width:110px}.scoring-table th:first-child{min-width:108px}.scoring-table td{min-width:108px}.scoring-table thead tr:nth-child(2) th{background:rgba(7,116,161,.94)}.ranking-table td{min-width:170px}.ranking-table td:first-child{min-width:100px}.rank-cell,.core-cell{text-align:center;font-weight:900}.core-cell .core{display:inline-flex;align-items:center;justify-content:center;min-width:30px;height:26px;border:1px solid rgba(99,235,204,.8);border-radius:999px;color:#062f30;background:#68edce}.weight-cell{color:#a8d9eb;text-align:center;font-weight:800}.analysis-field{display:block;margin-top:28px}.analysis-field>span{display:block;margin-bottom:10px;color:#dff5ff;font-size:15px;font-weight:800}.analysis-field :deep(.el-textarea__inner){min-height:180px!important;border:1px solid rgba(111,190,225,.3);border-radius:12px;box-shadow:none;color:#fff;background:#102233;line-height:1.7}.table-wrap :deep(.el-input__wrapper),.table-wrap :deep(.el-input-number){width:100%;background:#102233}.table-wrap :deep(.el-input__wrapper){box-shadow:0 0 0 1px rgba(111,190,225,.3) inset}.table-wrap :deep(.el-input__inner){color:#fff}.table-wrap :deep(.el-input-number .el-input__wrapper){background:#102233}.table-wrap small{display:block;margin-top:7px;color:#8fa8b8;font-size:12px}.operation-column{width:100px;text-align:center}.empty-row{text-align:center;color:#99b5c7}.table-wrap tfoot th,.table-wrap tfoot td{border-bottom:0;background:rgba(7,49,72,.74)}.total-error{color:#ff8c8c;font-weight:900}.hint{margin:18px 0 0;color:#95b2c3;line-height:1.8}.footer-actions{display:flex;justify-content:center;gap:16px;margin-top:30px}.footer-actions :deep(.el-button){min-width:156px;height:44px;font-weight:800}.footer-actions :deep(.el-button .el-icon){margin-left:5px}@media (max-width:1100px){.release-channel-page{grid-template-columns:1fr}}@media (max-width:720px){.release-channel-page{padding:14px}.training-core{padding:18px}.task-card{padding:18px}.workbench-head{flex-direction:column}.table-actions,.footer-actions{align-items:stretch;flex-direction:column}.table-actions :deep(.el-button),.footer-actions :deep(.el-button){width:100%}}
/* 与标准实训页保持一致的步骤条:编号、步骤状态和箭头连接均可清晰识别。 */
.step-tabs{display:flex;align-items:center;gap:8px;margin:20px 0 28px;padding:18px 22px;overflow-x:auto;border:1px solid rgba(0,180,255,.3);border-radius:22px;background:rgba(1,18,31,.62)}.step-tabs .step-tab{display:flex;flex:1 1 0;align-items:center;min-width:145px;min-height:66px;gap:10px;padding:8px 12px;border:1px solid rgba(68,141,177,.35);border-radius:12px;color:#9ab3d0;background:rgba(3,27,43,.52);font:inherit;text-align:left;cursor:pointer;transition:.2s ease}.step-tabs .step-tab:hover{border-color:rgba(89,223,255,.82);color:#fff;background:rgba(8,92,131,.52);transform:translateY(-1px)}.step-tabs .step-tab:focus-visible{outline:2px solid #72e8ff;outline-offset:3px}.step-tabs .step-marker{display:grid;flex:0 0 38px;width:38px;height:38px;place-items:center;border:1px solid rgba(101,176,208,.6);border-radius:50%;color:#a6c4d9;background:rgba(3,25,40,.8);font-size:12px;font-weight:900}.step-tabs .step-copy{display:grid;min-width:0;gap:4px}.step-tabs .step-copy b{overflow:hidden;font-size:14px;font-weight:800;text-overflow:ellipsis;white-space:nowrap}.step-tabs .step-copy small{color:#66b6d4;font-size:10px;font-weight:800;letter-spacing:.06em}.step-tabs .step-tab.active{border-color:#53d8fb;color:#fff;background:linear-gradient(135deg,rgba(7,156,207,.85),rgba(13,105,161,.92));box-shadow:inset 0 0 24px rgba(77,213,255,.22)}.step-tabs .step-tab.active .step-marker{border-color:#72e8ff;color:#fff;background:linear-gradient(135deg,#08b7e8,#1375d0);box-shadow:0 0 0 5px rgba(25,179,237,.14),0 0 18px rgba(46,208,255,.58)}.step-tabs .step-tab.complete{color:#dffbff}.step-tabs .step-tab.complete .step-marker{border-color:rgba(84,230,221,.85);color:#062b37;background:#6ef0e1}.step-tabs .step-connector{position:relative;flex:0 0 24px;height:2px;background:rgba(86,198,235,.48)}.step-tabs .step-connector::after{position:absolute;top:50%;right:0;width:7px;height:7px;border-top:1px solid #73b1ce;border-right:1px solid #73b1ce;content:'';transform:translateY(-50%) rotate(45deg)}
@media(max-width:720px){.step-tabs{align-items:stretch;flex-direction:column;padding:14px}.step-tabs .step-tab{flex:0 0 auto;width:100%}.step-tabs .step-connector{flex:0 0 18px;width:2px;height:18px;margin-left:18px}.step-tabs .step-connector::after{top:auto;right:auto;bottom:0;left:50%;transform:translateX(-50%) rotate(135deg)}}
</style>

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save