feat: unify student training page layout

fix/non-blocking-route-navigation
chenyuan 3 months ago
parent 33b113e9cb
commit 0235a1eafd

@ -179,31 +179,35 @@
### 5.2 桌面端尺寸与布局
- 步骤栏容器占当前工作台可用宽度的 `100%`;不设置固定总宽度。
- 容器内边距:`6px`;项目间距:`6px`;最小高度:`58px`(由 `46px` 步骤按钮加上下内边距构成)。
- 容器圆角:`60px`;边框:`1px solid rgba(0,180,255,.3)`;背景:`rgba(0,0,0,.3)`。
- 每个步骤项使用 `flex: 1 1 0` 平均分配可用宽度,最小高度 `46px`;禁止按名称设置固定宽度。
- 步骤项左右内边距 `14px`,内容水平/垂直居中;图标与配置文字的间距 `8px`,图标建议尺寸 `16px`。
- 默认文字:`14px / 800 / #9ab3d0`;长名称允许自然换行或在极窄空间缩小布局,不能溢出容器。
- 步骤栏采用**流程式导航**:步骤卡、圆形编号节点、步骤间连线与箭头共同构成流程;不得退回为只有文字的胶囊标签。
- 为避免与工作台内容区过于局促,桌面端步骤栏可相对内容区向左右各扩展 `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`),**已完成步骤不得替换为勾选符号**。已完成态只能通过节点和连线颜色表达。
- 步骤项依次显示“步骤名称”和操作提示;不得显示 `STAGE` 或其他英文阶段眉题。未激活项常驻显示“点击切换 →”,当前项显示“当前步骤”。
- 步骤项之间使用低饱和青蓝 `2px` 连线与右箭头表达先后关系;已完成区段可提高亮度,连线不承担点击区域。
- **桌面端步骤间距(硬性值):** 单段连接线使用 `flex: 0 1 40px`、`min-width: 18px`、`height: 2px`、`margin: 0 8px`;箭头使用 `6px × 6px` 的右向描边。不得沿用历史的 `64px` 连接线或 `0 14px` 两侧外边距,避免步骤卡之间出现过宽空白。
- 步骤名称由配置提供,最多显示两行,超出后使用省略号截断;必须保留完整名称的悬停提示(如 `title`)和可访问名称。不得因长名称压缩编号节点、操作提示或相邻步骤卡。
- 当全部步骤的最小宽度无法容纳时,步骤栏允许横向滚动;禁止把步骤卡、文字或连线压缩变形。
### 5.3 交互状态
| 状态 | 文字 | 背景 | 描边与效果 |
| --- | --- | --- | --- |
| 默认/待完成 | `#9ab3d0` | 透明 | `1px solid transparent` |
| Hover(可点击) | `#e9fbff` | `rgba(17,126,178,.28)` | `rgba(99,217,255,.45)` |
| 当前/激活 | `#ffffff` | `linear-gradient(95deg, #0a6b9e, #0a5c86)` | `rgba(0,170,255,.55)`;`0 4px 12px rgba(0,0,0,.3)` |
| 已完成(如业务提供) | `#dff7ff` | `rgba(13,100,141,.42)` | `rgba(92,224,255,.42)` |
| 禁用(如业务提供) | `#8299aa` | 透明 | `opacity: .45`;无指针手势 |
| 默认/待完成 | `#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`;无指针手势,且不显示“点击切换” |
- 过渡仅用于颜色、背景、描边和阴影,时长 `0.2s`;不得做闪烁、跳动或自动轮播动画。
- 过渡仅用于 `transform`、颜色、背景、描边和阴影,时长 `0.2s`;不得做闪烁、跳动或自动轮播动画。
- 激活状态只表现当前业务状态,不根据名称、索引或固定第一个项目推断。
### 5.4 响应式规则
- 视口宽度大于 `900px`:保持横向均分布局。
- 视口宽度小于等于 `900px`:步骤栏改为纵向排列,步骤项宽度 `100%`,项目间距 `8px`,容器圆角可收敛为 `24px`。
- 任何尺寸下均保留完整的可点击面积;不因名称长度缩小步骤按钮高度至 `40px` 以下。
- 视口宽度大于 `900px`:保持横向流程布局;内容不足时横向滚动,不能挤压变形。
- 视口宽度小于等于 `900px`:步骤栏取消左右扩展,改为纵向排列,步骤项宽度 `100%`;连线改为节点左侧的竖向短线与向下箭头,容器圆角收敛为 `20px`。
- 任何尺寸下均保留完整的可点击面积;不因名称长度缩小步骤按钮高度至 `66px` 以下。
## 6. 公共按钮规范

@ -91,7 +91,8 @@ const getCurrentTeachingClassId = async () => {
return classId;
};
router.beforeEach(async (to, from, next) => {
let routess = proxy.$cache.session.getJSON("disabledTitles");
const disabledTitles = proxy.$cache.session.getJSON("disabledTitles");
const routess = Array.isArray(disabledTitles) ? disabledTitles : [];
// 获取当前和上一个模块
const toModule = findParentByTitle(dynamicRoutes, to.meta.title)?.meta?.title;
const fromModule = findParentByTitle(dynamicRoutes, from.meta.title)?.meta?.title;

@ -4,8 +4,8 @@
<section class="task-header"><div class="task-badge"><el-icon><FirstAidKit /></el-icon> 核心实训 · 产品上线与运营推广</div><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"><div><p class="section-eyebrow">Training Task</p><h3>任务实训</h3><p class="workbench-description">{{ taskRequirement }}</p></div><div class="workbench-actions"><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /><button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon>导出实训成果</button></div></div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="产品问题诊断步骤"><button v-for="(step,index) in configuredSteps" :key="`${step}-${index}`" type="button" class="step-tab" :class="{ active: activeStep === index + 1 }" :aria-current="activeStep === index + 1 ? 'step' : undefined" @click="setCurrentStep(index + 1)">{{ step }}</button></nav>
<div class="workbench-head workbench-head--actions-only"><div class="workbench-actions"><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /><button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon>导出实训成果</button></div></div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="产品问题诊断步骤"><template v-for="(step,index) in configuredSteps" :key="`${step}-${index}`"><button type="button" :class="['step-tab',{active:activeStep===index+1,complete:activeStep>index+1}]" :aria-current="activeStep===index+1?'step':undefined" :aria-label="`第${index+1}步:${step}`" @click="setCurrentStep(index+1)"><span class="step-marker">{{ String(index+1).padStart(2,'0') }}</span><span class="step-copy"><strong :title="step">{{ step }}</strong><span class="step-action">{{ activeStep===index+1?'当前步骤':'点击切换 →' }}</span></span></button><span v-if="index<configuredSteps.length-1" class="step-connector" aria-hidden="true"></span></template></nav>
<section class="step-card">
<div class="step-title-row"><div><p v-if="hasConfiguredSteps" class="section-eyebrow">Step {{ String(activeStep).padStart(2, '0') }}</p><h3>{{ activeStepName }}</h3></div></div>
<nav class="analysis-tabs" role="tablist" aria-label="产品问题诊断工具"><button v-for="(item,index) in modules" :key="item.name" type="button" class="analysis-tab" :class="{ active: activeModule === index }" :aria-selected="activeModule === index" :disabled="item.disabled" role="tab" @click="setActiveModule(index)">{{ item.name }}</button></nav>
@ -62,4 +62,12 @@ onMounted(async () => { try { modules.value.forEach((item) => { try { item.disab
.diagnosis{display:grid;grid-template-columns:minmax(720px,1fr) 340px;gap:24px;min-height:calc(100vh - 76px);padding:24px 24px 32px;color:#eef5ff;background:#06111d}.training-core{min-width: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)}.task-header{margin-bottom:24px}.task-badge{display:inline-flex;align-items:center;gap:8px;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);font-size:12px;font-weight:700}.task-title{margin:12px 0 0;color:#fff;font-size:30px;font-weight:900;line-height:1.3}.task-card,.step-card{border:1px solid rgba(82,174,226,.42);border-radius:26px;background:rgba(0,25,42,.64)}.task-card{margin-top:28px;padding:18px 22px 22px}.workbench-head{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin-bottom:22px;padding-bottom:18px;border-bottom:1px solid rgba(0,180,255,.2)}.section-eyebrow{margin:0;color:#71dfff;font-size:12px;font-weight:800;line-height:1.4}.workbench-head h3{margin:4px 0 8px;color:#fff;font-size:26px;font-weight:800;line-height:1.3}.workbench-description{margin:0;color:#d7ecff;font-size:15px;line-height:1.8}.workbench-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;flex-shrink:0}.outline-action,.btn-nav,.analysis-tab{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:0 18px;border:1px solid rgba(99,217,255,.55);border-radius:999px;color:#e9fbff;background:rgba(17,126,178,.36);font:inherit;font-size:14px;font-weight:800;cursor:pointer}.step-indicator{display:flex;gap:6px;min-height:58px;margin:0 0 32px;padding:6px;overflow-x:auto;border:1px solid rgba(0,180,255,.3);border-radius:999px;background:rgba(0,20,35,.65)}.step-tab{flex:1 0 max-content;min-height:46px;padding:0 18px;border:1px solid transparent;border-radius:999px;color:#9ab3d0;background:transparent;font:inherit;font-size:14px;font-weight:800;cursor:pointer}.step-tab.active,.analysis-tab.active{border-color:rgba(92,224,255,.7);color:#fff;background:linear-gradient(95deg,#0a6b9e,#0a5c86)}.step-card{padding:18px 22px 22px}.step-title-row{margin-bottom:14px}.step-title-row h3{margin:4px 0 0;color:#fff;font-size:22px;font-weight:900}.analysis-tabs{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}.analysis-tab:disabled{cursor:not-allowed;opacity:.45}.business-workspace{min-width:0;overflow:hidden;border:1px solid rgba(0,180,255,.22);border-radius:18px;background:rgba(1,14,26,.72)}.step-navigation{display:flex;justify-content:center;gap:14px;margin-top:22px}.btn-nav.btn-save,.btn-nav.btn-primary{border-color:rgba(92,224,255,.7);color:#fff;background:linear-gradient(95deg,#0b84bd,#096491)}.btn-nav:disabled{cursor:not-allowed;opacity:.45}.training-actions{display:flex;flex-wrap:wrap;justify-content:center;gap:52px;margin-top:36px}.training-actions :deep(.training-action){min-width:172px;height:64px;padding:0 28px;border:1px solid #2c83aa;border-radius:6px;color:#dff7ff;background:rgba(13,64,88,.62);font-size:24px;font-weight:900}
.business-workspace :deep(.training-intro){display:none!important}.business-workspace :deep(.training-step-area){margin:0!important;padding:18px!important;border:0!important;border-radius:0!important;color:#d7ecff!important;background:transparent!important;box-shadow:none!important}.business-workspace :deep(.taskfooter){display:none!important}.business-workspace :deep(.five),.business-workspace :deep(.kano),.business-workspace :deep(.market),.business-workspace :deep(.traffic),.business-workspace :deep(.main-top),.business-workspace :deep(.app-main){color:#d7ecff!important;background:transparent!important}.business-workspace :deep(.el-scrollbar){height:auto!important}.business-workspace :deep(.el-table){--el-table-border-color:rgba(77,167,220,.2);--el-table-header-bg-color:rgba(8,55,82,.98);--el-table-header-text-color:#eaffff;--el-table-row-hover-bg-color:rgba(11,84,122,.45);--el-table-bg-color:rgba(1,14,26,.72);--el-table-tr-bg-color:rgba(1,14,26,.48);--el-table-text-color:#d7f1ff;overflow:hidden;border:1px solid rgba(0,180,255,.22);border-radius:16px;background:rgba(1,14,26,.72)!important}.business-workspace :deep(.el-table th.el-table__cell){color:#eaffff!important;background:rgba(8,55,82,.98)!important;font-weight:900}.business-workspace :deep(.el-table tr),.business-workspace :deep(.el-table td.el-table__cell){color:#d7f1ff!important;background:rgba(1,14,26,.48)!important}.business-workspace :deep(.el-input__wrapper),.business-workspace :deep(.el-textarea__inner),.business-workspace :deep(.el-select__wrapper){border:1px solid rgba(45,95,126,.95)!important;border-radius:14px!important;box-shadow:none!important;color:#eef5ff!important;background:#0f1a24!important}.business-workspace :deep(.el-input__inner),.business-workspace :deep(.el-textarea__inner){color:#eef5ff!important}.business-workspace :deep(.el-button){min-height:40px;padding:0 18px;border:1px solid rgba(92,224,255,.7)!important;border-radius:999px;color:#fff!important;font-size:14px;font-weight:800;background:linear-gradient(95deg,#0b84bd,#096491)!important}.business-workspace :deep(.caseBacktitle){display:inline-flex;align-items:center;min-height:28px;padding:4px 12px;border:1px solid rgba(106,226,255,.45);border-radius:999px;color:#71dfff;background:rgba(0,127,184,.25);font-size:12px;font-weight:800}.business-workspace :deep(.caseBacktitle img){display:none}.business-workspace :deep(.df),.business-workspace :deep(.left),.business-workspace :deep(.right){color:#d7ecff!important;background:transparent!important}
@media(max-width:1100px){.diagnosis{grid-template-columns:1fr}}@media(max-width:900px){.diagnosis{padding:14px}.training-core{padding:18px;border-radius:24px}.workbench-head,.workbench-actions,.step-navigation{flex-direction:column;align-items:stretch}.workbench-actions,.outline-action,.btn-nav,.training-actions :deep(.training-action){width:100%}.step-indicator{flex-direction:column;border-radius:26px}.step-tab{flex:none}}
.diagnosis .step-indicator { display:flex; align-items:center; gap:0; width:calc(100% + 24px); min-height:0; margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:18px; background:linear-gradient(110deg,rgba(4,39,58,.9),rgba(2,20,33,.94)); box-shadow:inset 0 1px 0 rgba(169,229,255,.08); }
.diagnosis .step-tab { display:flex; flex:1 0 145px; min-width:0; align-items:center; min-height:66px; padding:8px 10px; border:1px solid rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); text-align:left; }
.diagnosis .step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,0.78),rgba(5,62,102,0.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }
.diagnosis .step-marker { display:inline-flex; flex:0 0 38px; align-items:center; justify-content:center; width:38px; height:38px; border:1px solid rgba(113,223,255,.55); border-radius:50%; color:#bdefff; background:rgba(3,27,43,.72); font-size:13px; font-weight:900; }.diagnosis .step-copy { display:grid; min-width:0; gap:4px; margin-left:10px; }.diagnosis .step-copy strong { display:-webkit-box; overflow:hidden; color:inherit; font-size:14px; line-height:1.25; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.diagnosis .step-action { color:#7ec7e7; font-size:12px; font-weight:700; }
.diagnosis .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); }.diagnosis .step-tab.complete { color:#dffbff; }.diagnosis .step-tab.complete .step-marker { border-color:rgba(84,230,221,.85); color:#062b37; background:#6ef0e1; box-shadow:0 0 12px rgba(79,231,218,.35); }
.diagnosis .step-connector { position:relative; flex:0 1 40px; min-width:18px; height:2px; margin:0 8px; overflow:visible; background:rgba(77,167,220,.54); }.diagnosis .step-connector::after { position:absolute; top:50%; right:-1px; width:6px; height:6px; border-top:1px solid currentColor; border-right:1px solid currentColor; color:#78cfe9; content:""; transform:translateY(-50%) rotate(45deg); }.diagnosis .step-tab.complete + .step-connector { background:linear-gradient(90deg,#6ef0e1,#1cb8e9); box-shadow:0 0 9px rgba(53,218,235,.46); }.diagnosis .step-tab.complete + .step-connector::after { color:#63e6f1; }
@media(max-width:900px){.diagnosis .step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; }.diagnosis .step-tab { flex:0 0 auto; width:100%; }.diagnosis .step-connector { flex:0 0 20px; min-width:0; width:2px; height:20px; margin:0 0 0 29px; }.diagnosis .step-connector::after { top:auto; right:50%; bottom:-1px; transform:translateX(50%) rotate(135deg); }}
.diagnosis .workbench-head--actions-only { justify-content:flex-end; margin-bottom:18px; padding-bottom:0; border-bottom:0; }
</style>

@ -4,8 +4,8 @@
<section class="task-header"><div class="task-badge"><el-icon><TrendCharts /></el-icon> 核心实训 · 产品上线与运营推广</div><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"><div><p class="section-eyebrow">Training Task</p><h3>任务实训</h3><p class="workbench-description">{{ taskRequirement }}</p></div><div class="workbench-actions"><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /><button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon>导出实训成果</button></div></div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="品类结构优化步骤"><button v-for="(step,index) in configuredSteps" :key="`${step}-${index}`" type="button" class="step-tab" :class="{active:activeStep===index+1}" :aria-current="activeStep===index+1?'step':undefined" @click="setCurrentStep(index+1)">{{ step }}</button></nav>
<div class="workbench-head workbench-head--actions-only"><div class="workbench-actions"><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /><button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon>导出实训成果</button></div></div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="品类结构优化步骤"><template v-for="(step,index) in configuredSteps" :key="`${step}-${index}`"><button type="button" :class="['step-tab',{active:activeStep===index+1,complete:activeStep>index+1}]" :aria-current="activeStep===index+1?'step':undefined" :aria-label="`第${index+1}步:${step}`" @click="setCurrentStep(index+1)"><span class="step-marker">{{ String(index+1).padStart(2,'0') }}</span><span class="step-copy"><strong :title="step">{{ step }}</strong><span class="step-action">{{ activeStep===index+1?'当前步骤':'点击切换 →' }}</span></span></button><span v-if="index<configuredSteps.length-1" class="step-connector" aria-hidden="true"></span></template></nav>
<section v-show="!hasConfiguredSteps || activeStep <= 2" class="step-card">
<div class="step-title-row"><div><p v-if="hasConfiguredSteps" class="section-eyebrow">Step {{ String(activeStep).padStart(2,'0') }}</p><h3>{{ activeStepName }}</h3></div></div>
<div class="analysis-tabs" role="tablist" aria-label="品类分析工具"><button v-for="(item,index) in modules" :key="item.name" type="button" class="analysis-tab" :class="{active:activeModule===index}" :aria-selected="activeModule===index" role="tab" @click="setActiveModule(index)">{{ item.name }}</button></div>
@ -55,4 +55,28 @@ onMounted(async()=>{ try{const res=await getTrainingTaskByKey(TASK_KEY);taskConf
<style scoped lang="scss">
.optimization{display:grid;grid-template-columns:minmax(720px,1fr) 340px;gap:24px;min-height:calc(100vh - 76px);padding:24px 24px 32px;color:#eef5ff;background:#06111d}.training-core{min-width: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)}.task-header{margin-bottom:24px}.task-badge{display:inline-flex;align-items:center;gap:8px;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);font-size:12px;font-weight:700}.task-title{margin:12px 0 0;color:#fff;font-size:30px;font-weight:900;line-height:1.3}.task-card,.step-card{border:1px solid rgba(82,174,226,.42);border-radius:26px;background:rgba(0,25,42,.64)}.task-card{margin-top:28px;padding:18px 22px 22px}.workbench-head{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin-bottom:22px;padding-bottom:18px;border-bottom:1px solid rgba(0,180,255,.2)}.section-eyebrow{margin:0;color:#71dfff;font-size:12px;font-weight:800;line-height:1.4}.workbench-head h3{margin:4px 0 8px;color:#fff;font-size:26px;font-weight:800;line-height:1.3}.workbench-description,.step-desc{margin:0;color:#d7ecff;font-size:15px;line-height:1.8}.workbench-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;flex-shrink:0}.outline-action,.btn-nav,.analysis-tab{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:0 18px;border:1px solid rgba(99,217,255,.55);border-radius:999px;color:#e9fbff;background:rgba(17,126,178,.36);font:inherit;font-size:14px;font-weight:800;cursor:pointer}.step-indicator{display:flex;gap:6px;min-height:58px;margin:0 0 32px;padding:6px;overflow-x:auto;border:1px solid rgba(0,180,255,.3);border-radius:999px;background:rgba(0,20,35,.65)}.step-tab{flex:1 0 max-content;min-height:46px;padding:0 18px;border:1px solid transparent;border-radius:999px;color:#9ab3d0;background:transparent;font:inherit;font-size:14px;font-weight:800;cursor:pointer}.step-tab.active,.analysis-tab.active{border-color:rgba(92,224,255,.7);color:#fff;background:linear-gradient(95deg,#0a6b9e,#0a5c86)}.step-card{margin-top:32px;padding:18px 22px 22px}.step-indicator+.step-card,.workbench-head+.step-card{margin-top:0}.step-title-row{margin-bottom:14px}.step-title-row h3{margin:4px 0 0;color:#fff;font-size:22px;font-weight:900}.analysis-tabs{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}.business-workspace{min-width:0;overflow:hidden;border:1px solid rgba(0,180,255,.22);border-radius:18px;background:rgba(1,14,26,.72)}.strategy-card{display:grid;gap:6px;margin-top:18px;padding:16px;border:1px solid rgba(0,180,255,.22);border-radius:16px;background:rgba(1,14,26,.72)}.strategy-card strong{color:#71dfff}.strategy-card span{color:#d7ecff;font-size:14px;line-height:1.7}.step-navigation{display:flex;justify-content:center;gap:14px;margin-top:22px}.btn-nav.btn-save,.btn-nav.btn-primary{border-color:rgba(92,224,255,.7);color:#fff;background:linear-gradient(95deg,#0b84bd,#096491)}.btn-nav:disabled{cursor:not-allowed;opacity:.45}.training-actions{display:flex;flex-wrap:wrap;justify-content:center;gap:52px;margin-top:36px}.training-actions :deep(.training-action){min-width:172px;height:64px;padding:0 28px;border:1px solid #2c83aa;border-radius:6px;color:#dff7ff;background:rgba(13,64,88,.62);font-size:24px;font-weight:900}@media(max-width:1100px){.optimization{grid-template-columns:1fr}}@media(max-width:900px){.optimization{padding:14px}.training-core{padding:18px;border-radius:24px}.workbench-head,.workbench-actions,.step-navigation{flex-direction:column;align-items:stretch}.workbench-actions,.outline-action,.btn-nav,.training-actions :deep(.training-action){width:100%}.step-indicator{flex-direction:column;border-radius:26px}.step-tab{flex:none}}
.optimization {
.workbench-actions { margin-left: auto; }
.step-indicator { display:flex; align-items:center; gap:12px; width:calc(100% + 24px); min-height:0; margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:18px; background:linear-gradient(110deg,rgba(4,39,58,.9),rgba(2,20,33,.94)); box-shadow:inset 0 1px 0 rgba(169,229,255,.08); }
.step-tab { display:flex; flex:1 0 145px; align-items:center; min-height:66px; padding:8px 10px; border:1px solid rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); text-align:left; transition:transform .2s,color .2s,background .2s,border-color .2s,box-shadow .2s; }
.step-tab:hover,.step-tab:focus-visible { transform:translateY(-2px); border-color:rgba(89,223,255,.82); color:#fff; background:rgba(8,92,131,.52); box-shadow:0 8px 18px rgba(0,0,0,.2); outline:none; }
.step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(95deg,#0a6b9e,#0a5c86); box-shadow:0 8px 20px rgba(0,120,170,.22); }
.step-marker { display:inline-flex; flex:0 0 38px; align-items:center; justify-content:center; width:38px; height:38px; border:1px solid rgba(113,223,255,.55); border-radius:50%; color:#bdefff; background:rgba(3,27,43,.72); font-size:13px; font-weight:900; }
.step-copy { display:grid; min-width:0; gap:4px; margin-left:10px; }.step-copy strong { display:-webkit-box; overflow:hidden; color:inherit; font-size:14px; line-height:1.25; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.step-copy span { color:#7ec7e7; font-size:12px; font-weight:700; }.step-tab.active .step-marker { border-color:#a8f3ff; color:#fff; background:linear-gradient(135deg,#0bb2ed,#0a6b9e); box-shadow:0 0 0 4px rgba(77,215,255,.13),0 0 18px rgba(77,215,255,.4); }.step-tab.complete .step-marker { border-color:#59e0ca; color:#dffbff; background:rgba(13,132,119,.5); }.step-connector { flex:0 0 28px; height:2px; background:rgba(77,167,220,.54); color:#78cfe9; font-size:16px; line-height:0; text-align:right; }.step-connector.complete { background:#55ddcc; color:#75f0df; }
}
@media (max-width:1100px) { .optimization { grid-template-columns:1fr; } } @media (max-width:900px) { .optimization { padding:14px; .training-core { padding:18px; border-radius:24px; }.workbench-head,.workbench-actions,.step-navigation { flex-direction:column; align-items:stretch; }.workbench-actions,.outline-action,.btn-nav,.training-actions :deep(.training-action) { width:100%; }.step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; }.step-tab { flex:0 0 auto; width:100%; }.step-connector { flex:0 0 22px; width:2px; height:22px; margin-left:29px; text-align:center; writing-mode:vertical-rl; }.training-actions { gap:14px; } } }
.optimization .step-indicator { display:flex; align-items:center; gap:0; width:calc(100% + 24px); min-height:0; margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:18px; background:linear-gradient(110deg,rgba(4,39,58,.9),rgba(2,20,33,.94)); box-shadow:inset 0 1px 0 rgba(169,229,255,.08); }
.optimization .step-tab { display:flex; flex:1 0 145px; min-width:0; align-items:center; min-height:66px; padding:8px 10px; border:1px solid rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); text-align:left; }
.optimization .step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,0.78),rgba(5,62,102,0.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }
.optimization .step-marker { display:inline-flex; flex:0 0 38px; align-items:center; justify-content:center; width:38px; height:38px; border:1px solid rgba(113,223,255,.55); border-radius:50%; color:#bdefff; background:rgba(3,27,43,.72); font-size:13px; font-weight:900; }
.optimization .step-copy { display:grid; min-width:0; gap:4px; margin-left:10px; }.optimization .step-copy strong { display:-webkit-box; overflow:hidden; color:inherit; font-size:14px; line-height:1.25; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.optimization .step-action { color:#7ec7e7; font-size:12px; font-weight:700; }
.optimization .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); }.optimization .step-tab.complete { color:#dffbff; }.optimization .step-tab.complete .step-marker { border-color:rgba(84,230,221,.85); color:#062b37; background:#6ef0e1; box-shadow:0 0 12px rgba(79,231,218,.35); }
.optimization .step-connector { position:relative; flex:0 1 40px; min-width:18px; height:2px; margin:0 8px; overflow:visible; background:rgba(77,167,220,.54); }.optimization .step-connector::after { position:absolute; top:50%; right:-1px; width:6px; height:6px; border-top:1px solid currentColor; border-right:1px solid currentColor; color:#78cfe9; content:""; transform:translateY(-50%) rotate(45deg); }.optimization .step-tab.complete + .step-connector { background:linear-gradient(90deg,#6ef0e1,#1cb8e9); box-shadow:0 0 9px rgba(53,218,235,.46); }.optimization .step-tab.complete + .step-connector::after { color:#63e6f1; }
@media (max-width:900px) { .optimization .step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; }.optimization .step-tab { flex:0 0 auto; width:100%; }.optimization .step-connector { flex:0 0 20px; min-width:0; width:2px; height:20px; margin:0 0 0 29px; }.optimization .step-connector::after { top:auto; right:50%; bottom:-1px; transform:translateX(50%) rotate(135deg); } }
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -4,8 +4,8 @@
<section class="task-header"><div class="task-badge"><el-icon><TrendCharts /></el-icon> 核心实训 · 供应渠道管理</div><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"><div><p class="section-eyebrow">Training Task</p><h3>任务实训</h3><p class="workbench-description">{{ taskRequirement }}</p></div><div class="workbench-actions"><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /><button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon>导出实训成果</button></div></div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="采购需求预测步骤"><button v-for="(step,index) in configuredSteps" :key="`${step}-${index}`" type="button" class="step-tab" :class="{active:activeStep===index+1}" :aria-current="activeStep===index+1?'step':undefined" @click="setCurrentStep(index+1)">{{ step }}</button></nav>
<div class="workbench-head workbench-head--actions-only"><div class="workbench-actions"><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /><button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon>导出实训成果</button></div></div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="采购需求预测步骤"><template v-for="(step,index) in configuredSteps" :key="`${step}-${index}`"><button type="button" :class="['step-tab',{active:activeStep===index+1,complete:activeStep>index+1}]" :aria-current="activeStep===index+1?'step':undefined" :aria-label="`第${index+1}步:${step}`" @click="setCurrentStep(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">{{ activeStep===index+1?'当前步骤':'点击切换' }} <span aria-hidden="true">→</span></span></span></button><span v-if="index<configuredSteps.length-1" class="step-connector" aria-hidden="true"></span></template></nav>
<section v-show="isSectionVisible(1)" class="step-card"><div class="step-title-row"><div><p class="section-eyebrow">Step {{ String(activeStep).padStart(2,'0') }}</p><h3>{{ activeStepName }}</h3></div></div>
<p style="font-size: 16px; font-weight: normal">季节指数预测法,亦称“温斯特法”,是根据时间序列中的数据资料所呈现的季节变动规律性,对预测目标未来状况作出预测的方法。它首先建立预测模型来求得历史上各期的趋势值,然后用实际值除以趋势值来求出季节指数。若是有两个以上的波动周期,还需要求出相同时期的季节指数平均值,再用它去修正预测值。</p>
@ -745,10 +745,36 @@ onMounted(async () => {
display: grid; grid-template-columns: minmax(720px, 1fr) 340px; gap: 24px; min-height: calc(100vh - 76px); padding: 24px 24px 32px; color: #eef5ff; background: #06111d;
.training-core { min-width: 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); }
.task-header { margin-bottom: 24px; }.task-badge { display: inline-flex; align-items: center; gap: 8px; 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); font-size: 12px; font-weight: 700; }.task-title { margin: 12px 0 0; color: #fff; font-size: 30px; font-weight: 900; line-height: 1.3; }
.task-card,.step-card { border: 1px solid rgba(82,174,226,.42); border-radius: 26px; background: rgba(0,25,42,.64); }.task-card { margin-top: 28px; padding: 18px 22px 22px; }.workbench-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; margin-bottom: 22px; padding-bottom: 18px; border-bottom: 1px solid rgba(0,180,255,.2); }.section-eyebrow { margin: 0; color: #71dfff; font-size: 12px; font-weight: 800; line-height: 1.4; }.workbench-head h3 { margin: 4px 0 8px; color: #fff; font-size: 26px; font-weight: 800; line-height: 1.3; }.workbench-description { margin: 0; color: #d7ecff; font-size: 15px; line-height: 1.8; }.workbench-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; flex-shrink: 0; }
.outline-action,.btn-nav { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 40px; padding: 0 18px; border: 1px solid rgba(99,217,255,.55); border-radius: 999px; color: #e9fbff; background: rgba(17,126,178,.36); font: inherit; font-size: 14px; font-weight: 800; cursor: pointer; }.step-indicator { display: flex; gap: 6px; min-height: 58px; margin: 0 0 32px; padding: 6px; overflow-x: auto; border: 1px solid rgba(0,180,255,.3); border-radius: 999px; background: rgba(0,20,35,.65); }.step-tab { flex: 1 0 max-content; min-height: 46px; padding: 0 18px; border: 1px solid transparent; border-radius: 999px; color: #9ab3d0; background: transparent; font: inherit; font-size: 14px; font-weight: 800; cursor: pointer; }.step-tab.active { border-color: rgba(92,224,255,.7); color: #fff; background: linear-gradient(95deg,#0a6b9e,#0a5c86); }.step-card { margin-top: 32px; padding: 18px 22px 22px; }.step-indicator + .step-card,.workbench-head + .step-card { margin-top: 0; }.step-title-row { margin-bottom: 14px; }.step-title-row h3 { margin: 4px 0 0; color: #fff; font-size: 22px; font-weight: 900; line-height: 1.3; }.caseBacktitle { display: flex; align-items: center; gap: 8px; margin: 20px 0 10px; color: #dff5ff; font-size: 16px; font-weight: 800; }.caseBacktitle img { display: none; }.step-card > p { color: #d7ecff; font-size: 15px; line-height: 1.8; }
.task-card,.step-card { border: 1px solid rgba(82,174,226,.42); border-radius: 26px; background: rgba(0,25,42,.64); }.task-card { margin-top: 28px; padding: 18px 22px 22px; }.workbench-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; margin-bottom: 22px; padding-bottom: 18px; border-bottom: 1px solid rgba(0,180,255,.2); }.section-eyebrow { margin: 0; color: #71dfff; font-size: 12px; font-weight: 800; line-height: 1.4; }.workbench-head h3 { margin: 4px 0 8px; color: #fff; font-size: 26px; font-weight: 800; line-height: 1.3; }.workbench-description { margin: 0; color: #d7ecff; font-size: 15px; line-height: 1.8; }.workbench-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; margin-left: auto; gap: 10px; flex-shrink: 0; }
.outline-action,.btn-nav { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 40px; padding: 0 18px; border: 1px solid rgba(99,217,255,.55); border-radius: 999px; color: #e9fbff; background: rgba(17,126,178,.36); font: inherit; font-size: 14px; font-weight: 800; cursor: pointer; }.step-indicator { display:flex; align-items:center; gap:12px; width:calc(100% + 24px); min-height:0; margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:18px; background:linear-gradient(110deg,rgba(4,39,58,.9),rgba(2,20,33,.94)); box-shadow:inset 0 1px 0 rgba(169,229,255,.08); }.step-tab { display:flex; flex:1 0 145px; align-items:center; min-height:66px; padding:8px 10px; border:1px solid rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); font:inherit; font-size:14px; font-weight:800; text-align:left; cursor:pointer; transition:transform .2s,color .2s,background .2s,border-color .2s,box-shadow .2s; }.step-tab:hover,.step-tab:focus-visible { transform:translateY(-2px); border-color:rgba(89,223,255,.82); color:#fff; background:rgba(8,92,131,.52); box-shadow:0 8px 18px rgba(0,0,0,.2); outline:none; }.step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(95deg,#0a6b9e,#0a5c86); box-shadow:0 8px 20px rgba(0,120,170,.22); }.step-marker { display:inline-flex; flex:0 0 38px; align-items:center; justify-content:center; width:38px; height:38px; border:1px solid rgba(113,223,255,.55); border-radius:50%; color:#bdefff; background:rgba(3,27,43,.72); font-size:13px; font-weight:900; }.step-copy { display:grid; min-width:0; gap:4px; margin-left:10px; }.step-copy strong { display:-webkit-box; overflow:hidden; color:inherit; font-size:14px; line-height:1.25; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.step-copy span { color:#7ec7e7; font-size:12px; font-weight:700; }.step-tab.active .step-marker { border-color:#a8f3ff; color:#fff; background:linear-gradient(135deg,#0bb2ed,#0a6b9e); box-shadow:0 0 0 4px rgba(77,215,255,.13),0 0 18px rgba(77,215,255,.4); }.step-tab.complete .step-marker { border-color:#59e0ca; color:#dffbff; background:rgba(13,132,119,.5); }.step-connector { flex:0 0 28px; height:2px; background:rgba(77,167,220,.54); color:#78cfe9; font-size:16px; line-height:0; text-align:right; }.step-connector.complete { background:#55ddcc; color:#75f0df; }.step-card { margin-top: 32px; padding: 18px 22px 22px; }.step-indicator + .step-card,.workbench-head + .step-card { margin-top: 0; }.step-title-row { margin-bottom: 14px; }.step-title-row h3 { margin: 4px 0 0; color: #fff; font-size: 22px; font-weight: 900; line-height: 1.3; }.caseBacktitle { display: flex; align-items: center; gap: 8px; margin: 20px 0 10px; color: #dff5ff; font-size: 16px; font-weight: 800; }.caseBacktitle img { display: none; }.step-card > p { color: #d7ecff; font-size: 15px; line-height: 1.8; }
:deep(.el-table) { width: 100% !important; max-width: 100%; border-radius: 18px; }:deep(.el-table th.el-table__cell) { background: rgba(8,55,82,.98) !important; }:deep(.el-input__wrapper) { min-height: 42px; border-radius: 14px; background: #0f1a24; }.online { border-radius: 14px; background: rgba(1,14,26,.72); }.step-navigation { display: flex; justify-content: center; gap: 14px; margin-top: 22px; }.btn-nav.btn-save,.btn-nav.btn-primary { border-color: rgba(92,224,255,.7); color: #fff; background: linear-gradient(95deg,#0b84bd,#096491); }.btn-nav:disabled { cursor: not-allowed; opacity: .45; }.taskfooter { gap: 52px; margin-top: 36px; padding-top: 0; border-top: 0; }.taskfooter :deep(.el-button) { min-width: 172px; height: 64px; border: 1px solid #2c83aa !important; border-radius: 6px; color: #dff7ff !important; background: rgba(13,64,88,.62) !important; font-size: 24px; font-weight: 900; }
}
@media (max-width: 1100px) { .channel { grid-template-columns: 1fr; } }
@media (max-width: 900px) { .channel { padding: 14px; .training-core { padding: 18px; border-radius: 24px; }.workbench-head,.workbench-actions,.step-navigation { flex-direction: column; align-items: stretch; }.workbench-actions,.outline-action,.btn-nav,.taskfooter :deep(.el-button) { width: 100%; }.step-indicator { flex-direction: column; border-radius: 26px; }.step-tab { flex: none; }.taskfooter { gap: 14px; } } }
@media (max-width: 900px) { .channel { padding: 14px; .training-core { padding: 18px; border-radius: 24px; }.workbench-head,.workbench-actions,.step-navigation { flex-direction: column; align-items: stretch; }.workbench-actions,.outline-action,.btn-nav,.taskfooter :deep(.el-button) { width: 100%; }.step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; }.step-tab { flex:0 0 auto; width:100%; }.step-connector { flex:0 0 22px; width:2px; height:22px; margin-left:29px; text-align:center; writing-mode:vertical-rl; }.taskfooter { gap: 14px; } } }
.step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,.78),rgba(5,62,102,.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }.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; box-shadow:0 0 12px rgba(79,231,218,.35); }.step-connector.complete { background:linear-gradient(90deg,#6ef0e1,#1cb8e9); color:#63e6f1; box-shadow:0 0 9px rgba(53,218,235,.46); }
/* Standard page step navigation */
.step-indicator { position:relative; display:flex; align-items:center; gap:0; width:calc(100% + 24px); margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:18px; background:linear-gradient(90deg,rgba(6,42,61,.58),rgba(1,18,31,.42)),rgba(0,0,0,.3); box-shadow:inset 0 1px 0 rgba(145,233,255,.08); 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 rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); box-shadow:inset 0 1px 0 rgba(135,221,255,.04); font:inherit; font-size:14px; font-weight:800; text-align:left; cursor:pointer; transition:transform .2s ease,color .2s ease,border-color .2s ease,background .2s ease,box-shadow .2s ease; }
.step-tab:hover { transform:translateY(-2px); border-color:rgba(89,223,255,.82); color:#fff; background:rgba(8,92,131,.52); box-shadow:0 7px 18px rgba(0,0,0,.24),inset 0 1px 0 rgba(152,239,255,.16); }
.step-tab:hover .step-action { color:#9bf4ff; }
.step-tab:focus-visible { outline:2px solid rgba(92,224,255,.7); outline-offset:2px; }
.step-tab.active { border-color:rgba(89,226,255,0.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,0.78),rgba(5,62,102,0.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }
.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,.6); border-radius:50%; color:#a6c4d9; background:rgba(3,25,40,.8); box-shadow:inset 0 0 12px rgba(38,174,233,.08); font-size:12px; font-weight:900; letter-spacing:.04em; }
.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:.06em; line-height:1; }
.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 .step-marker { border-color:rgba(84,230,221,.85); color:#062b37; background:#6ef0e1; box-shadow:0 0 12px rgba(79,231,218,.35); }
.step-connector { position:relative; flex:0 1 40px; min-width:18px; height:2px; margin:0 8px; overflow:visible; background:rgba(98,169,201,.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,.46); }
.step-tab.complete + .step-connector::after { border-color:#63e6f1; }
@media (max-width:900px) { .step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; } .step-tab { flex:0 0 auto; width:100%; } .step-connector { flex:0 0 20px; min-width:0; width:2px; height:20px; margin-left:29px; } }
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -16,12 +16,17 @@
</section>
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<section class="task-card">
<div class="workbench-head">
<div><p class="section-eyebrow">Training Task</p><h3>任务实训</h3><p class="workbench-description">{{ taskRequirement }}</p></div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions"><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /><button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon>导出实训成果</button></div>
</div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="供应商评估步骤">
<button v-for="(step, index) in configuredSteps" :key="`${step}-${index}`" type="button" class="step-tab" :class="{ active: activeStep === index + 1 }" :aria-current="activeStep === index + 1 ? 'step' : undefined" @click="setCurrentStep(index + 1)">{{ step }}</button>
<template v-for="(step, index) in configuredSteps" :key="`${step}-${index}`">
<button type="button" :class="['step-tab', { active: activeStep === index + 1, complete: activeStep > index + 1 }]" :aria-current="activeStep === index + 1 ? 'step' : undefined" :aria-label="`第${index + 1}步:${step}`" @click="setCurrentStep(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">{{ activeStep === index + 1 ? "当前步骤" : "点击切换" }} <span aria-hidden="true">→</span></span></span>
</button>
<span v-if="index < configuredSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section v-show="isSectionVisible(1)" class="step-card">
<div class="step-title-row"><div><p class="section-eyebrow">Step {{ String(activeStep).padStart(2, '0') }}</p><h3>{{ activeStepName }}</h3></div></div>
@ -1134,11 +1139,20 @@ onMounted(async () => {
.section-eyebrow { margin: 0; color: #71dfff; font-size: 12px; font-weight: 800; line-height: 1.4; }
.workbench-head h3 { margin: 4px 0 8px; color: #fff; font-size: 26px; font-weight: 800; line-height: 1.3; }
.workbench-description { margin: 0; color: #d7ecff; font-size: 15px; line-height: 1.8; }
.workbench-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; flex-shrink: 0; }
.workbench-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; margin-left: auto; gap: 10px; flex-shrink: 0; }
.outline-action, .btn-nav { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 40px; padding: 0 18px; border: 1px solid rgba(99, 217, 255, .55); border-radius: 999px; color: #e9fbff; background: rgba(17, 126, 178, .36); font: inherit; font-size: 14px; font-weight: 800; cursor: pointer; }
.step-indicator { display: flex; gap: 6px; min-height: 58px; margin: 0 0 32px; padding: 6px; overflow-x: auto; border: 1px solid rgba(0, 180, 255, .3); border-radius: 999px; background: rgba(0, 20, 35, .65); }
.step-tab { flex: 1 0 max-content; min-height: 46px; padding: 0 18px; border: 1px solid transparent; border-radius: 999px; color: #9ab3d0; background: transparent; font: inherit; font-size: 14px; font-weight: 800; cursor: pointer; }
.step-tab.active { border-color: rgba(92, 224, 255, .7); color: #fff; background: linear-gradient(95deg, #0a6b9e, #0a5c86); }
.step-indicator { display: flex; align-items: center; gap: 12px; width: calc(100% + 24px); min-height: 0; margin: 0 -12px 32px; padding: 18px 22px; overflow-x: auto; border: 1px solid rgba(0, 180, 255, .3); border-radius: 18px; background: linear-gradient(110deg, rgba(4, 39, 58, .9), rgba(2, 20, 33, .94)); box-shadow: inset 0 1px 0 rgba(169, 229, 255, .08); }
.step-tab { display: flex; flex: 1 0 145px; align-items: center; min-height: 66px; padding: 8px 10px; border: 1px solid rgba(68, 141, 177, .35); border-radius: 8px; color: #9ab3d0; background: rgba(3, 27, 43, .52); font: inherit; font-size: 14px; font-weight: 800; text-align: left; cursor: pointer; transition: transform .2s, color .2s, background .2s, border-color .2s, box-shadow .2s; }
.step-tab:hover, .step-tab:focus-visible { transform: translateY(-2px); border-color: rgba(89, 223, 255, .82); color: #fff; background: rgba(8, 92, 131, .52); box-shadow: 0 8px 18px rgba(0, 0, 0, .2); outline: none; }
.step-tab.active { border-color: rgba(89, 226, 255, .85); color: #fff; background: linear-gradient(95deg, #0a6b9e, #0a5c86); box-shadow: 0 8px 20px rgba(0, 120, 170, .22); }
.step-marker { display: inline-flex; flex: 0 0 38px; align-items: center; justify-content: center; width: 38px; height: 38px; border: 1px solid rgba(113, 223, 255, .55); border-radius: 50%; color: #bdefff; background: rgba(3, 27, 43, .72); font-size: 13px; font-weight: 900; }
.step-copy { display: grid; min-width: 0; gap: 4px; margin-left: 10px; }
.step-copy strong { display: -webkit-box; overflow: hidden; color: inherit; font-size: 14px; line-height: 1.25; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
.step-copy span { color: #7ec7e7; font-size: 12px; font-weight: 700; }
.step-tab.active .step-marker { border-color: #a8f3ff; color: #fff; background: linear-gradient(135deg, #0bb2ed, #0a6b9e); box-shadow: 0 0 0 4px rgba(77, 215, 255, .13), 0 0 18px rgba(77, 215, 255, .4); }
.step-tab.complete .step-marker { border-color: #59e0ca; color: #dffbff; background: rgba(13, 132, 119, .5); }
.step-connector { flex: 0 0 28px; height: 2px; background: rgba(77, 167, 220, .54); color: #78cfe9; font-size: 16px; line-height: 0; text-align: right; }
.step-connector.complete { background: #55ddcc; color: #75f0df; }
.step-card { margin-top: 32px; padding: 18px 22px 22px; }
.step-indicator + .step-card, .workbench-head + .step-card { margin-top: 0; }
.step-title-row { margin-bottom: 14px; }
@ -1158,5 +1172,31 @@ onMounted(async () => {
}
@media (max-width: 1100px) { .supplier { grid-template-columns: 1fr; } }
@media (max-width: 900px) { .supplier { padding: 14px; .training-core { padding: 18px; border-radius: 24px; } .workbench-head, .workbench-actions, .step-navigation { flex-direction: column; align-items: stretch; } .workbench-actions, .outline-action, .btn-nav, .taskfooter :deep(.el-button) { width: 100%; } .step-indicator { flex-direction: column; border-radius: 26px; } .step-tab { flex: none; } .taskfooter { gap: 14px; } } }
@media (max-width: 900px) { .supplier { padding: 14px; .training-core { padding: 18px; border-radius: 24px; } .workbench-head, .workbench-actions, .step-navigation { flex-direction: column; align-items: stretch; } .workbench-actions, .outline-action, .btn-nav, .taskfooter :deep(.el-button) { width: 100%; } .step-indicator { align-items: stretch; flex-direction: column; width: 100%; margin-right: 0; margin-left: 0; overflow: visible; border-radius: 20px; } .step-tab { flex: 0 0 auto; width: 100%; } .step-connector { flex: 0 0 22px; width: 2px; height: 22px; margin-left: 29px; text-align: center; writing-mode: vertical-rl; } .taskfooter { gap: 14px; } } }
.step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,.78),rgba(5,62,102,.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }.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; box-shadow:0 0 12px rgba(79,231,218,.35); }.step-connector.complete { background:linear-gradient(90deg,#6ef0e1,#1cb8e9); color:#63e6f1; box-shadow:0 0 9px rgba(53,218,235,.46); }
/* Standard page step navigation */
.step-indicator { position:relative; display:flex; align-items:center; gap:0; width:calc(100% + 24px); margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:18px; background:linear-gradient(90deg,rgba(6,42,61,.58),rgba(1,18,31,.42)),rgba(0,0,0,.3); box-shadow:inset 0 1px 0 rgba(145,233,255,.08); 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 rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); box-shadow:inset 0 1px 0 rgba(135,221,255,.04); font:inherit; font-size:14px; font-weight:800; text-align:left; cursor:pointer; transition:transform .2s ease,color .2s ease,border-color .2s ease,background .2s ease,box-shadow .2s ease; }
.step-tab:hover { transform:translateY(-2px); border-color:rgba(89,223,255,.82); color:#fff; background:rgba(8,92,131,.52); box-shadow:0 7px 18px rgba(0,0,0,.24),inset 0 1px 0 rgba(152,239,255,.16); }
.step-tab:hover .step-action { color:#9bf4ff; }
.step-tab:focus-visible { outline:2px solid rgba(92,224,255,.7); outline-offset:2px; }
.step-tab.active { border-color:rgba(89,226,255,0.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,0.78),rgba(5,62,102,0.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }
.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,.6); border-radius:50%; color:#a6c4d9; background:rgba(3,25,40,.8); box-shadow:inset 0 0 12px rgba(38,174,233,.08); font-size:12px; font-weight:900; letter-spacing:.04em; }
.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:.06em; line-height:1; }
.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 .step-marker { border-color:rgba(84,230,221,.85); color:#062b37; background:#6ef0e1; box-shadow:0 0 12px rgba(79,231,218,.35); }
.step-connector { position:relative; flex:0 1 40px; min-width:18px; height:2px; margin:0 8px; overflow:visible; background:rgba(98,169,201,.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,.46); }
.step-tab.complete + .step-connector::after { border-color:#63e6f1; }
@media (max-width:900px) { .step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; } .step-tab { flex:0 0 auto; width:100%; } .step-connector { flex:0 0 20px; min-width:0; width:2px; height:20px; margin-left:29px; } }
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -61,9 +61,7 @@
<script setup>
import { getAiTrainingEvaluation, requestAiTrainingAssessment, requestAiTrainingHelp } from "@/api/aiTrainingEvaluation";
import reportNormalizer from "./trainingAiEvaluationReport.cjs";
const { normalizeEvaluationReports, normalizeOperation } = reportNormalizer;
import { normalizeEvaluationReports, normalizeOperation } from "./trainingAiEvaluationReport";
const props = defineProps({
taskKey: { type: String, default: "" },

@ -0,0 +1,62 @@
function safeParseJson(value) {
if (typeof value !== "string" || !value.trim()) return null;
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
} catch (error) {
return null;
}
}
function formatReport(value) {
if (value == null) return "";
const parsed = typeof value === "string" ? safeParseJson(value) : value;
if (parsed == null) return typeof value === "string" ? value : "";
try {
return JSON.stringify(parsed, null, 2);
} catch (error) {
return String(value);
}
}
function normalizeCriteria(value) {
if (!Array.isArray(value)) return [];
return value.map((item) => {
if (typeof item === "string") return { name: item, score: "", description: "" };
return {
name: String(item?.name || item?.criterion || item?.title || "评分项"),
score: item?.score ?? item?.weight ?? "",
description: String(item?.description || item?.detail || ""),
};
});
}
function normalizeOperation(operation, root, type) {
const value = operation && typeof operation === "object" ? operation : {};
const prefix = type === "help" ? "help" : "assessment";
const flatReportKey = type === "help" ? "helpReportJson" : "assessmentReportJson";
return {
...value,
status: String(value.status || root?.[`${prefix}Status`] || "PENDING").toUpperCase(),
report: value.report || root?.[flatReportKey] || root?.[`${prefix}Report`] || "",
scoreCriteria: value.scoreCriteria || root?.scoreCriteria || [],
};
}
function normalizeEvaluationReports(evaluation) {
const root = evaluation && typeof evaluation === "object" ? evaluation : {};
const help = normalizeOperation(root.help || root.helpEvaluation, root, "help");
const assessment = normalizeOperation(root.assessment || root.assessmentEvaluation, root, "assessment");
const assessmentReportData = safeParseJson(root.assessmentReportJson);
return {
help,
assessment,
helpReport: formatReport(help.report || help.content || help.suggestion),
assessmentReport: formatReport(assessment.report || assessment.content || assessment.feedback),
score: root.assessmentScore ?? assessment.score ?? assessmentReportData?.score ?? root.score ?? null,
scoreCriteria: normalizeCriteria(assessmentReportData?.criteria || assessment.scoreCriteria || root.scoreCriteria),
};
}
export { formatReport, normalizeCriteria, normalizeEvaluationReports, normalizeOperation, safeParseJson };

@ -14,12 +14,7 @@
/>
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
@ -33,12 +28,17 @@
<template v-for="(step, index) in flowSteps" :key="step">
<button
type="button"
class="step-tab"
:class="{ active: form.activeStep === index }"
:class="['step-tab', { active: form.activeStep === index, complete: form.activeStep > index }]"
:aria-label="`第${index + 1}步:${step}`"
@click="setActiveStep(index)"
>
{{ step }}
<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">{{ form.activeStep === index ? "当前步骤" : "点击切换" }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="index < flowSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
@ -51,6 +51,12 @@
/>
<section class="step-card consumer-behavior-step-card">
<div v-if="activeStepName" class="step-title-row">
<div>
<p class="section-eyebrow">Step {{ String(form.activeStep + 1).padStart(2, '0') }}</p>
<h3>{{ activeStepName }}</h3>
</div>
</div>
<section v-if="form.activeStep === 0" class="step-panel">
<div class="questionnaire-head">
<p class="task-desc questionnaire-desc">
@ -81,10 +87,6 @@
</section>
<section v-else-if="form.activeStep === 1" class="step-panel">
<div class="panel-title">
<span>Step 02</span>
<h4>{{ flowSteps[1] }}</h4>
</div>
<p class="task-desc">假如本次调研共回收有效问卷 500 份,以下是各题目的数据统计结果:</p>
<div class="data-board">
<pre>{{ surveyStats }}</pre>
@ -103,10 +105,6 @@
</section>
<section v-else-if="form.activeStep === 2" class="step-panel">
<div class="panel-title">
<span>Step 03</span>
<h4>{{ flowSteps[2] }}</h4>
</div>
<p class="task-desc">
请根据上一步骤整理的Excel文件,利用Excel进行数据可视化,上传Excel可视化操作截图。
</p>
@ -131,10 +129,6 @@
</section>
<section v-else class="step-panel">
<div class="panel-title">
<span>Step 04</span>
<h4>{{ flowSteps[3] }}</h4>
</div>
<p class="task-desc">
请根据问卷设计、数据收集与数据可视化结果,整理形成最终分析报告。
</p>
@ -142,16 +136,21 @@
</section>
<div class="step-actions">
<el-button class="nav-action" :disabled="form.activeStep === 0" @click="goPrev">上一步</el-button>
<el-button class="nav-action nav-action--previous" :disabled="form.activeStep === 0" @click="goPrev">
<el-icon><ArrowLeft /></el-icon>
上一步
</el-button>
<el-button class="nav-action nav-action--save" :loading="saving" @click="saveCurrentProgress">
<el-icon><CircleCheck /></el-icon>
保存当前进度
</el-button>
<el-button class="nav-action nav-action--next" :disabled="form.activeStep === flowSteps.length - 1" @click="goNext">
下一步
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--save" :loading="saving" @click="saveCurrentProgress">
保存当前进度
</el-button>
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
</el-button>
@ -175,7 +174,7 @@
<script setup>
import { computed, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref } from "vue";
import { Document, Download, Upload } from "@element-plus/icons-vue";
import { ArrowLeft, ArrowRight, CircleCheck, Document, Download, Upload } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
@ -398,6 +397,7 @@ const taskBackground = computed(() => taskConfig.value?.background || defaultBac
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals));
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement);
const flowSteps = computed(() => parseArray(taskConfig.value?.steps, []).slice(0, 4));
const activeStepName = computed(() => flowSteps.value[form.value.activeStep] || "");
const progressItems = computed(() => [
{ text: form.value.questionnaireFileName ? "已完成:问卷上传" : "待完成:问卷设计与上传", status: form.value.questionnaireFileName ? "done" : "" },
@ -654,9 +654,9 @@ function exportOutcome() {
max-width: 100%;
min-height: calc(100vh - 106px);
margin: 0;
padding: 0 0 32px;
padding: 24px 24px 32px;
color: #e8f7ff;
background: transparent;
background: #06111d;
}
.training-core {
@ -687,10 +687,7 @@ function exportOutcome() {
.task-title {
margin: 0;
color: transparent;
background: linear-gradient(135deg, #ffffff, #8bcbff);
-webkit-background-clip: text;
background-clip: text;
color: #ffffff;
font-size: 30px;
line-height: 1.3;
font-weight: 900;
@ -762,9 +759,9 @@ function exportOutcome() {
.workbench-head h3 {
margin: 4px 0 8px;
color: #ffffff;
font-size: 26px !important;
font-weight: 800 !important;
line-height: 1.3 !important;
font-size: 26px;
font-weight: 800;
line-height: 1.3;
}
.workbench-actions {
@ -772,6 +769,7 @@ function exportOutcome() {
flex: 0 0 auto;
flex-wrap: wrap;
justify-content: flex-end;
margin-left: auto;
gap: 10px;
}
@ -804,41 +802,155 @@ function exportOutcome() {
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-bottom: 32px;
padding: 6px;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
scrollbar-width: thin;
}
.step-tab {
display: inline-flex;
flex: 1 1 0;
display: flex;
flex: 1 0 145px;
align-items: center;
justify-content: center;
gap: 8px;
min-width: 0;
min-height: 46px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 40px;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: transparent;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font-family: inherit;
font-size: 14px;
font-weight: 800;
text-align: center;
transition: color 0.2s, background 0.2s, border-color 0.2s, box-shadow 0.2s;
text-align: left;
cursor: pointer;
transition: transform 0.2s ease, color 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
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);
.step-action {
color: #9bf4ff;
}
}
.step-tab.active {
border-color: rgba(0, 170, 255, 0.55);
&.active {
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
.step-marker {
border-color: #72e8ff;
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
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);
}
}
&.complete {
color: #dffbff;
.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-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-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);
&::after {
border-color: #63e6f1;
}
}
.step-panel {
@ -849,6 +961,21 @@ function exportOutcome() {
margin-bottom: 0;
}
.step-title-row {
display: flex;
justify-content: space-between;
gap: 18px;
margin-bottom: 14px;
h3 {
margin: 4px 0 0;
color: #ffffff;
font-size: 22px;
font-weight: 900;
line-height: 1.3;
}
}
.panel-title {
padding-bottom: 14px;
margin-bottom: 18px;
@ -986,7 +1113,7 @@ function exportOutcome() {
border-radius: 14px;
outline: none;
resize: vertical;
color: #f4fbff;
color: #eef5ff;
background: #0f1a24;
font-size: 14px;
line-height: 1.7;
@ -1017,6 +1144,12 @@ function exportOutcome() {
.upload-action,
.mini-upload {
min-height: 40px;
padding: 0 18px;
border: 1px solid rgba(92, 224, 255, 0.7);
border-radius: 999px;
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
font-weight: 800;
}
@ -1029,9 +1162,13 @@ function exportOutcome() {
border: 1px solid rgba(95, 232, 255, 0.38);
border-radius: 999px;
color: #9fe8ff;
background: rgba(10, 68, 95, 0.42);
background: rgba(1, 14, 26, 0.72);
font-size: 13px;
font-weight: 700;
span {
overflow-wrap: anywhere;
}
}
.data-board {
@ -1045,7 +1182,7 @@ function exportOutcome() {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
color: #d3e5f3;
color: #d7ecff;
font-family: inherit;
font-size: 14px;
line-height: 1.78;
@ -1097,7 +1234,7 @@ function exportOutcome() {
border-radius: 14px;
outline: none;
resize: vertical;
color: #f4fbff;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 14px;
@ -1149,13 +1286,14 @@ function exportOutcome() {
.step-actions {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
margin-top: 28px;
margin-top: 22px;
margin-bottom: 36px;
}
.nav-action {
min-width: 112px;
min-height: 40px;
padding: 0 18px;
border-color: rgba(99, 217, 255, 0.55);
@ -1167,8 +1305,19 @@ function exportOutcome() {
}
.nav-action--next {
color: #fff;
background: linear-gradient(180deg, rgba(25, 143, 192, 0.98), rgba(11, 108, 156, 0.98));
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.nav-action--save {
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.nav-action.is-disabled {
opacity: 0.45;
}
.training-actions {
@ -1227,13 +1376,20 @@ function exportOutcome() {
}
@media (max-width: 900px) {
.consumer-behavior-page {
padding: 14px 14px 18px;
}
.training-core {
padding: 18px;
border-radius: 24px;
}
.workbench-head,
.workbench-actions {
.workbench-actions,
.step-title-row,
.step-actions,
.training-actions {
flex-direction: column;
align-items: stretch;
}
@ -1244,35 +1400,42 @@ function exportOutcome() {
}
.step-indicator {
overflow-x: auto;
align-items: stretch;
flex-direction: column;
width: 100%;
margin-right: 0;
margin-left: 0;
overflow: visible;
border-radius: 20px;
}
.step-tab {
min-width: 132px;
flex: 0 0 auto;
width: 100%;
}
.step-connector {
flex: 0 0 22px;
width: 2px;
height: 22px;
margin-left: 29px;
text-align: center;
writing-mode: vertical-rl;
}
@media (max-width: 760px) {
.consumer-behavior-page {
padding-bottom: 18px;
.step-actions :deep(.nav-action),
.training-actions :deep(.training-action) {
width: 100%;
}
}
@media (max-width: 760px) {
.training-core,
.task-card,
.step-card {
padding: 18px;
}
.step-actions,
.training-actions {
align-items: stretch;
flex-direction: column;
}
.step-tab {
width: 100%;
}
.questionnaire-head {
flex-direction: column;
}
@ -1293,5 +1456,11 @@ function exportOutcome() {
grid-template-columns: 1fr;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -14,12 +14,7 @@
/>
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
@ -30,24 +25,29 @@
</div>
<nav v-if="trainingSteps.length" class="step-indicator" aria-label="消费场景解构步骤">
<template v-for="(step, index) in trainingSteps" :key="`${index}-${step}`">
<button
v-for="(step, index) in trainingSteps"
:key="`${index}-${step}`"
type="button"
class="step-tab"
:class="{ active: form.activeStep === index + 1 }"
:class="['step-tab', { active: form.activeStep === index + 1, complete: form.activeStep > index + 1 }]"
:aria-current="form.activeStep === index + 1 ? 'step' : undefined"
:aria-label="`第${index + 1}步:${step}`"
@click="setCurrentStep(index + 1)"
>
{{ step }}
<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">{{ form.activeStep === 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 consumer-scenario-step-card">
<div v-if="activeStepName" class="step-title-row">
<div>
<p class="section-eyebrow">Step {{ String(form.activeStep).padStart(2, "0") }}</p>
<h4>{{ activeStepName }}</h4>
<h3>{{ activeStepName }}</h3>
</div>
</div>
@ -161,16 +161,21 @@
</section>
<div v-if="hasConfiguredSteps" class="step-actions">
<button type="button" class="nav-action" :disabled="form.activeStep === 1" @click="goPreviousStep">上一步</button>
<button type="button" class="nav-action nav-action--previous" :disabled="form.activeStep === 1" @click="goPreviousStep">
<el-icon><ArrowLeft /></el-icon>
上一步
</button>
<button type="button" class="nav-action nav-action--save" :disabled="saving" @click="saveCurrentProgress">
<el-icon><CircleCheck /></el-icon>
保存当前进度
</button>
<button type="button" class="nav-action nav-action--next" :disabled="form.activeStep >= trainingSteps.length" @click="goNextStep">
下一步
<el-icon><ArrowRight /></el-icon>
</button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--save" :loading="saving" @click="saveCurrentProgress">
保存当前进度
</el-button>
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
</el-button>
@ -194,7 +199,7 @@
<script setup>
import { computed, getCurrentInstance, onMounted, ref } from "vue";
import { Download } from "@element-plus/icons-vue";
import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
@ -524,9 +529,9 @@ function exportOutcome() {
max-width: 100%;
min-height: calc(100vh - 106px);
margin: 0;
padding: 0 0 32px;
padding: 24px 24px 32px;
color: #e8f7ff;
background: transparent;
background: #06111d;
}
.training-core {
@ -557,10 +562,7 @@ function exportOutcome() {
.task-title {
margin: 0;
color: transparent;
background: linear-gradient(135deg, #ffffff, #8bcbff);
-webkit-background-clip: text;
background-clip: text;
color: #ffffff;
font-size: 30px;
line-height: 1.3;
font-weight: 900;
@ -632,9 +634,9 @@ function exportOutcome() {
.workbench-head h3 {
margin: 4px 0 8px;
color: #ffffff;
font-size: 26px !important;
font-weight: 800 !important;
line-height: 1.3 !important;
font-size: 26px;
font-weight: 800;
line-height: 1.3;
}
.workbench-actions {
@ -642,6 +644,7 @@ function exportOutcome() {
flex: 0 0 auto;
flex-wrap: wrap;
justify-content: flex-end;
margin-left: auto;
gap: 10px;
}
@ -674,39 +677,52 @@ function exportOutcome() {
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-bottom: 32px;
padding: 6px;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
scrollbar-width: thin;
}
.step-tab {
display: inline-flex;
flex: 1 1 0;
display: flex;
flex: 1 0 145px;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 40px;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: transparent;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font-family: inherit;
font-size: 14px;
font-weight: 800;
text-align: center;
text-align: left;
cursor: pointer;
transition: color 0.2s, background 0.2s, border-color 0.2s, box-shadow 0.2s;
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: rgba(99, 217, 255, 0.45);
color: #e9fbff;
background: rgba(17, 126, 178, 0.28);
transform: translateY(-2px);
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);
.step-action {
color: #9bf4ff;
}
}
&:focus-visible {
@ -715,10 +731,105 @@ function exportOutcome() {
}
&.active {
border-color: rgba(0, 170, 255, 0.55);
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
.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);
}
}
&.complete {
color: #dffbff;
.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-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-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);
&::after {
border-color: #63e6f1;
}
}
@ -735,7 +846,7 @@ function exportOutcome() {
padding-bottom: 14px;
border-bottom: 1px solid rgba(0, 180, 255, 0.2);
h4 {
h3 {
margin: 4px 0 0;
color: #ffffff;
font-size: 22px;
@ -845,6 +956,12 @@ function exportOutcome() {
color: #eef5ff;
background: #0f1a24;
font: inherit;
&:focus {
border-color: #5fe7ff;
background: rgba(10, 31, 46, 0.98);
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
.journey-table-wrap {
@ -868,6 +985,7 @@ function exportOutcome() {
.journey-table thead th {
height: 46px;
padding: 12px;
color: #eaffff;
background: rgba(8, 55, 82, 0.98);
font-weight: 900;
@ -875,6 +993,7 @@ function exportOutcome() {
.journey-table tbody th {
width: 120px;
padding: 12px;
color: #eafaff;
background: rgba(1, 14, 26, 0.72);
font-weight: 900;
@ -886,16 +1005,17 @@ function exportOutcome() {
.journey-table td {
height: 108px;
padding: 0;
padding: 12px;
background: rgba(1, 14, 26, 0.48);
}
.table-input {
display: block;
width: 100%;
height: 108px;
min-height: 110px;
padding: 12px 14px;
border: 0;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
resize: none;
color: #eef5ff;
@ -910,6 +1030,12 @@ function exportOutcome() {
color: #8299aa;
}
.table-input:focus {
border-color: #5fe7ff;
background: rgba(10, 31, 46, 0.98);
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
.reference-map {
margin-top: 28px;
padding: 16px;
@ -920,9 +1046,11 @@ function exportOutcome() {
.step-actions {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
margin-top: 28px;
margin-top: 22px;
margin-bottom: 36px;
}
.nav-action {
@ -950,6 +1078,12 @@ function exportOutcome() {
background: linear-gradient(95deg, #0b84bd, #096491);
}
.nav-action--save {
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.reference-title {
margin-bottom: 14px;
color: #dff5ff;
@ -1101,7 +1235,7 @@ function exportOutcome() {
background: rgba(18, 85, 116, 0.78);
}
@media (max-width: 1180px) {
@media (max-width: 1100px) {
.consumer-scenario-page {
grid-template-columns: 1fr;
}
@ -1109,7 +1243,7 @@ function exportOutcome() {
@media (max-width: 900px) {
.consumer-scenario-page {
padding-bottom: 18px;
padding: 14px 14px 18px;
}
.training-core {
@ -1118,7 +1252,10 @@ function exportOutcome() {
}
.workbench-head,
.workbench-actions {
.workbench-actions,
.step-title-row,
.step-actions,
.training-actions {
flex-direction: column;
align-items: stretch;
}
@ -1129,11 +1266,28 @@ function exportOutcome() {
}
.step-indicator {
overflow-x: auto;
align-items: stretch;
flex-direction: column;
width: 100%;
margin-right: 0;
margin-left: 0;
overflow: visible;
border-radius: 20px;
}
.step-tab {
min-width: 132px;
flex: 0 0 auto;
width: 100%;
}
.step-connector {
flex: 0 0 20px;
min-width: 0;
width: 2px;
height: 20px;
margin-left: 29px;
text-align: center;
writing-mode: vertical-rl;
}
.journey-flow,
@ -1141,12 +1295,8 @@ function exportOutcome() {
grid-template-columns: 1fr;
}
.step-actions {
flex-direction: column;
align-items: stretch;
}
.nav-action {
.nav-action,
.training-action {
width: 100%;
}
@ -1161,15 +1311,12 @@ function exportOutcome() {
grid-template-columns: 1fr;
}
.training-actions {
flex-direction: column;
align-items: stretch;
gap: 14px;
}
.training-action {
width: 100%;
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -14,12 +14,7 @@
/>
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
@ -30,14 +25,22 @@
</div>
<nav v-if="trainingSteps.length" class="step-indicator" aria-label="竞争环境分析步骤">
<span
v-for="(step, index) in trainingSteps"
:key="`${index}-${step}`"
class="step-tab"
:class="{ active: currentStep === index + 1 }"
<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)"
>
{{ step }}
<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 competitive-step-card">
@ -94,8 +97,22 @@
<textarea v-model="form.competitionConclusion" 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--save" :loading="saving" @click="saveCurrentProgress">保存当前进度</el-button>
<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>
@ -116,7 +133,7 @@ import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vu
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { Download } from "@element-plus/icons-vue";
import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue";
const TASK_KEY = "competitive-environment-analysis";
const { proxy } = getCurrentInstance();
@ -212,6 +229,12 @@ const taskRequirement = computed(() => taskConfig.value?.requirements || default
const trainingSteps = computed(() => parseArray(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: "进行中:SWOT四象限归类", status: "doing" },
{ text: form.value.entryStrategy ? "已填写:市场切入策略" : "待完成:市场切入策略", status: form.value.entryStrategy ? "done" : "" },
@ -302,14 +325,18 @@ function buildAnswerPayload(saveAction = "SAVE") {
};
}
async function saveAnswer(saveAction = "SAVE") {
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;
}
@ -319,6 +346,23 @@ 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");
}
@ -353,9 +397,9 @@ function exportOutcome() {
max-width: 100%;
min-height: calc(100vh - 106px);
margin: 0;
padding: 0 0 32px;
padding: 24px 24px 32px;
color: #e8f7ff;
background: transparent;
background: #06111d;
}
.training-core {
@ -447,9 +491,9 @@ function exportOutcome() {
h3 {
margin: 4px 0 8px;
color: #ffffff;
font-size: 26px !important;
font-weight: 800 !important;
line-height: 1.3 !important;
font-size: 26px;
font-weight: 800;
line-height: 1.3;
}
}
@ -457,6 +501,7 @@ function exportOutcome() {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
margin-left: auto;
justify-content: flex-end;
gap: 10px;
}
@ -498,38 +543,157 @@ function exportOutcome() {
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-bottom: 32px;
padding: 6px;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
overflow-x: auto;
}
.step-tab {
display: inline-flex;
flex: 1 1 0;
display: flex;
flex: 1 0 145px;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 40px;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: transparent;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font-family: inherit;
font-size: 14px;
font-weight: 800;
text-align: center;
transition: color 0.2s, background 0.2s, border-color 0.2s, box-shadow 0.2s;
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: 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-action {
color: #9bf4ff;
}
}
&:focus-visible {
outline: 2px solid #72e8ff;
outline-offset: 4px;
}
&.active {
border-color: rgba(0, 170, 255, 0.55);
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
.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);
}
}
&.complete {
color: #dffbff;
.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-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;
gap: 4px;
min-width: 0;
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;
}
.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);
&::after {
border-color: #63e6f1;
}
}
@ -635,7 +799,7 @@ function exportOutcome() {
.swot-input {
min-height: 128px;
padding: 12px 14px;
border-radius: 16px;
border-radius: 14px;
}
.field-block {
@ -654,7 +818,7 @@ function exportOutcome() {
.long-textarea {
min-height: 170px;
padding: 14px 16px;
border-radius: 18px;
border-radius: 14px;
}
.competitor-block {
@ -669,10 +833,10 @@ function exportOutcome() {
}
.competitor-table {
overflow: hidden;
border: 1px solid rgba(82, 174, 226, 0.48);
border-radius: 18px;
background: rgba(0, 13, 24, 0.68);
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 22px;
background: rgba(1, 14, 26, 0.72);
}
.competitor-row {
@ -686,8 +850,8 @@ function exportOutcome() {
justify-content: center;
min-height: 58px;
padding: 12px;
border-right: 1px solid rgba(87, 159, 200, 0.28);
color: #d7ecff;
border-right: 1px solid rgba(77, 167, 220, 0.2);
color: #d7f1ff;
line-height: 1.65;
text-align: center;
}
@ -699,10 +863,10 @@ function exportOutcome() {
.competitor-head {
border-top: 0;
background: linear-gradient(180deg, #0b7fb3, #075c88);
background: rgba(8, 55, 82, 0.98);
div {
color: #effdff;
color: #eaffff;
font-weight: 900;
}
}
@ -758,25 +922,69 @@ function exportOutcome() {
background: rgba(18, 85, 116, 0.78);
}
.step-actions {
display: flex;
justify-content: center;
gap: 14px;
margin-top: 22px;
}
.step-actions :deep(.step-action-btn) {
min-height: 40px;
padding: 0 18px;
border-radius: 999px;
font-size: 14px;
font-weight: 800;
}
.step-actions :deep(.step-action-btn--previous) {
border-color: rgba(99, 217, 255, 0.55);
color: #e9fbff;
background: rgba(17, 126, 178, 0.36);
}
.step-actions :deep(.step-action-btn--save),
.step-actions :deep(.step-action-btn--next) {
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.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) {
.competitive-page {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.swot-grid {
grid-template-columns: 1fr;
margin: 8px 0 36px;
margin: 20px 0;
}
.competitive-page {
padding: 14px 14px 18px;
}
@media (max-width: 900px) {
.training-core {
padding: 18px;
border-radius: 24px;
}
.workbench-head,
.workbench-actions {
.workbench-actions,
.step-title-row,
.step-actions,
.training-actions {
flex-direction: column;
align-items: stretch;
}
@ -785,6 +993,49 @@ function exportOutcome() {
.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 {
gap: 14px;
}
.step-actions :deep(.step-action-btn),
.training-actions :deep(.training-action) {
width: 100%;
}
}
@media (max-width: 760px) {
@ -798,4 +1049,10 @@ function exportOutcome() {
overflow-x: auto;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -14,12 +14,7 @@
/>
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
@ -30,17 +25,22 @@
</div>
<nav v-if="flowSteps.length" class="step-indicator" aria-label="需求转化为功能步骤">
<template v-for="(step, index) in flowSteps" :key="`${step}-${index}`">
<button
v-for="(step, index) in flowSteps"
:key="`${step}-${index}`"
type="button"
class="step-tab"
:class="{ active: form.activeStep === index + 1 }"
:class="['step-tab', { active: form.activeStep === index + 1, complete: form.activeStep > index + 1 }]"
:aria-current="form.activeStep === index + 1 ? 'step' : undefined"
:aria-label="`第${index + 1}步:${step}`"
@click="setCurrentStep(index + 1)"
>
{{ step }}
<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">{{ form.activeStep === index + 1 ? "当前步骤" : "点击切换" }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="index < flowSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section v-show="isSectionVisible(1)" class="step-card conversion-content">
@ -1047,7 +1047,7 @@ function exportOutcome() {
background: rgba(18, 85, 116, 0.78);
}
@media (max-width: 1180px) {
@media (max-width: 1100px) {
.feature-conversion-page {
grid-template-columns: 1fr;
}
@ -1177,6 +1177,7 @@ function exportOutcome() {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
margin-left: auto;
gap: 10px;
flex-shrink: 0;
}
@ -1209,43 +1210,157 @@ function exportOutcome() {
}
.step-indicator {
position: relative;
display: flex;
gap: 6px;
min-height: 58px;
margin: 0;
padding: 6px;
align-items: center;
gap: 0;
width: calc(100% + 24px);
min-height: 0;
margin: 0 -12px 32px;
padding: 18px 22px;
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 999px;
background: rgba(0, 20, 35, 0.65);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
scrollbar-width: thin;
}
.step-tab {
flex: 1 0 max-content;
min-height: 46px;
padding: 0 18px;
border: 1px solid transparent;
border-radius: 999px;
display: flex;
flex: 1 0 145px;
align-items: center;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: transparent;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font: inherit;
font-size: 14px;
font-weight: 800;
cursor: pointer;
transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
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;
}
.step-tab:hover {
transform: translateY(-2px);
color: #ffffff;
border-color: rgba(89, 223, 255, 0.82);
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);
}
.step-tab:hover .step-action {
color: #9bf4ff;
}
.step-tab:hover,
.step-tab:focus-visible {
color: #e9fbff;
border-color: rgba(99, 217, 255, 0.55);
outline: none;
outline: 2px solid rgba(92, 224, 255, 0.7);
outline-offset: 2px;
}
.step-tab.active {
border-color: rgba(92, 224, 255, 0.7);
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
}
.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.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 .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);
&::after {
border-color: #63e6f1;
}
}
.step-card {
@ -1472,7 +1587,7 @@ function exportOutcome() {
margin-top: 36px;
}
@media (max-width: 1180px) {
@media (max-width: 1100px) {
.feature-conversion-page {
grid-template-columns: 1fr;
}
@ -1503,18 +1618,40 @@ function exportOutcome() {
}
.step-indicator {
border-radius: 26px;
align-items: stretch;
flex-direction: column;
width: 100%;
margin-right: 0;
margin-left: 0;
overflow: visible;
border-radius: 20px;
}
.step-tab {
flex: none;
flex: 0 0 auto;
width: 100%;
}
.step-connector {
flex: 0 0 20px;
min-width: 0;
width: 2px;
height: 20px;
margin-left: 29px;
text-align: center;
writing-mode: vertical-rl;
}
.step-navigation {
flex-direction: column;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -14,12 +14,7 @@
/>
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
@ -30,14 +25,22 @@
</div>
<nav v-if="trainingSteps.length" class="step-indicator" aria-label="行业需求分析步骤">
<span
v-for="(step, index) in trainingSteps"
:key="`${index}-${step}`"
class="step-tab"
:class="{ active: currentStep === index + 1 }"
<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)"
>
{{ step }}
<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">
@ -88,8 +91,22 @@
<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--save" :loading="saving" @click="saveCurrentProgress">保存当前进度</el-button>
<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>
@ -110,7 +127,7 @@ import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vu
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { Download } from "@element-plus/icons-vue";
import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue";
const TASK_KEY = "industry-demand-analysis";
const { proxy } = getCurrentInstance();
@ -208,6 +225,12 @@ const taskRequirement = computed(() => taskConfig.value?.requirements || default
const trainingSteps = computed(() => parseArray(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" },
@ -315,14 +338,18 @@ function buildAnswerPayload(saveAction = "SAVE") {
};
}
async function saveAnswer(saveAction = "SAVE") {
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;
}
@ -332,6 +359,23 @@ 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");
}
@ -366,9 +410,9 @@ function exportOutcome() {
max-width: 100%;
min-height: calc(100vh - 106px);
margin: 0;
padding: 0 0 32px;
padding: 24px 24px 32px;
color: #e8f7ff;
background: transparent;
background: #06111d;
}
.training-core {
@ -460,9 +504,9 @@ function exportOutcome() {
h3 {
margin: 4px 0 8px;
color: #ffffff;
font-size: 26px !important;
font-weight: 800 !important;
line-height: 1.3 !important;
font-size: 26px;
font-weight: 800;
line-height: 1.3;
}
}
@ -470,6 +514,7 @@ function exportOutcome() {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
margin-left: auto;
justify-content: flex-end;
gap: 10px;
}
@ -511,38 +556,159 @@ function exportOutcome() {
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-bottom: 32px;
padding: 6px;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
overflow-x: auto;
scrollbar-width: thin;
}
.step-tab {
display: inline-flex;
flex: 1 1 0;
display: flex;
flex: 1 0 145px;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 40px;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: transparent;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font-family: inherit;
font-size: 14px;
font-weight: 800;
text-align: center;
transition: color 0.2s, background 0.2s, border-color 0.2s, box-shadow 0.2s;
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: 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-action {
color: #9bf4ff;
}
}
&:focus-visible {
outline: 2px solid #72e8ff;
outline-offset: 4px;
}
&.active {
border-color: rgba(0, 170, 255, 0.55);
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
.step-marker {
border-color: #72e8ff;
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
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);
}
}
&.complete {
color: #dffbff;
.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-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;
gap: 4px;
min-width: 0;
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-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);
&::after {
border-color: #63e6f1;
}
}
@ -648,7 +814,7 @@ function exportOutcome() {
.pest-input {
min-height: 128px;
padding: 12px 14px;
border-radius: 16px;
border-radius: 14px;
}
.field-block {
@ -667,7 +833,7 @@ function exportOutcome() {
.long-textarea {
min-height: 150px;
padding: 14px 16px;
border-radius: 18px;
border-radius: 14px;
}
.comparison-block {
@ -779,25 +945,67 @@ function exportOutcome() {
background: rgba(18, 85, 116, 0.78);
}
.step-actions {
display: flex;
justify-content: center;
gap: 14px;
margin-top: 22px;
}
.step-actions :deep(.step-action-btn) {
min-height: 40px;
padding: 0 18px;
border-radius: 999px;
font-size: 14px;
font-weight: 800;
}
.step-actions :deep(.step-action-btn--previous) {
border-color: rgba(99, 217, 255, 0.55);
color: #e9fbff;
background: rgba(17, 126, 178, 0.36);
}
.step-actions :deep(.step-action-btn--save),
.step-actions :deep(.step-action-btn--next) {
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.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: 8px 0 36px;
margin: 20px 0;
}
.industry-demand-page {
padding: 14px 14px 18px;
}
@media (max-width: 720px) {
.training-core {
padding: 18px;
border-radius: 24px;
}
.workbench-head,
.workbench-actions {
.workbench-actions,
.step-title-row {
flex-direction: column;
align-items: stretch;
}
@ -807,8 +1015,59 @@ function exportOutcome() {
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>

@ -14,12 +14,7 @@
/>
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
@ -30,19 +25,30 @@
</div>
<nav v-if="steps.length" class="step-indicator" aria-label="目标用户画像步骤">
<template v-for="(step, index) in steps" :key="step.no">
<button
v-for="step in steps"
:key="step.no"
type="button"
:class="['step-tab', { active: currentStep === step.no }]"
:class="['step-tab', { active: currentStep === step.no, complete: currentStep > step.no }]"
:aria-label="`第${step.no}步:${step.name}`"
@click="currentStep = step.no"
>
<el-icon><component :is="step.icon" /></el-icon>
<span>{{ step.name }}</span>
<span class="step-marker">{{ String(step.no).padStart(2, '0') }}</span>
<span class="step-copy">
<strong class="step-name" :title="step.name">{{ step.name }}</strong>
<span class="step-action">{{ currentStep === step.no ? "当前步骤" : "点击切换" }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="index < steps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section class="step-card target-profile-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>
<section v-show="currentStep === 1" class="step-panel">
<p class="task-desc">
通过电商数据平台,搜索“智能吸顶灯”、助眠产品等产品的客户群画像。如没有电商数据平台账号,可通过百度指数进行搜索,并将搜索结果截图,进行分析。
@ -199,10 +205,22 @@
</div>
</section>
<div class="training-actions">
<el-button class="training-action training-action--save" :loading="saving" @click="saveCurrentProgress">
<div v-if="steps.length" class="step-actions">
<el-button class="step-action step-action--previous" :disabled="currentStep <= 1" @click="currentStep -= 1">
<el-icon><ArrowLeft /></el-icon>
上一步
</el-button>
<el-button class="step-action step-action--save" :loading="saving" @click="saveCurrentProgress">
<el-icon><CircleCheck /></el-icon>
保存当前进度
</el-button>
<el-button class="step-action step-action--next" :disabled="currentStep >= steps.length" @click="currentStep += 1">
下一步
<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>
@ -225,7 +243,7 @@
</template>
<script setup>
import { Document, Download, Operation, Search, TrendCharts, Upload } from "@element-plus/icons-vue";
import { ArrowLeft, ArrowRight, CircleCheck, Document, Download, Upload } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
@ -253,8 +271,6 @@ const defaultGoals = [
"能够形成面向产品定位和后续选品判断的用户画像结论。",
];
const stepIcons = [Search, Document, TrendCharts, Operation];
const interviewTranscripts = [
{
code: "U01",
@ -407,9 +423,9 @@ const steps = computed(() =>
flowSteps.value.map((name, index) => ({
no: index + 1,
name,
icon: stepIcons[index],
}))
);
const activeStepName = computed(() => steps.value.find((step) => step.no === currentStep.value)?.name || "");
const progressItems = computed(() => [
...steps.value.map((step) => ({
@ -630,9 +646,9 @@ function exportOutcome() {
max-width: 100%;
min-height: calc(100vh - 106px);
margin: 0;
padding: 0 0 32px;
padding: 24px 24px 32px;
color: #e8f7ff;
background: transparent;
background: #06111d;
}
.training-core {
@ -662,10 +678,7 @@ function exportOutcome() {
.task-title {
margin: 0;
color: transparent;
background: linear-gradient(135deg, #ffffff, #8bcbff);
-webkit-background-clip: text;
background-clip: text;
color: #ffffff;
font-size: 30px;
line-height: 1.3;
font-weight: 900;
@ -724,9 +737,9 @@ function exportOutcome() {
h3 {
margin: 4px 0 8px;
color: #ffffff;
font-size: 26px !important;
font-weight: 800 !important;
line-height: 1.3 !important;
font-size: 26px;
font-weight: 800;
line-height: 1.3;
}
}
@ -735,6 +748,7 @@ function exportOutcome() {
flex: 0 0 auto;
flex-wrap: wrap;
justify-content: flex-end;
margin-left: auto;
gap: 10px;
}
@ -779,108 +793,171 @@ function exportOutcome() {
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-bottom: 32px;
padding: 6px;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
overflow-x: auto;
padding: 18px 22px;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
scrollbar-width: thin;
}
.step-tab {
display: inline-flex;
flex: 1 1 0;
display: flex;
flex: 1 0 145px;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 46px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 40px;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: transparent;
font-size: 15px;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font-family: inherit;
font-size: 14px;
font-weight: 800;
line-height: 1.2;
text-align: center;
text-align: left;
cursor: pointer;
transition: transform 0.2s ease, color 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
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);
.step-action {
color: #9bf4ff;
}
}
&.active {
border-color: rgba(0, 170, 255, 0.55);
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
.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);
}
}
.flow-steps {
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-bottom: 32px;
padding: 6px;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 60px;
background: rgba(0, 0, 0, 0.3);
box-shadow: none;
&.complete {
color: #dffbff;
.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);
}
}
}
.flow-pill {
.step-marker {
display: inline-flex;
flex: 1;
flex: 0 0 38px;
align-items: center;
justify-content: center;
gap: 8px;
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;
min-height: 46px;
height: auto;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 40px;
color: #9ab3d0;
background: transparent;
box-shadow: none;
font-family: inherit;
font-size: 14px;
font-weight: 800;
cursor: pointer;
transition: color 0.2s, background 0.2s, border-color 0.2s, box-shadow 0.2s;
gap: 4px;
margin-left: 10px;
.el-icon {
font-size: 16px;
}
&.active {
border-color: rgba(0, 170, 255, 0.55);
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
.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;
}
.flow-arrow {
color: #c9d7df;
font-size: 25px;
font-weight: 900;
letter-spacing: -4px;
.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-panel {
animation: fadeStep 0.18s ease;
.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);
}
@keyframes fadeStep {
from {
opacity: 0;
transform: translateY(6px);
.step-tab.complete + .step-connector {
background: linear-gradient(90deg, #6ef0e1, #1cb8e9);
box-shadow: 0 0 9px rgba(53, 218, 235, 0.46);
&::after {
border-color: #63e6f1;
}
to {
opacity: 1;
transform: translateY(0);
}
.step-title-row {
display: flex;
justify-content: space-between;
gap: 18px;
margin-bottom: 14px;
h3 {
margin: 4px 0 0;
color: #ffffff;
font-size: 22px;
font-weight: 900;
line-height: 1.3;
}
}
@ -989,31 +1066,31 @@ function exportOutcome() {
.portrait-table-wrap {
overflow-x: auto;
border: 1px solid rgba(128, 190, 224, 0.62);
border-radius: 8px;
background: rgba(5, 17, 28, 0.38);
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 22px;
background: rgba(1, 14, 26, 0.72);
}
.portrait-record {
min-width: 780px;
& + & {
border-top: 2px solid rgba(128, 190, 224, 0.62);
border-top: 1px solid rgba(77, 167, 220, 0.2);
}
}
.portrait-user {
min-height: 110px;
padding: 0;
border-bottom: 1px solid rgba(128, 190, 224, 0.62);
padding: 12px;
border-bottom: 1px solid rgba(77, 167, 220, 0.2);
label,
.portrait-example {
display: flex;
align-items: center;
min-height: 28px;
min-height: 38px;
margin: 0;
padding: 0 2px;
padding: 0;
color: #d7ecff;
font-size: 15px;
line-height: 1.6;
@ -1028,16 +1105,20 @@ function exportOutcome() {
.portrait-input {
flex: 1;
min-width: 120px;
height: 28px;
border: 0;
height: 38px;
padding: 0 10px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
color: #f4fbff;
background: transparent;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
&:focus {
background: rgba(24, 97, 132, 0.24);
border-color: #5fe7ff;
background: rgba(10, 31, 46, 0.98);
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
@ -1050,26 +1131,28 @@ function exportOutcome() {
display: flex;
align-items: center;
justify-content: center;
min-height: 58px;
border-bottom: 1px solid rgba(128, 190, 224, 0.62);
color: #d7ecff;
font-size: 16px;
font-weight: 800;
min-height: 52px;
padding: 12px;
border-bottom: 1px solid rgba(77, 167, 220, 0.2);
color: #eaffff;
background: rgba(8, 55, 82, 0.98);
font-size: 14px;
font-weight: 900;
& + & {
border-left: 1px solid rgba(128, 190, 224, 0.62);
border-left: 1px solid rgba(77, 167, 220, 0.2);
}
}
.portrait-cell {
min-height: 190px;
padding: 18px 4px;
padding: 12px;
border: 0;
border-left: 1px solid rgba(128, 190, 224, 0.62);
border-left: 1px solid rgba(77, 167, 220, 0.2);
outline: none;
resize: vertical;
color: #f4fbff;
background: transparent;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
line-height: 1.7;
@ -1079,7 +1162,8 @@ function exportOutcome() {
}
&:focus {
background: rgba(24, 97, 132, 0.24);
background: rgba(10, 31, 46, 0.98);
box-shadow: inset 0 0 0 1px #5fe7ff, 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
@ -1096,9 +1180,9 @@ function exportOutcome() {
.segmentation-table-wrap {
overflow-x: auto;
border: 1px solid rgba(128, 190, 224, 0.62);
border-radius: 8px;
background: rgba(5, 17, 28, 0.38);
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 22px;
background: rgba(1, 14, 26, 0.72);
}
.segmentation-table {
@ -1109,14 +1193,16 @@ function exportOutcome() {
th,
td {
border: 1px solid rgba(128, 190, 224, 0.62);
border: 1px solid rgba(77, 167, 220, 0.2);
}
th {
height: 58px;
color: #d7ecff;
font-size: 16px;
font-weight: 800;
height: 52px;
padding: 12px;
color: #eaffff;
background: rgba(8, 55, 82, 0.98);
font-size: 14px;
font-weight: 900;
text-align: center;
}
@ -1127,23 +1213,28 @@ function exportOutcome() {
td {
height: 160px;
padding: 0;
padding: 12px;
color: #d7f1ff;
background: rgba(1, 14, 26, 0.48);
}
}
.segmentation-market-input,
.segmentation-cell-input {
width: 100%;
height: 100%;
border: 0;
min-height: 38px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
color: #f4fbff;
background: transparent;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
&:focus {
background: rgba(24, 97, 132, 0.24);
border-color: #5fe7ff;
background: rgba(10, 31, 46, 0.98);
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
@ -1154,7 +1245,7 @@ function exportOutcome() {
.segmentation-cell-input {
min-height: 160px;
padding: 16px;
padding: 12px 14px;
resize: vertical;
line-height: 1.7;
}
@ -1187,15 +1278,15 @@ function exportOutcome() {
.core-function-input {
display: block;
width: 100%;
min-height: 76px;
min-height: 110px;
margin-bottom: 28px;
padding: 12px 14px;
border: 1px solid rgba(128, 190, 224, 0.62);
border-radius: 6px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
resize: vertical;
color: #f4fbff;
background: rgba(8, 20, 32, 0.72);
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
line-height: 1.7;
@ -1228,11 +1319,11 @@ function exportOutcome() {
width: 132px;
height: 38px;
padding: 0 10px;
border: 1px solid rgba(128, 190, 224, 0.62);
border-radius: 4px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
color: #f4fbff;
background: rgba(8, 20, 32, 0.72);
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
@ -1258,12 +1349,12 @@ function exportOutcome() {
width: 100%;
min-height: 210px;
padding: 14px;
border: 1px solid rgba(128, 190, 224, 0.62);
border-radius: 6px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
resize: vertical;
color: #f4fbff;
background: rgba(8, 20, 32, 0.72);
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
line-height: 1.7;
@ -1278,13 +1369,16 @@ function exportOutcome() {
.product-reference {
display: flex;
justify-content: center;
padding-top: 18px;
padding: 16px;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 22px;
background: rgba(1, 14, 26, 0.72);
img {
width: min(100%, 320px);
height: auto;
border-radius: 10px;
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.24);
border-radius: 14px;
box-shadow: none;
}
}
@ -1322,13 +1416,13 @@ function exportOutcome() {
padding: 8px 14px;
border: 1px solid rgba(93, 213, 255, 0.42);
border-radius: 999px;
color: #d7ecff;
background: rgba(0, 34, 53, 0.72);
color: #9fe8ff;
background: rgba(1, 14, 26, 0.72);
font-size: 13px;
font-weight: 700;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow-wrap: anywhere;
}
}
@ -1346,16 +1440,16 @@ function exportOutcome() {
.profile-textarea {
width: 100%;
min-height: 420px;
padding: 16px 18px;
border: 1px solid rgba(84, 177, 230, 0.62);
border-radius: 18px;
min-height: 110px;
padding: 12px 14px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
resize: vertical;
color: #f4fbff;
background: rgba(8, 20, 32, 0.92);
font-size: 14px;
line-height: 1.72;
color: #eef5ff;
background: #0f1a24;
font-size: 15px;
line-height: 1.7;
transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
&::placeholder {
@ -1370,6 +1464,37 @@ function exportOutcome() {
}
}
.step-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
margin-top: 22px;
margin-bottom: 36px;
}
.step-actions :deep(.step-action) {
min-height: 40px;
padding: 0 18px;
border: 1px solid rgba(99, 217, 255, 0.55);
border-radius: 999px;
color: #e9fbff;
background: rgba(17, 126, 178, 0.36);
font-size: 14px;
font-weight: 800;
}
.step-actions :deep(.step-action--save),
.step-actions :deep(.step-action--next) {
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.step-actions :deep(.step-action.is-disabled) {
opacity: 0.45;
}
.training-actions {
display: flex;
flex-wrap: wrap;
@ -1431,6 +1556,10 @@ function exportOutcome() {
}
@media (max-width: 900px) {
.target-profile-page {
padding: 14px 14px 18px;
}
.training-core {
padding: 18px;
border-radius: 24px;
@ -1438,28 +1567,52 @@ function exportOutcome() {
.workbench-head,
.workbench-actions,
.upload-panel {
.upload-panel,
.step-title-row,
.step-actions,
.training-actions {
flex-direction: column;
align-items: stretch;
}
.workbench-actions :deep(.material-download),
.workbench-actions .outline-action,
.upload-action {
.upload-action,
.step-actions :deep(.step-action),
.training-actions :deep(.training-action) {
width: 100%;
}
}
@media (max-width: 760px) {
.flow-steps {
.step-indicator {
align-items: stretch;
flex-direction: column;
width: 100%;
margin-right: 0;
margin-left: 0;
overflow: visible;
border-radius: 20px;
}
.flow-pill {
.step-tab {
flex: 0 0 auto;
width: 100%;
}
.step-connector {
flex: 0 0 22px;
width: 2px;
height: 22px;
margin-left: 29px;
text-align: center;
writing-mode: vertical-rl;
}
.product-plan-layout {
gap: 20px;
}
}
@media (max-width: 760px) {
.interview-tabs {
flex-direction: column;
gap: 12px;
@ -1490,9 +1643,12 @@ function exportOutcome() {
font-size: 17px;
}
.profile-textarea {
min-height: 300px;
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -12,12 +12,7 @@
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
@ -28,16 +23,24 @@
</div>
<nav class="step-indicator" aria-label="新产品调查与分析步骤">
<template v-for="(step, index) in steps" :key="step.no">
<button
v-for="step in steps"
:key="step.no"
type="button"
:class="['step-tab', { active: currentStep === step.no }]"
:class="['step-tab', { active: currentStep === step.no, complete: currentStep > step.no }]"
:aria-label="`切换至第 ${step.no} 步:${step.name}`"
:aria-current="currentStep === step.no ? 'step' : undefined"
@click="currentStep = step.no"
>
<el-icon><component :is="step.icon" /></el-icon>
<span>{{ step.name }}</span>
<span class="step-marker" aria-hidden="true">
{{ String(step.no).padStart(2, '0') }}
</span>
<span class="step-copy">
<strong class="step-name" :title="step.name">{{ step.name }}</strong>
<span class="step-action">{{ currentStep === step.no ? '当前步骤' : '点击切换' }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="index < steps.length - 1" class="step-connector" aria-hidden="true" />
</template>
</nav>
<section v-show="currentStep === 1" class="step-content active-step">
@ -768,6 +771,13 @@ function exportOutcome() {
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
.workbench-actions {
display: flex;
flex: 0 0 auto;
@ -822,40 +832,158 @@ function exportOutcome() {
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-bottom: 32px;
padding: 6px;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
scrollbar-width: thin;
}
.step-tab {
display: inline-flex;
flex: 1;
display: flex;
flex: 1 0 145px;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 46px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 40px;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: transparent;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font-family: inherit;
font-size: 14px;
font-weight: 800;
cursor: pointer;
transition: color 0.2s, background 0.2s, border-color 0.2s, box-shadow 0.2s;
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: 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-action {
color: #9bf4ff;
}
}
&:focus-visible {
outline: 2px solid #72e8ff;
outline-offset: 4px;
}
&.active {
border-color: rgba(0, 170, 255, 0.55);
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
.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);
}
}
&.complete {
color: #dffbff;
.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-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-connector {
position: relative;
flex: 0 1 40px;
min-width: 18px;
height: 2px;
margin: 0 8px;
overflow: visible;
background: rgba(98, 169, 201, 0.34);
&::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);
&::after {
border-color: #63e6f1;
}
}
@ -1148,6 +1276,36 @@ function exportOutcome() {
width: 100%;
}
.step-indicator {
width: 100%;
margin: 0 0 32px;
padding: 16px;
border-radius: 20px;
overflow-x: visible;
}
.step-tab {
flex: 0 0 auto;
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;
&::after {
top: auto;
right: auto;
bottom: -1px;
left: 50%;
transform: translateX(-50%) rotate(135deg);
}
}
.form-grid,
.form-grid.three,
.price-options {

@ -12,12 +12,7 @@
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<section class="factors-workbench">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
@ -28,16 +23,29 @@
</div>
<nav v-if="trainingSteps.length" class="step-indicator" aria-label="实训步骤">
<span v-for="(step, index) in trainingSteps" :key="`${step}-${index}`" class="step-tab" :class="{ active: index === 0 }">
{{ step }}
<template v-for="(step, index) in trainingSteps" :key="`${step}-${index}`">
<button
type="button"
:class="['step-tab', { active: currentStep === index, complete: currentStep > index }]"
:aria-label="`切换至第 ${index + 1} 步:${step}`"
:aria-current="currentStep === index ? 'step' : undefined"
@click="currentStep = index"
>
<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 ? '当前步骤' : '点击切换' }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="index < trainingSteps.length - 1" class="step-connector" aria-hidden="true" />
</template>
</nav>
<section class="factor-step-card">
<div v-if="trainingSteps.length" class="step-title-row">
<div>
<p class="section-eyebrow">Step 01</p>
<h3>{{ trainingSteps[0] }}</h3>
<p class="section-eyebrow">第 {{ String(currentStep + 1).padStart(2, '0') }} 步</p>
<h3>{{ trainingSteps[currentStep] }}</h3>
</div>
</div>
@ -127,6 +135,7 @@ const userStore = useUserStore();
const taskConfig = ref(null);
const saving = ref(false);
const draftReady = ref(false);
const currentStep = ref(0);
const getLocalDraftKey = () => {
const userInfo = userStore.userInfo || {};
@ -235,8 +244,10 @@ async function loadTaskConfig() {
try {
const res = await getTrainingTaskByKey(TASK_KEY);
taskConfig.value = res?.data || null;
currentStep.value = 0;
} catch (error) {
taskConfig.value = null;
currentStep.value = 0;
}
}
@ -426,7 +437,7 @@ function exportOutcome() {
gap: 24px;
width: 100%;
min-height: calc(100vh - 106px);
padding: 0 0 32px;
padding: 24px 24px 32px;
color: #e8f7ff;
background: transparent;
}
@ -558,37 +569,158 @@ function exportOutcome() {
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-bottom: 32px;
padding: 6px;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
scrollbar-width: thin;
}
.step-tab {
display: inline-flex;
flex: 1 1 0;
display: flex;
flex: 1 0 145px;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 40px;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font-family: inherit;
font-size: 14px;
font-weight: 800;
line-height: 1.4;
text-align: center;
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: 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-action {
color: #9bf4ff;
}
}
&:focus-visible {
outline: 2px solid #72e8ff;
outline-offset: 4px;
}
&.active {
border-color: rgba(0, 170, 255, 0.55);
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
.step-marker {
border-color: #72e8ff;
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
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);
}
}
&.complete {
color: #dffbff;
.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-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-connector {
position: relative;
flex: 0 1 40px;
min-width: 18px;
height: 2px;
margin: 0 8px;
background: rgba(98, 169, 201, 0.34);
&::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);
&::after {
border-color: #63e6f1;
}
}
@ -836,13 +968,17 @@ function exportOutcome() {
background: rgba(18, 85, 116, 0.78);
}
@media (max-width: 1180px) {
@media (max-width: 1100px) {
.product-factors {
grid-template-columns: 1fr;
}
}
@media (max-width: 860px) {
@media (max-width: 900px) {
.product-factors {
padding: 14px 14px 18px;
}
.factors-core {
padding: 18px;
border-radius: 24px;
@ -857,8 +993,35 @@ function exportOutcome() {
align-items: stretch;
}
.step-indicator {
width: 100%;
margin: 0 0 32px;
padding: 16px;
overflow-x: visible;
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;
&::after {
top: auto;
right: auto;
bottom: -1px;
left: 50%;
transform: translateX(-50%) rotate(135deg);
}
}
.product-entry {
@ -869,4 +1032,10 @@ function exportOutcome() {
grid-template-columns: 1fr;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -9,12 +9,7 @@
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon>导出实训成果</button>
@ -22,17 +17,22 @@
</div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="售后服务流程设计步骤">
<template v-for="(step, index) in configuredSteps" :key="`${step}-${index}`">
<button
v-for="(step, index) in configuredSteps"
:key="`${step}-${index}`"
type="button"
class="step-tab"
:class="{ active: activeStep === index + 1 }"
:class="['step-tab', { active: activeStep === index + 1, complete: activeStep > index + 1 }]"
:aria-current="activeStep === index + 1 ? 'step' : undefined"
:aria-label="`第${index + 1}步:${step}`"
@click="setCurrentStep(index + 1)"
>
{{ step }}
<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">{{ activeStep === index + 1 ? "当前步骤" : "点击切换" }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="index < configuredSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section v-show="isSectionVisible(1)" class="step-card">
@ -188,14 +188,41 @@ function exportOutcome() { const blob = new Blob([JSON.stringify(buildProgress()
.after-sales-page { display: grid; grid-template-columns: minmax(720px, 1fr) 340px; gap: 24px; min-height: calc(100vh - 76px); padding: 24px 24px 32px; color: #eef5ff; background: #06111d; }
.training-core { min-width: 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); }
.task-header { margin-bottom: 24px; }.task-badge { display:inline-flex; align-items:center; gap:8px; 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); font-size:12px; font-weight:700; }.task-title { margin:12px 0 0; color:#fff; font-size:30px; font-weight:900; line-height:1.3; }
.task-card,.step-card { border:1px solid rgba(82,174,226,.42); border-radius:26px; background:rgba(0,25,42,.64); }.task-card { margin-top:28px; padding:18px 22px 22px; }.workbench-head { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; margin-bottom:22px; padding-bottom:18px; border-bottom:1px solid rgba(0,180,255,.2); }.section-eyebrow { margin:0; color:#71dfff; font-size:12px; font-weight:800; line-height:1.4; }.workbench-head h3 { margin:4px 0 8px; color:#fff; font-size:26px; font-weight:800; line-height:1.3; }.workbench-description,.step-content p,.step-desc { margin:0; color:#d7ecff; font-size:15px; line-height:1.8; }.workbench-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:10px; flex-shrink:0; }
.task-card,.step-card { border:1px solid rgba(82,174,226,.42); border-radius:26px; background:rgba(0,25,42,.64); }.task-card { margin-top:28px; padding:18px 22px 22px; }.workbench-head { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; margin-bottom:22px; padding-bottom:18px; border-bottom:1px solid rgba(0,180,255,.2); }.section-eyebrow { margin:0; color:#71dfff; font-size:12px; font-weight:800; line-height:1.4; }.workbench-head h3 { margin:4px 0 8px; color:#fff; font-size:26px; font-weight:800; line-height:1.3; }.workbench-description,.step-content p,.step-desc { margin:0; color:#d7ecff; font-size:15px; line-height:1.8; }.workbench-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; margin-left:auto; gap:10px; flex-shrink:0; }
.outline-action,.btn-nav { display:inline-flex; align-items:center; justify-content:center; gap:8px; min-height:40px; padding:0 18px; border:1px solid rgba(99,217,255,.55); border-radius:999px; color:#e9fbff; background:rgba(17,126,178,.36); font:inherit; font-size:14px; font-weight:800; cursor:pointer; }.outline-action:hover,.outline-action:focus-visible,.btn-nav:hover:not(:disabled),.btn-nav:focus-visible { border-color:rgba(116,235,255,.85); background:rgba(18,146,204,.5); outline:none; transform:translateY(-1px); }
.step-indicator { display:flex; gap:6px; min-height:58px; margin:0 0 32px; padding:6px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:999px; background:rgba(0,20,35,.65); }.step-tab { flex:1 0 max-content; min-height:46px; padding:0 18px; border:1px solid transparent; border-radius:999px; color:#9ab3d0; background:transparent; font:inherit; font-size:14px; font-weight:800; cursor:pointer; }.step-tab:hover,.step-tab:focus-visible { border-color:rgba(99,217,255,.55); color:#e9fbff; outline:none; }.step-tab.active { border-color:rgba(92,224,255,.7); color:#fff; background:linear-gradient(95deg,#0a6b9e,#0a5c86); }
.step-indicator { display:flex; align-items:center; gap:12px; width:calc(100% + 24px); min-height:0; margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:18px; background:linear-gradient(110deg,rgba(4,39,58,.9),rgba(2,20,33,.94)); box-shadow:inset 0 1px 0 rgba(169,229,255,.08); }.step-tab { display:flex; flex:1 0 145px; align-items:center; min-height:66px; padding:8px 10px; border:1px solid rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); font:inherit; font-size:14px; font-weight:800; text-align:left; cursor:pointer; transition:transform .2s,color .2s,background .2s,border-color .2s,box-shadow .2s; }.step-tab:hover,.step-tab:focus-visible { transform:translateY(-2px); border-color:rgba(89,223,255,.82); color:#fff; background:rgba(8,92,131,.52); box-shadow:0 8px 18px rgba(0,0,0,.2); outline:none; }.step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(95deg,#0a6b9e,#0a5c86); box-shadow:0 8px 20px rgba(0,120,170,.22); }.step-marker { display:inline-flex; flex:0 0 38px; align-items:center; justify-content:center; width:38px; height:38px; border:1px solid rgba(113,223,255,.55); border-radius:50%; color:#bdefff; background:rgba(3,27,43,.72); font-size:13px; font-weight:900; }.step-copy { display:grid; min-width:0; gap:4px; margin-left:10px; }.step-copy strong { display:-webkit-box; overflow:hidden; color:inherit; font-size:14px; line-height:1.25; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.step-copy span { color:#7ec7e7; font-size:12px; font-weight:700; }.step-tab.active .step-marker { border-color:#a8f3ff; color:#fff; background:linear-gradient(135deg,#0bb2ed,#0a6b9e); box-shadow:0 0 0 4px rgba(77,215,255,.13),0 0 18px rgba(77,215,255,.4); }.step-tab.complete .step-marker { border-color:#59e0ca; color:#dffbff; background:rgba(13,132,119,.5); }.step-connector { flex:0 0 28px; height:2px; background:rgba(77,167,220,.54); color:#78cfe9; font-size:16px; line-height:0; text-align:right; }.step-connector.complete { background:#55ddcc; color:#75f0df; }
.step-card { margin-top:32px; padding:18px 22px 22px; }.step-indicator + .step-card,.workbench-head + .step-card { margin-top:0; }.step-card + .step-card { margin-top:16px; }.step-title-row { margin-bottom:14px; }.step-title-row h3 { margin:4px 0 0; color:#fff; font-size:22px; font-weight:900; line-height:1.3; }.step-content h4 { margin:0 0 12px; color:#fff; font-size:22px; font-weight:900; line-height:1.3; }
.service-flow { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; margin:20px 0; }.service-flow article,.hint-card { border:1px solid rgba(0,180,255,.22); border-radius:18px; background:rgba(1,14,26,.72); }.service-flow article { display:grid; gap:6px; min-height:112px; padding:16px; }.service-flow strong { color:#e9fbff; font-size:15px; }.service-flow span { color:#b7d8e9; font-size:13px; line-height:1.65; }.hint-card { margin-top:20px; padding:14px 16px; color:#d7ecff; font-size:14px; line-height:1.75; }.hint-card strong { color:#71dfff; }
.field-block { display:block; margin:16px 0 0; }.field-block span { display:block; margin-bottom:8px; color:#dff5ff; font-size:14px; font-weight:800; line-height:1.6; }.field-block :deep(.el-input__wrapper),.field-block :deep(.el-textarea__inner),.table-wrap :deep(.el-textarea__inner) { border:1px solid rgba(45,95,126,.95); border-radius:14px; box-shadow:none; background:#0f1a24; }.field-block :deep(.el-input__wrapper) { min-height:42px; padding:0 14px; }.field-block :deep(.el-input__inner),.field-block :deep(.el-textarea__inner),.table-wrap :deep(.el-textarea__inner) { color:#eef5ff; font-size:14px; }.field-block :deep(.el-textarea__inner),.table-wrap :deep(.el-textarea__inner) { min-height:110px !important; padding:12px 14px; line-height:1.65; }.field-block :deep(.el-input__wrapper.is-focus),.field-block :deep(.el-textarea__inner:focus),.table-wrap :deep(.el-textarea__inner:focus) { border-color:rgba(92,224,255,.7); box-shadow:0 0 0 3px rgba(0,174,255,.13); }
.table-panel { margin-top:22px; }.panel-title { margin-bottom:12px; }.panel-title h4 { margin:0 0 4px; color:#fff; font-size:18px; font-weight:900; }.panel-title p { color:#9ab3d0; font-size:14px; }.table-wrap { overflow-x:auto; border:1px solid rgba(0,180,255,.22); border-radius:18px; background:rgba(1,14,26,.72); }.table-wrap table { width:100%; min-width:760px; border-collapse:collapse; }.table-wrap th,.table-wrap td { padding:12px; border-right:1px solid rgba(77,167,220,.2); border-bottom:1px solid rgba(77,167,220,.2); vertical-align:top; }.table-wrap thead th { color:#eefaff; background:rgba(8,55,82,.98); font-size:14px; font-weight:800; }.table-wrap tbody th { width:18%; color:#dff5ff; font-size:14px; }.table-wrap td { color:#d7ecff; font-size:14px; line-height:1.7; }.table-wrap tr:last-child th,.table-wrap tr:last-child td { border-bottom:0; }.table-wrap th:last-child,.table-wrap td:last-child { border-right:0; }
.step-navigation { display:flex; justify-content:center; gap:14px; margin-top:22px; }.btn-nav.btn-save,.btn-nav.btn-primary { border-color:rgba(92,224,255,.7); color:#fff; background:linear-gradient(95deg,#0b84bd,#096491); }.btn-nav:disabled { cursor:not-allowed; opacity:.45; transform:none; }.training-actions { display:flex; flex-wrap:wrap; justify-content:center; gap:52px; margin-top:36px; }.training-actions :deep(.training-action) { min-width:172px; height:64px; padding:0 28px; border:1px solid #2c83aa; border-radius:6px; color:#dff7ff; background:rgba(13,64,88,.62); font-size:24px; font-weight:900; }.training-actions :deep(.training-action:hover:not(.is-disabled)) { border-color:#46b5e7; color:#fff; background:rgba(18,85,116,.78); }
@media (max-width:1100px) { .after-sales-page { grid-template-columns:1fr; } }
@media (max-width:900px) { .after-sales-page { padding:14px; }.training-core { padding:18px; border-radius:24px; }.workbench-head,.workbench-actions,.step-navigation { flex-direction:column; align-items:stretch; }.workbench-actions,.outline-action,.btn-nav,.training-actions :deep(.training-action) { width:100%; }.step-indicator { flex-direction:column; border-radius:26px; }.step-tab { flex:none; }.service-flow { grid-template-columns:1fr; } }
@media (max-width:900px) { .after-sales-page { padding:14px; }.training-core { padding:18px; border-radius:24px; }.workbench-head,.workbench-actions,.step-navigation { flex-direction:column; align-items:stretch; }.workbench-actions,.outline-action,.btn-nav,.training-actions :deep(.training-action) { width:100%; }.step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; }.step-tab { flex:0 0 auto; width:100%; }.step-connector { flex:0 0 22px; width:2px; height:22px; margin-left:29px; text-align:center; writing-mode:vertical-rl; }.service-flow { grid-template-columns:1fr; } }
.step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,.78),rgba(5,62,102,.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }.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; box-shadow:0 0 12px rgba(79,231,218,.35); }.step-connector.complete { background:linear-gradient(90deg,#6ef0e1,#1cb8e9); color:#63e6f1; box-shadow:0 0 9px rgba(53,218,235,.46); }
/* Standard page step navigation */
.step-indicator { position:relative; display:flex; align-items:center; gap:0; width:calc(100% + 24px); margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:18px; background:linear-gradient(90deg,rgba(6,42,61,.58),rgba(1,18,31,.42)),rgba(0,0,0,.3); box-shadow:inset 0 1px 0 rgba(145,233,255,.08); 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 rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); box-shadow:inset 0 1px 0 rgba(135,221,255,.04); font:inherit; font-size:14px; font-weight:800; text-align:left; cursor:pointer; transition:transform .2s ease,color .2s ease,border-color .2s ease,background .2s ease,box-shadow .2s ease; }
.step-tab:hover { transform:translateY(-2px); border-color:rgba(89,223,255,.82); color:#fff; background:rgba(8,92,131,.52); box-shadow:0 7px 18px rgba(0,0,0,.24),inset 0 1px 0 rgba(152,239,255,.16); }
.step-tab:hover .step-action { color:#9bf4ff; }
.step-tab:focus-visible { outline:2px solid rgba(92,224,255,.7); outline-offset:2px; }
.step-tab.active { border-color:rgba(89,226,255,0.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,0.78),rgba(5,62,102,0.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }
.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,.6); border-radius:50%; color:#a6c4d9; background:rgba(3,25,40,.8); box-shadow:inset 0 0 12px rgba(38,174,233,.08); font-size:12px; font-weight:900; letter-spacing:.04em; }
.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:.06em; line-height:1; }
.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 .step-marker { border-color:rgba(84,230,221,.85); color:#062b37; background:#6ef0e1; box-shadow:0 0 12px rgba(79,231,218,.35); }
.step-connector { position:relative; flex:0 1 40px; min-width:18px; height:2px; margin:0 8px; overflow:visible; background:rgba(98,169,201,.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,.46); }
.step-tab.complete + .step-connector::after { border-color:#63e6f1; }
@media (max-width:900px) { .step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; } .step-tab { flex:0 0 auto; width:100%; } .step-connector { flex:0 0 20px; min-width:0; width:2px; height:20px; margin-left:29px; } }
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

File diff suppressed because one or more lines are too long

@ -12,12 +12,7 @@
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
@ -28,17 +23,22 @@
</div>
<nav v-if="configuredSteps.length" class="step-indicator" aria-label="产品价值主张及定位步骤">
<template v-for="(step, index) in configuredSteps" :key="`${step}-${index}`">
<button
v-for="(step, index) in configuredSteps"
:key="`${step}-${index}`"
type="button"
class="step-tab"
:class="{ active: form.activeStep === index + 1 }"
:class="['step-tab', { active: form.activeStep === index + 1, complete: form.activeStep > index + 1 }]"
:aria-current="form.activeStep === index + 1 ? 'step' : undefined"
:aria-label="`第${index + 1}步:${step}`"
@click="setCurrentStep(index + 1)"
>
{{ step }}
<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">{{ form.activeStep === index + 1 ? "当前步骤" : "点击切换" }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="index < configuredSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section v-show="isSectionVisible(1)" class="step-card">
@ -470,6 +470,7 @@ function exportOutcome() {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
margin-left: auto;
gap: 10px;
flex-shrink: 0;
}
@ -503,42 +504,157 @@ function exportOutcome() {
}
.step-indicator {
position: relative;
display: flex;
gap: 6px;
min-height: 58px;
margin: 0 0 32px;
padding: 6px;
align-items: center;
gap: 0;
width: calc(100% + 24px);
min-height: 0;
margin: 0 -12px 32px;
padding: 18px 22px;
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 999px;
background: rgba(0, 20, 35, 0.65);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
scrollbar-width: thin;
}
.step-tab {
flex: 1 0 max-content;
min-height: 46px;
padding: 0 18px;
border: 1px solid transparent;
border-radius: 999px;
display: flex;
flex: 1 0 145px;
align-items: center;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: transparent;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font: inherit;
font-size: 14px;
font-weight: 800;
text-align: left;
cursor: pointer;
transition: transform 0.2s ease, color 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.step-tab:hover {
transform: translateY(-2px);
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);
}
.step-tab:hover .step-action {
color: #9bf4ff;
}
.step-tab:hover,
.step-tab:focus-visible {
border-color: rgba(99, 217, 255, 0.55);
color: #e9fbff;
outline: none;
outline: 2px solid rgba(92, 224, 255, 0.7);
outline-offset: 2px;
}
.step-tab.active {
border-color: rgba(92, 224, 255, 0.7);
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
}
.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.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 .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);
&::after {
border-color: #63e6f1;
}
}
.step-card {
@ -667,8 +783,31 @@ function exportOutcome() {
.outline-action,
.btn-nav,
.training-actions :deep(.training-action) { width: 100%; }
.step-indicator { flex-direction: column; border-radius: 26px; }
.step-tab { flex: none; }
.step-indicator {
align-items: stretch;
flex-direction: column;
width: 100%;
margin-right: 0;
margin-left: 0;
overflow: visible;
border-radius: 20px;
}
.step-tab { flex: 0 0 auto; width: 100%; }
.step-connector {
flex: 0 0 20px;
min-width: 0;
width: 2px;
height: 20px;
margin-left: 29px;
text-align: center;
writing-mode: vertical-rl;
}
.value-grid { grid-template-columns: 1fr; }
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -14,12 +14,7 @@
/>
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
@ -30,19 +25,32 @@
</div>
<nav v-if="steps.length" class="step-indicator" aria-label="市场机会识别与选品步骤">
<template v-for="step in steps" :key="step.no">
<button
v-for="step in steps"
:key="step.no"
type="button"
:class="['step-tab', { active: currentStep === step.no }]"
@click="currentStep = step.no"
:class="['step-tab', { active: currentStep === step.no, complete: currentStep > step.no }]"
:aria-label="`切换至第 ${step.no} 步:${step.name}`"
:aria-current="currentStep === step.no ? 'step' : undefined"
@click="changeStep(step.no)"
>
<el-icon><component :is="step.icon" /></el-icon>
<span>{{ step.name }}</span>
<span class="step-marker" aria-hidden="true">{{ String(step.no).padStart(2, '0') }}</span>
<span class="step-copy">
<strong class="step-name" :title="step.name">{{ step.name }}</strong>
<span class="step-action">{{ currentStep === step.no ? '当前步骤' : '点击切换' }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="step.no < steps.length" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section class="step-card market-selection-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>
<section v-show="currentStep === 1" class="step-content">
<p class="task-desc">聚焦“智能空气监测仪/空气质量检测仪”品类,完成以下 PEST 分析表。</p>
@ -167,10 +175,22 @@
</label>
</section>
<div class="training-actions">
<el-button class="training-action training-action--save" :loading="saving" @click="saveCurrentProgress">
<div v-if="steps.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 === steps.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>
@ -193,7 +213,7 @@
</template>
<script setup>
import { DataAnalysis, Download, Operation, Search, TrendCharts } from "@element-plus/icons-vue";
import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
@ -216,8 +236,6 @@ const defaultGoals = [
"能够为后续数据化选品和综合判断提供结构化依据。",
];
const stepIcons = [Search, DataAnalysis, TrendCharts, Operation];
const pestRows = [
{
key: "political",
@ -293,9 +311,15 @@ const steps = computed(() =>
flowSteps.value.map((name, index) => ({
no: index + 1,
name,
icon: stepIcons[index] || Search,
}))
);
const activeStepName = computed(() => steps.value[currentStep.value - 1]?.name || "");
watch(steps, (items) => {
if (items.length && currentStep.value > items.length) {
currentStep.value = items.length;
}
});
const progressItems = computed(() => [
{ text: "进行中:PEST 四维度分析", status: "doing" },
@ -402,11 +426,17 @@ function buildAnswerPayload(status) {
};
}
async function persist(status = "IN_PROGRESS") {
async function persist(status = "IN_PROGRESS", silent = false) {
saving.value = true;
try {
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
if (!silent) {
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
}
} catch (error) {
if (!silent) {
proxy?.$modal?.msgError?.(status === "SUBMITTED" ? "提交失败" : "保存失败");
}
} finally {
saving.value = false;
}
@ -416,6 +446,23 @@ function saveCurrentProgress() {
return persist("IN_PROGRESS");
}
function changeStep(targetStep) {
const maxStep = steps.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 persist("IN_PROGRESS", true);
}
function previousStep() {
changeStep(currentStep.value - 1);
}
function nextStep() {
changeStep(currentStep.value + 1);
}
function submitTask() {
return persist("SUBMITTED");
}
@ -454,9 +501,9 @@ function exportOutcome() {
max-width: 100%;
min-height: calc(100vh - 106px);
margin: 0;
padding: 0 0 32px;
padding: 24px 24px 32px;
color: #e8f7ff;
background: transparent;
background: #06111d;
}
.training-core {
@ -548,9 +595,9 @@ function exportOutcome() {
h3 {
margin: 4px 0 8px;
color: #ffffff;
font-size: 26px !important;
font-weight: 800 !important;
line-height: 1.3 !important;
font-size: 26px;
font-weight: 800;
line-height: 1.3;
}
}
@ -558,6 +605,7 @@ function exportOutcome() {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
margin-left: auto;
justify-content: flex-end;
gap: 10px;
}
@ -599,59 +647,177 @@ function exportOutcome() {
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-bottom: 32px;
padding: 6px;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
overflow-x: auto;
}
.step-tab {
display: inline-flex;
flex: 1 1 0;
display: flex;
flex: 1 0 145px;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 46px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 40px;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: transparent;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font-family: inherit;
font-size: 15px;
font-size: 14px;
font-weight: 800;
cursor: pointer;
transition: color 0.2s, background 0.2s, border-color 0.2s, box-shadow 0.2s;
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: 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-action {
color: #9bf4ff;
}
}
.step-tab.active {
border-color: rgba(0, 170, 255, 0.55);
&:focus-visible {
outline: 2px solid #72e8ff;
outline-offset: 4px;
}
&.active {
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
.step-marker {
border-color: #72e8ff;
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
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-card {
margin-bottom: 0;
&.complete {
color: #dffbff;
.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-content {
animation: fade 0.2s ease;
.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;
}
@keyframes fade {
from {
opacity: 0;
transform: translateY(6px);
.step-copy {
display: grid;
gap: 4px;
min-width: 0;
margin-left: 10px;
}
to {
opacity: 1;
transform: translateY(0);
.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;
}
.step-connector {
position: relative;
flex: 0 0 32px;
min-width: 18px;
height: 2px;
margin: 0 14px;
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);
&::after {
border-color: #63e6f1;
}
}
.step-card {
margin-bottom: 0;
}
.step-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 14px;
h3 {
margin: 4px 0 0;
color: #ffffff;
font-size: 22px;
font-weight: 900;
line-height: 1.3;
}
}
@ -668,10 +834,10 @@ function exportOutcome() {
.table-title {
margin-bottom: 12px;
color: #eafaff;
font-size: 16px;
font-weight: 900;
text-align: center;
color: #dff5ff;
font-size: 15px;
font-weight: 800;
text-align: left;
}
.pest-table {
@ -761,11 +927,11 @@ function exportOutcome() {
input {
width: 190px;
height: 34px;
padding: 0 10px;
border: 1px solid rgba(82, 174, 226, 0.7);
border-radius: 8px;
color: #f4fbff;
min-height: 42px;
padding: 0 14px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
color: #eef5ff;
background: #0f1a24;
outline: none;
}
@ -837,8 +1003,8 @@ function exportOutcome() {
transition: 0.2s;
&:focus {
border-color: #00aaff;
box-shadow: 0 0 8px rgba(0, 170, 255, 0.35);
border-color: rgba(92, 224, 255, 0.7);
box-shadow: 0 0 0 3px rgba(0, 174, 255, 0.13);
}
&::placeholder {
@ -917,6 +1083,43 @@ function exportOutcome() {
background: rgba(18, 85, 116, 0.78);
}
.step-actions {
display: flex;
justify-content: center;
gap: 14px;
margin-top: 22px;
}
.step-actions :deep(.step-action-btn) {
min-height: 40px;
padding: 0 18px;
border-radius: 999px;
font-size: 14px;
font-weight: 800;
}
.step-actions :deep(.step-action-btn--previous) {
border-color: rgba(99, 217, 255, 0.55);
color: #e9fbff;
background: rgba(17, 126, 178, 0.36);
}
.step-actions :deep(.step-action-btn--save),
.step-actions :deep(.step-action-btn--next) {
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.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) {
.market-selection-page {
grid-template-columns: 1fr;
@ -930,7 +1133,10 @@ function exportOutcome() {
}
.workbench-head,
.workbench-actions {
.workbench-actions,
.step-title-row,
.step-actions,
.training-actions {
flex-direction: column;
align-items: stretch;
}
@ -939,18 +1145,56 @@ function exportOutcome() {
.workbench-actions .outline-action {
width: 100%;
}
.market-selection-page {
padding: 14px 14px 18px;
}
@media (max-width: 760px) {
.step-indicator {
align-items: stretch;
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 {
gap: 14px;
}
.step-actions :deep(.step-action-btn),
.training-actions :deep(.training-action) {
width: 100%;
}
}
@media (max-width: 760px) {
.pest-table {
overflow-x: auto;
}
@ -960,4 +1204,10 @@ function exportOutcome() {
min-width: 650px;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -11,8 +11,8 @@
<main class="training-core">
<section class="task-header"><div class="task-badge"><el-icon><Money /></el-icon> 核心实训 · 产品规划与设计</div><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"><div><p class="section-eyebrow">Training Task</p><h3>任务实训</h3><p class="workbench-description">{{ taskRequirement }}</p></div><div class="workbench-actions"><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /><button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon>导出实训成果</button></div></div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="产品定价步骤"><button v-for="(step,index) in configuredSteps" :key="`${step}-${index}`" type="button" class="step-tab" :class="{active:activeStep===index+1}" :aria-current="activeStep===index+1?'step':undefined" @click="setCurrentStep(index+1)">{{ step }}</button></nav>
<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" /><button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon>导出实训成果</button></div></div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="产品定价步骤"><template v-for="(step,index) in configuredSteps" :key="`${step}-${index}`"><button type="button" :class="['step-tab',{active:activeStep===index+1,complete:activeStep>index+1}]" :aria-current="activeStep===index+1?'step':undefined" :aria-label="`第${index+1}步:${step}`" @click="setCurrentStep(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">{{ activeStep===index+1?'当前步骤':'点击切换' }} <span aria-hidden="true">→</span></span></span></button><span v-if="index<configuredSteps.length-1" class="step-connector" aria-hidden="true"></span></template></nav>
<section v-show="isSectionVisible(1)" class="step-card"><div class="step-title-row"><div><p class="section-eyebrow">Step {{ String(activeStep).padStart(2,'0') }}</p><h3>{{ activeStepName }}</h3></div></div>
<div class="caseBacktitle">
@ -814,5 +814,26 @@ onMounted(async () => {
display: grid; grid-template-columns: minmax(720px, 1fr) 340px; gap: 24px; min-height: calc(100vh - 76px); padding: 24px 24px 32px; color: #eef5ff; background: #06111d;
.training-core { min-width: 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); }.task-header { margin-bottom: 24px; }.task-badge { display: inline-flex; align-items: center; gap: 8px; 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); font-size: 12px; font-weight: 700; }.task-title { margin: 12px 0 0; color: #fff; font-size: 30px; font-weight: 900; line-height: 1.3; }.task-card,.step-card { border: 1px solid rgba(82,174,226,.42); border-radius: 26px; background: rgba(0,25,42,.64); }.task-card { margin-top: 28px; padding: 18px 22px 22px; }.workbench-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; margin-bottom: 22px; padding-bottom: 18px; border-bottom: 1px solid rgba(0,180,255,.2); }.section-eyebrow { margin: 0; color: #71dfff; font-size: 12px; font-weight: 800; line-height: 1.4; }.workbench-head h3 { margin: 4px 0 8px; color: #fff; font-size: 26px; font-weight: 800; line-height: 1.3; }.workbench-description { margin: 0; color: #d7ecff; font-size: 15px; line-height: 1.8; }.workbench-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; flex-shrink: 0; }.outline-action,.btn-nav { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 40px; padding: 0 18px; border: 1px solid rgba(99,217,255,.55); border-radius: 999px; color: #e9fbff; background: rgba(17,126,178,.36); font: inherit; font-size: 14px; font-weight: 800; cursor: pointer; }.step-indicator { display: flex; gap: 6px; min-height: 58px; margin: 0 0 32px; padding: 6px; overflow-x: auto; border: 1px solid rgba(0,180,255,.3); border-radius: 999px; background: rgba(0,20,35,.65); }.step-tab { flex: 1 0 max-content; min-height: 46px; padding: 0 18px; border: 1px solid transparent; border-radius: 999px; color: #9ab3d0; background: transparent; font: inherit; font-size: 14px; font-weight: 800; cursor: pointer; }.step-tab.active { border-color: rgba(92,224,255,.7); color: #fff; background: linear-gradient(95deg,#0a6b9e,#0a5c86); }.step-card { margin-top: 32px; padding: 18px 22px 22px; }.step-indicator + .step-card,.workbench-head + .step-card { margin-top: 0; }.step-title-row { margin-bottom: 14px; }.step-title-row h3 { margin: 4px 0 0; color: #fff; font-size: 22px; font-weight: 900; line-height: 1.3; }.caseBacktitle { display: flex; align-items: center; gap: 8px; margin: 20px 0 10px; color: #dff5ff; font-size: 16px; font-weight: 800; }.caseBacktitle img { display: none; }.step-card > p { color: #d7ecff; font-size: 15px; line-height: 1.8; }:deep(.el-table) { width: 100% !important; max-width: 100%; border-radius: 18px; }:deep(.el-table th.el-table__cell) { background: rgba(8,55,82,.98) !important; }:deep(.el-input__wrapper),:deep(.el-textarea__inner) { border-radius: 14px; background: #0f1a24; }.step-navigation { display: flex; justify-content: center; gap: 14px; margin-top: 22px; }.btn-nav.btn-save,.btn-nav.btn-primary { border-color: rgba(92,224,255,.7); color: #fff; background: linear-gradient(95deg,#0b84bd,#096491); }.btn-nav:disabled { cursor: not-allowed; opacity: .45; }.taskfooter { gap: 52px; margin-top: 36px; padding-top: 0; border-top: 0; }.taskfooter :deep(.el-button) { min-width: 172px; height: 64px; border: 1px solid #2c83aa !important; border-radius: 6px; color: #dff7ff !important; background: rgba(13,64,88,.62) !important; font-size: 24px; font-weight: 900; }
}
@media (max-width:1100px) { .price { grid-template-columns:1fr; } } @media (max-width:900px) { .price { padding:14px; .training-core { padding:18px; border-radius:24px; }.workbench-head,.workbench-actions,.step-navigation { flex-direction:column; align-items:stretch; }.workbench-actions,.outline-action,.btn-nav,.taskfooter :deep(.el-button) { width:100%; }.step-indicator { flex-direction:column; border-radius:26px; }.step-tab { flex:none; }.taskfooter { gap:14px; }.cnetent { flex-direction:column; .left,.right { width:100%; } } } }
.price {
.workbench-actions { margin-left: auto; }
.step-indicator { display:flex; align-items:center; gap:12px; width:calc(100% + 24px); min-height:0; margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:18px; background:linear-gradient(110deg,rgba(4,39,58,.9),rgba(2,20,33,.94)); box-shadow:inset 0 1px 0 rgba(169,229,255,.08); }
.step-tab { display:flex; flex:1 0 145px; align-items:center; min-height:66px; padding:8px 10px; border:1px solid rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); text-align:left; transition:transform .2s,color .2s,background .2s,border-color .2s,box-shadow .2s; }
.step-tab:hover,.step-tab:focus-visible { transform:translateY(-2px); border-color:rgba(89,223,255,.82); color:#fff; background:rgba(8,92,131,.52); box-shadow:0 8px 18px rgba(0,0,0,.2); outline:none; }
.step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(95deg,#0a6b9e,#0a5c86); box-shadow:0 8px 20px rgba(0,120,170,.22); }
.step-marker { display:inline-flex; flex:0 0 38px; align-items:center; justify-content:center; width:38px; height:38px; border:1px solid rgba(113,223,255,.55); border-radius:50%; color:#bdefff; background:rgba(3,27,43,.72); font-size:13px; font-weight:900; }
.step-copy { display:grid; min-width:0; gap:4px; margin-left:10px; }.step-copy strong { display:-webkit-box; overflow:hidden; color:inherit; font-size:14px; line-height:1.25; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.step-copy span { color:#7ec7e7; font-size:12px; font-weight:700; }.step-tab.active .step-marker { border-color:#a8f3ff; color:#fff; background:linear-gradient(135deg,#0bb2ed,#0a6b9e); box-shadow:0 0 0 4px rgba(77,215,255,.13),0 0 18px rgba(77,215,255,.4); }.step-tab.complete .step-marker { border-color:#59e0ca; color:#dffbff; background:rgba(13,132,119,.5); }.step-connector { flex:0 0 28px; height:2px; background:rgba(77,167,220,.54); color:#78cfe9; font-size:16px; line-height:0; text-align:right; }.step-connector.complete { background:#55ddcc; color:#75f0df; }
}
@media (max-width:1100px) { .price { grid-template-columns:1fr; } } @media (max-width:900px) { .price { padding:14px; .training-core { padding:18px; border-radius:24px; }.workbench-head,.workbench-actions,.step-navigation { flex-direction:column; align-items:stretch; }.workbench-actions,.outline-action,.btn-nav,.taskfooter :deep(.el-button) { width:100%; }.step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; }.step-tab { flex:0 0 auto; width:100%; }.step-connector { flex:0 0 22px; width:2px; height:22px; margin-left:29px; text-align:center; writing-mode:vertical-rl; }.taskfooter { gap:14px; }.cnetent { flex-direction:column; .left,.right { width:100%; } } } }
.step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,.78),rgba(5,62,102,.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }.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; box-shadow:0 0 12px rgba(79,231,218,.35); }.step-connector.complete { background:linear-gradient(90deg,#6ef0e1,#1cb8e9); color:#63e6f1; box-shadow:0 0 9px rgba(53,218,235,.46); }
.price .step-indicator { position:relative; display:flex; align-items:center; gap:0; width:calc(100% + 24px); margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border-radius:18px; background:linear-gradient(90deg,rgba(6,42,61,.58),rgba(1,18,31,.42)),rgba(0,0,0,.3); }
.price .step-tab { display:flex; flex:1 0 145px; align-items:center; min-width:0; min-height:66px; padding:8px 10px; border:1px solid rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); text-align:left; }
.price .step-tab.active { border-color:rgba(89,226,255,0.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,0.78),rgba(5,62,102,0.64)); box-shadow:0 0 18px rgba(24,183,235,.22); }
.price .step-copy { display:grid; min-width:0; gap:4px; margin-left:10px; }.price .step-name { display:-webkit-box; overflow:hidden; font-size:14px; line-height:1.25; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.price .step-action { color:#66b6d4; font-size:10px; font-weight:800; }.price .step-connector { position:relative; flex:0 1 40px; min-width:18px; height:2px; margin:0 8px; overflow:visible; background:rgba(98,169,201,.34); }.price .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); }.price .step-tab.complete + .step-connector { background:linear-gradient(90deg,#6ef0e1,#1cb8e9); }
@media (max-width:900px) { .price .step-indicator { flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; }.price .step-tab { flex:0 0 auto; width:100%; }.price .step-connector { flex:0 0 20px; min-width:0; width:2px; height:20px; margin-left:29px; } }
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -12,12 +12,7 @@
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
@ -28,17 +23,22 @@
</div>
<nav v-if="configuredSteps.length" class="step-indicator" aria-label="产品架构设计步骤">
<template v-for="(step, index) in configuredSteps" :key="`${step}-${index}`">
<button
v-for="(step, index) in configuredSteps"
:key="`${step}-${index}`"
type="button"
class="step-tab"
:class="{ active: activeStep === index + 1 }"
:class="['step-tab', { active: activeStep === index + 1, complete: activeStep > index + 1 }]"
:aria-current="activeStep === index + 1 ? 'step' : undefined"
:aria-label="`第${index + 1}步:${step}`"
@click="setCurrentStep(index + 1)"
>
{{ step }}
<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">{{ activeStep === index + 1 ? "当前步骤" : "点击切换" }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="index < configuredSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section v-show="isSectionVisible(1)" class="step-card">
@ -410,13 +410,25 @@ function exportOutcome() {
.section-eyebrow { margin: 0; color: #71dfff; font-size: 12px; font-weight: 800; line-height: 1.4; letter-spacing: .02em; }
.workbench-head h3 { margin: 4px 0 8px; color: #fff; font-size: 26px; font-weight: 800; line-height: 1.3; }
.workbench-description, .step-desc { margin: 0; color: #d7ecff; font-size: 15px; line-height: 1.8; }
.workbench-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; flex-shrink: 0; }
.workbench-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; margin-left: auto; gap: 10px; flex-shrink: 0; }
.outline-action, .btn-nav, .mini-add { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 40px; padding: 0 18px; border: 1px solid rgba(99, 217, 255, .55); border-radius: 999px; color: #e9fbff; background: rgba(17, 126, 178, .36); font: inherit; font-size: 14px; font-weight: 800; cursor: pointer; }
.outline-action:hover, .outline-action:focus-visible, .btn-nav:hover:not(:disabled), .btn-nav:focus-visible, .mini-add:hover, .mini-add:focus-visible { border-color: rgba(116, 235, 255, .85); background: rgba(18, 146, 204, .5); outline: none; transform: translateY(-1px); }
.step-indicator { display: flex; gap: 6px; min-height: 58px; margin: 0 0 32px; padding: 6px; overflow-x: auto; border: 1px solid rgba(0, 180, 255, .3); border-radius: 999px; background: rgba(0, 20, 35, .65); }
.step-tab { flex: 1 0 max-content; min-height: 46px; padding: 0 18px; border: 1px solid transparent; border-radius: 999px; color: #9ab3d0; background: transparent; font: inherit; font-size: 14px; font-weight: 800; cursor: pointer; }
.step-tab:hover, .step-tab:focus-visible { border-color: rgba(99, 217, 255, .55); color: #e9fbff; outline: none; }
.step-tab.active { border-color: rgba(92, 224, 255, .7); color: #fff; background: linear-gradient(95deg, #0a6b9e, #0a5c86); }
.step-indicator { position: relative; display: flex; align-items: center; gap: 0; width: calc(100% + 24px); min-height: 0; margin: 0 -12px 32px; padding: 18px 22px; overflow-x: auto; border: 1px solid rgba(0, 180, 255, .3); border-radius: 18px; background: linear-gradient(90deg, rgba(6, 42, 61, .58), rgba(1, 18, 31, .42)), rgba(0, 0, 0, .3); box-shadow: inset 0 1px 0 rgba(145, 233, 255, .08); 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 rgba(68, 141, 177, .35); border-radius: 8px; color: #9ab3d0; background: rgba(3, 27, 43, .52); box-shadow: inset 0 1px 0 rgba(135, 221, 255, .04); font: inherit; font-size: 14px; font-weight: 800; text-align: left; cursor: pointer; transition: transform .2s ease, color .2s ease, border-color .2s ease, background .2s ease, box-shadow .2s ease; }
.step-tab:hover { transform: translateY(-2px); border-color: rgba(89, 223, 255, .82); color: #fff; background: rgba(8, 92, 131, .52); box-shadow: 0 7px 18px rgba(0, 0, 0, .24), inset 0 1px 0 rgba(152, 239, 255, .16); }
.step-tab:hover .step-action { color: #9bf4ff; }
.step-tab:focus-visible { outline: 2px solid rgba(92, 224, 255, .7); outline-offset: 2px; }
.step-tab.active { border-color: rgba(89, 226, 255, 0.85); color: #fff; background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64)); box-shadow: 0 0 18px rgba(24, 183, 235, .22), inset 0 1px 0 rgba(176, 246, 255, .2); }
.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, .6); border-radius: 50%; color: #a6c4d9; background: rgba(3, 25, 40, .8); box-shadow: inset 0 0 12px rgba(38, 174, 233, .08); font-size: 12px; font-weight: 900; letter-spacing: .04em; transition: border-color .2s ease, color .2s ease, background .2s ease, box-shadow .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: .06em; line-height: 1; transition: color .2s ease; }
.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 .step-marker { border-color: rgba(84, 230, 221, .85); color: #062b37; background: #6ef0e1; box-shadow: 0 0 12px rgba(79, 231, 218, .35); }
.step-connector { position: relative; flex: 0 1 40px; min-width: 18px; height: 2px; margin: 0 8px; overflow: visible; background: rgba(98, 169, 201, .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, .46); }
.step-tab.complete + .step-connector::after { border-color: #63e6f1; }
.step-card { margin-top: 32px; padding: 18px 22px 22px; }
.step-indicator + .step-card, .workbench-head + .step-card { margin-top: 0; }
.step-card + .step-card { margin-top: 16px; }
@ -455,5 +467,12 @@ function exportOutcome() {
.training-actions :deep(.training-action) { min-width: 172px; height: 64px; padding: 0 28px; border: 1px solid #2c83aa; border-radius: 6px; color: #dff7ff; background: rgba(13, 64, 88, .62); font-size: 24px; font-weight: 900; }
.training-actions :deep(.training-action:hover:not(.is-disabled)) { border-color: #46b5e7; color: #fff; background: rgba(18, 85, 116, .78); }
@media (max-width: 1100px) { .structure-page { grid-template-columns: 1fr; } }
@media (max-width: 900px) { .structure-page { padding: 14px; } .training-core { padding: 18px; border-radius: 24px; } .workbench-head, .workbench-actions, .step-navigation { flex-direction: column; align-items: stretch; } .workbench-actions, .outline-action, .btn-nav, .training-actions :deep(.training-action) { width: 100%; } .step-indicator { flex-direction: column; border-radius: 26px; } .step-tab { flex: none; } .info-grid, .category-grid, .role-grid, .inline-fields { grid-template-columns: 1fr; } .role-card--wide { grid-column: span 1; } }
@media (max-width: 900px) { .structure-page { padding: 14px; } .training-core { padding: 18px; border-radius: 24px; } .workbench-head, .workbench-actions, .step-navigation { flex-direction: column; align-items: stretch; } .workbench-actions, .outline-action, .btn-nav, .training-actions :deep(.training-action) { width: 100%; } .step-indicator { align-items: stretch; flex-direction: column; width: 100%; margin-right: 0; margin-left: 0; overflow: visible; border-radius: 20px; } .step-tab { flex: 0 0 auto; width: 100%; } .step-connector { flex: 0 0 20px; min-width: 0; width: 2px; height: 20px; margin-left: 29px; text-align: center; writing-mode: vertical-rl; } .info-grid, .category-grid, .role-grid, .inline-fields { grid-template-columns: 1fr; } .role-card--wide { grid-column: span 1; } }
.step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,.78),rgba(5,62,102,.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }.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; box-shadow:0 0 12px rgba(79,231,218,.35); }.step-connector.complete { background:linear-gradient(90deg,#6ef0e1,#1cb8e9); color:#63e6f1; box-shadow:0 0 9px rgba(53,218,235,.46); }
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -543,24 +543,15 @@ onMounted(() => {
--test-panel-strong: rgba(1, 14, 26, 0.82);
--test-line: rgba(82, 174, 226, 0.42);
min-height: calc(100vh - 106px);
padding: 0 0 32px;
min-height: calc(100vh - 76px);
padding: 24px 24px 32px;
color: #eef5ff;
background:
radial-gradient(circle at 10% 0%, rgba(0, 170, 255, 0.12), transparent 32%),
linear-gradient(180deg, rgba(5, 17, 27, 0.98), rgba(3, 14, 24, 0.98));
background: #06111d;
:deep(.training-step-area) {
margin-top: 0 !important;
border-color: var(--test-line) !important;
background: var(--test-panel) !important;
box-shadow: inset 0 1px 0 rgba(169, 229, 255, 0.08), 0 22px 48px rgba(0, 0, 0, 0.22) !important;
@media (max-width: 1100px) {
& {
grid-template-columns: 1fr;
}
:deep(.training-step-area > p) {
color: #d7ecff;
font-size: 15px;
line-height: 1.8;
}
:deep(.el-table) {
@ -645,7 +636,7 @@ onMounted(() => {
line-height: 1.65;
}
:deep(.el-button) {
:deep(.el-button:not(.training-action)) {
display: inline-flex;
align-items: center;
justify-content: center;
@ -659,126 +650,12 @@ onMounted(() => {
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
:deep(.el-button:hover:not(.is-disabled)) {
:deep(.el-button:not(.training-action):hover:not(.is-disabled)) {
transform: translateY(-1px);
border-color: rgba(116, 235, 255, 0.85) !important;
background: rgba(18, 146, 204, 0.5) !important;
}
.taskfooter {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 52px;
margin-top: 36px;
padding-top: 28px;
border-top: 1px solid rgba(0, 180, 255, 0.2);
}
.taskfooter :deep(.el-button) {
min-width: 172px;
min-height: 64px;
padding: 0 28px;
border-color: #2c83aa !important;
border-radius: 6px;
color: #dff7ff !important;
font-size: 24px;
font-weight: 900;
background: rgba(13, 64, 88, 0.62) !important;
}
.taskfooter :deep(.el-button:hover:not(.is-disabled)) {
border-color: #46b5e7 !important;
color: #ffffff !important;
background: rgba(18, 85, 116, 0.78) !important;
}
:deep(.training-intro__steps) {
display: flex !important;
flex-wrap: nowrap !important;
align-items: center !important;
justify-content: space-between !important;
gap: 6px !important;
width: calc(100% - 48px) !important;
min-height: 58px;
margin: 0 24px 24px !important;
padding: 6px !important;
border: 1px solid rgba(0, 180, 255, 0.3) !important;
border-radius: 60px !important;
background: rgba(0, 0, 0, 0.3) !important;
}
:deep(.training-intro__steps .step-tab) {
display: inline-flex !important;
flex: 1 1 0 !important;
align-items: center !important;
justify-content: center !important;
gap: 8px !important;
min-width: 0 !important;
width: auto !important;
min-height: 46px !important;
height: 46px !important;
padding: 0 14px !important;
border: 1px solid transparent !important;
border-radius: 40px !important;
color: #9ab3d0 !important;
background: transparent !important;
box-shadow: none !important;
font-size: 14px !important;
font-weight: 800 !important;
line-height: 1.2 !important;
white-space: normal !important;
transition: color 0.2s, background 0.2s, border-color 0.2s, box-shadow 0.2s !important;
}
:deep(.training-intro__steps .step-tab:hover) {
border-color: rgba(99, 217, 255, 0.45) !important;
color: #e9fbff !important;
background: rgba(17, 126, 178, 0.28) !important;
}
:deep(.training-intro__steps .step-tab.active) {
border-color: rgba(0, 170, 255, 0.55) !important;
color: #ffffff !important;
background: linear-gradient(95deg, #0a6b9e, #0a5c86) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
}
@media (max-width: 900px) {
:deep(.training-intro__steps) {
flex-direction: column !important;
align-items: stretch !important;
gap: 8px !important;
width: calc(100% - 36px) !important;
margin: 0 18px 24px !important;
border-radius: 24px !important;
}
:deep(.training-intro__steps .step-tab) {
width: 100% !important;
}
.taskfooter {
gap: 14px;
}
.taskfooter :deep(.el-button) {
width: 100%;
}
:deep(.el-table) {
width: 100% !important;
overflow-x: auto;
}
}
@media (min-width: 901px) and (max-width: 1100px) {
:deep(.training-intro__steps) {
flex-direction: row !important;
align-items: center !important;
}
}
.release-phase {
display: grid;
gap: 14px;
@ -831,6 +708,10 @@ onMounted(() => {
}
@media (max-width: 900px) {
& {
padding: 14px 14px 18px;
}
.release-phase :deep(.el-button) {
width: 100%;
}

@ -757,8 +757,8 @@ onMounted(() => {
justify-content: flex-end;
}
@media (max-width: 768px) {
padding: 10px;
@media (max-width: 900px) {
padding: 14px;
.data-table,
.chart-container {

@ -9,12 +9,7 @@
<TrainingTaskBrief :background="page.background" :goals="page.goals" :requirements="page.requirements" :show-material="false" />
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="page.requirements || page.task" class="workbench-description">{{ page.requirements || page.task }}</p>
</div>
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<el-button
class="material-download"
@ -32,15 +27,28 @@
</div>
<nav v-if="configuredSteps.length" class="step-indicator" aria-label="实训步骤">
<span v-for="(step, index) in configuredSteps" :key="`${step}-${index}`" class="step-tab" :class="{ active: currentStep === index + 1 }">
{{ step }}
<template v-for="(step, index) in configuredSteps" :key="`${step}-${index}`">
<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="currentStep = 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 < configuredSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section v-if="processRows.length" class="process-step-card">
<div v-if="configuredSteps.length" class="step-title-row">
<div>
<p class="section-eyebrow">Step {{ String(currentStep).padStart(2, "0") }}</p>
<p class="section-eyebrow">第 {{ String(currentStep).padStart(2, "0") }} 步</p>
<h3>{{ activeStepName }}</h3>
</div>
</div>
@ -89,7 +97,7 @@
<section v-if="!processRows.length" class="generic-step-card">
<div v-if="configuredSteps.length" class="step-title-row">
<div>
<p class="section-eyebrow">Step {{ String(currentStep).padStart(2, "0") }}</p>
<p class="section-eyebrow">第 {{ String(currentStep).padStart(2, "0") }} 步</p>
<h3>{{ activeStepName }}</h3>
</div>
</div>
@ -468,9 +476,9 @@ function exportOutcome() {
max-width: 100%;
min-height: calc(100vh - 106px);
margin: 0;
padding: 0 0 32px;
padding: 24px 24px 32px;
color: #e8f7ff;
background: transparent;
background: #06111d;
}
.training-core,
@ -614,6 +622,7 @@ function exportOutcome() {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
margin-left: auto;
justify-content: flex-end;
gap: 10px;
}
@ -650,39 +659,157 @@ function exportOutcome() {
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-bottom: 32px;
padding: 6px;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
overflow-x: auto;
scrollbar-width: thin;
}
.step-tab {
display: inline-flex;
flex: 1 1 0;
display: flex;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 40px;
flex: 1 0 145px;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: transparent;
font-size: 15px;
font-weight: 800;
line-height: 1.2;
text-align: center;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font-family: inherit;
cursor: pointer;
text-align: left;
transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
&: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-action {
color: #9bf4ff;
}
}
&:focus-visible {
outline: 2px solid #72e8ff;
outline-offset: 4px;
}
&.active {
border-color: rgba(0, 170, 255, 0.55);
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
.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);
}
}
&.complete {
color: #dffbff;
.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-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;
gap: 4px;
min-width: 0;
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-connector {
position: relative;
flex: 0 1 40px;
min-width: 18px;
height: 2px;
margin: 0 8px;
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;
}
.step-title-row {
@ -1054,41 +1181,6 @@ function exportOutcome() {
line-height: 1 !important;
}
.step-indicator {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-bottom: 32px;
padding: 6px;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 60px;
background: rgba(0, 0, 0, 0.3);
}
.step-tab {
display: inline-flex;
flex: 1 1 0;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 40px;
color: #9ab3d0;
font-size: 14px;
font-weight: 800;
line-height: 1.4;
text-align: center;
&.active {
border-color: rgba(0, 170, 255, 0.55);
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
}
.process-step-card {
padding: 18px 22px;
border: 1px solid rgba(82, 174, 226, 0.42);
@ -1181,15 +1273,35 @@ function exportOutcome() {
}
}
@media (max-width: 900px) {
@media (max-width: 1100px) {
.student-training-page {
grid-template-columns: 1fr;
max-width: 980px;
margin: 0 auto;
}
}
@media (max-width: 900px) {
.student-training-page {
margin: 0;
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 :deep(.outline-action) {
width: 100%;
}
.workbench-head {
flex-direction: column;
}
@ -1198,20 +1310,68 @@ function exportOutcome() {
grid-template-columns: 1fr;
}
.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;
width: 2px;
min-width: 0;
height: 20px;
align-self: flex-start;
margin: 4px 0 4px 18px;
}
.step-connector::after {
top: auto;
right: auto;
bottom: -1px;
left: 50%;
transform: translateX(-50%) rotate(135deg);
}
.product-development-process-page {
.workbench-actions,
.step-indicator,
.step-title-row,
.upload-action {
flex-direction: column;
align-items: stretch;
}
.step-tab,
.upload-btn {
width: 100%;
}
}
.training-actions {
flex-direction: column;
align-items: stretch;
gap: 14px;
}
.training-actions :deep(.training-action) {
width: 100%;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>

@ -0,0 +1,97 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const pages = ["demand/index.vue", "demand/environment.vue", "demand/people.vue", "demand/consumer-behavior.vue", "demand/consumer-scenario.vue", "demand/feature-conversion.vue", "product/index.vue", "product/differentiation.vue", "product/structure.vue", "product/after-sales-service.vue", "product/components/ProductStandardTrainingPage.vue", "channel/supplier.vue", "channel/index.vue", "product/price.vue", "assessment/optimization.vue", "assessment/diagnosis.vue"];
for (const pageName of pages) {
const page = fs.readFileSync(path.resolve(__dirname, `../src/views/${pageName}`), "utf8");
const tab = page;
const connector = page;
assert.match(tab, /min-width:\s*0;/, `${pageName} should let standard step tabs share the available width`);
assert.match(page, /background:\s*linear-gradient\(115deg,\s*rgba\(4,\s*109,\s*155,\s*0\.78\),\s*rgba\(5,\s*62,\s*102,\s*0\.64\)\);/, `${pageName} should use the standard active-step gradient`);
assert.match(connector, /overflow:\s*visible;/, `${pageName} should not clip the standard connector arrow`);
assert.match(page, /right:\s*-1px;/, `${pageName} should position the standard connector arrow outside the line end`);
}
const peoplePage = fs.readFileSync(path.resolve(__dirname, "../src/views/demand/people.vue"), "utf8");
assert.match(peoplePage, /class="step-connector" aria-hidden="true"><\/span>/, "people.vue should use the standard CSS connector instead of a text arrow");
const behaviorPage = fs.readFileSync(path.resolve(__dirname, "../src/views/demand/consumer-behavior.vue"), "utf8");
assert.match(behaviorPage, /class="step-connector" aria-hidden="true"><\/span>/, "consumer-behavior.vue should use the standard CSS connector instead of a text arrow");
const scenarioPage = fs.readFileSync(path.resolve(__dirname, "../src/views/demand/consumer-scenario.vue"), "utf8");
assert.match(scenarioPage, /class="step-connector" aria-hidden="true"><\/span>/, "consumer-scenario.vue should use the standard CSS connector instead of a text arrow");
const conversionPage = fs.readFileSync(path.resolve(__dirname, "../src/views/demand/feature-conversion.vue"), "utf8");
assert.match(conversionPage, /class="step-connector" aria-hidden="true"><\/span>/, "feature-conversion.vue should use the standard CSS connector instead of a text arrow");
const differentiationPage = fs.readFileSync(path.resolve(__dirname, "../src/views/product/differentiation.vue"), "utf8");
assert.match(differentiationPage, /class="step-connector" aria-hidden="true"><\/span>/, "differentiation.vue should use the standard CSS connector instead of a text arrow");
const structurePage = fs.readFileSync(path.resolve(__dirname, "../src/views/product/structure.vue"), "utf8");
assert.match(structurePage, /class="step-connector" aria-hidden="true"><\/span>/, "structure.vue should use the standard CSS connector instead of a text arrow");
const afterSalesPage = fs.readFileSync(path.resolve(__dirname, "../src/views/product/after-sales-service.vue"), "utf8");
assert.match(afterSalesPage, /class="step-connector" aria-hidden="true"><\/span>/, "after-sales-service.vue should use the standard CSS connector instead of a text arrow");
const productStandardPage = fs.readFileSync(path.resolve(__dirname, "../src/views/product/components/ProductStandardTrainingPage.vue"), "utf8");
assert.match(productStandardPage, /class="step-connector" aria-hidden="true"><\/span>/, "ProductStandardTrainingPage.vue should use the standard CSS connector instead of a text arrow");
const supplierPage = fs.readFileSync(path.resolve(__dirname, "../src/views/channel/supplier.vue"), "utf8");
assert.match(supplierPage, /class="step-connector" aria-hidden="true"><\/span>/, "supplier.vue should use the standard CSS connector instead of a text arrow");
const channelPage = fs.readFileSync(path.resolve(__dirname, "../src/views/channel/index.vue"), "utf8");
assert.match(channelPage, /class="step-connector" aria-hidden="true"><\/span>/, "channel/index.vue should use the standard CSS connector instead of a text arrow");
const pricePage = fs.readFileSync(path.resolve(__dirname, "../src/views/product/price.vue"), "utf8");
assert.match(pricePage, /class="step-connector" aria-hidden="true"><\/span>/, "price.vue should use the standard CSS connector instead of a text arrow");
const optimizationPage = fs.readFileSync(path.resolve(__dirname, "../src/views/assessment/optimization.vue"), "utf8");
assert.match(optimizationPage, /class="step-connector" aria-hidden="true"><\/span>/, "optimization.vue should use the standard CSS connector instead of a text arrow");
const diagnosisPage = fs.readFileSync(path.resolve(__dirname, "../src/views/assessment/diagnosis.vue"), "utf8");
assert.match(diagnosisPage, /class="step-connector" aria-hidden="true"><\/span>/, "diagnosis.vue should use the standard CSS connector instead of a text arrow");
assert.doesNotMatch(diagnosisPage, /<p class="section-eyebrow">Training Task<\/p><h3>任务实训<\/h3><p class="workbench-description">/, "diagnosis.vue should not render the removed task-training heading or description");
assert.match(diagnosisPage, /<TrainingMaterialButton :material-name="taskConfig\?\.materialName" :material-url="taskConfig\?\.materialUrl" \/>/, "diagnosis.vue should retain the training file download control");
const newProductSurveyPage = fs.readFileSync(path.resolve(__dirname, "../src/views/foundation/new-product-survey.vue"), "utf8");
assert.doesNotMatch(newProductSurveyPage, /<p class="section-eyebrow">Training Task<\/p>[\s\S]*?<p v-if="taskRequirement" class="workbench-description">/, "new-product-survey.vue should not render the removed task-training heading or description");
assert.match(newProductSurveyPage, /<TrainingMaterialButton :material-name="taskConfig\?\.materialName" :material-url="taskConfig\?\.materialUrl" \/>/, "new-product-survey.vue should retain the training file download control");
const testIndexPage = fs.readFileSync(path.resolve(__dirname, "../src/views/test/index.vue"), "utf8");
assert.match(testIndexPage, /@media \(max-width: 1100px\)[\s\S]*?grid-template-columns: 1fr;/, "test/index.vue should keep the standard desktop two-column layout until the 1100px breakpoint");
const promotionPage = fs.readFileSync(path.resolve(__dirname, "../src/views/test/promotion.vue"), "utf8");
assert.match(promotionPage, /@media \(max-width: 900px\)[\s\S]*?\.promotion-phase :deep\(\.el-button\) \{ width: 100%; \}/, "test/promotion.vue should switch the standard training controls to the mobile layout at 900px");
const studentTaskWorkbenchPages = [
"training/GenericTrainingPage.vue",
"product/components/ProductStandardTrainingPage.vue",
"foundation/new-product-survey.vue",
"foundation/product-development-factors.vue",
"demand/index.vue",
"demand/environment.vue",
"demand/people.vue",
"demand/consumer-behavior.vue",
"demand/consumer-scenario.vue",
"demand/feature-conversion.vue",
"product/index.vue",
"product/price.vue",
"product/differentiation.vue",
"product/structure.vue",
"product/after-sales-service.vue",
"channel/index.vue",
"channel/supplier.vue",
"assessment/diagnosis.vue",
"assessment/optimization.vue"
];
for (const pageName of studentTaskWorkbenchPages) {
const page = fs.readFileSync(path.resolve(__dirname, `../src/views/${pageName}`), "utf8");
assert.doesNotMatch(page, /class="workbench-head">\s*<div>[\s\S]*?workbench-description/, `${pageName} should not render the task-workbench heading or description`);
assert.match(page, /TrainingMaterialButton|class="material-download"/, `${pageName} should retain the file download control`);
}
console.log("Demand index step navigation matches the standard-page treatment.");

@ -0,0 +1,13 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const root = path.resolve(__dirname, "..");
const source = fs.readFileSync(path.join(root, "src/views/training/GenericTrainingPage.vue"), "utf8");
const shell = source.match(/\.student-training-page \{([\s\S]*?)\n\}/)?.[1] || "";
assert.match(shell, /padding:\s*24px 24px 32px;/, "generic training pages must use the standard shell padding");
assert.match(shell, /background:\s*#06111d;/, "generic training pages must keep the standard continuous deep-blue canvas");
assert.match(source, /@media \(max-width: 1100px\) \{\s*\.student-training-page \{\s*grid-template-columns:\s*1fr;/, "generic training pages must stack the AI column at the standard 1100px breakpoint");
console.log("Generic training page uses the standard shell spacing.");

@ -0,0 +1,23 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const root = path.resolve(__dirname, "..");
const pages = [
"src/views/training/GenericTrainingPage.vue",
"src/views/foundation/new-product-survey.vue",
"src/views/foundation/product-development-factors.vue",
"src/views/demand/index.vue",
"src/views/demand/environment.vue",
];
for (const page of pages) {
const source = fs.readFileSync(path.join(root, page), "utf8");
const connector = source.match(/\.step-connector \{([\s\S]*?)\n\}/)?.[1] || "";
assert.match(connector, /flex:\s*0 1 40px;/, `${page} should use a compact 40px connector basis`);
assert.match(connector, /min-width:\s*18px;/, `${page} should retain an 18px visible connector minimum`);
assert.match(connector, /margin:\s*0 8px;/, `${page} should use compact connector spacing`);
}
console.log("Step indicator desktop spacing is compact.");
Loading…
Cancel
Save