fix: 修复实训资料显示与步骤导航

master
陈沅 1 day ago
parent d8b861bb68
commit ffbc5d567b

@ -5,7 +5,13 @@
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<section class="workbench">
<div class="workbench-top"><div><p>产品问题诊断与迭代</p><h2>{{ currentStep === 1 ? '全链路转化漏斗诊断' : currentStep === 2 ? '用户评论根因分析' : 'AB 测试验证方案' }}</h2></div><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /></div>
<nav class="step-tabs" aria-label=""><button type="button" :class="{ active: currentStep === 1 }" @click="currentStep = 1"><b>01</b><span>全链路漏斗诊断<small>转化率与异常节点</small></span></button><i></i><button type="button" :class="{ active: currentStep === 2 }" @click="currentStep = 2"><b>02</b><span>用户评论根因分析<small>词云与情感倾向</small></span></button><i></i><button type="button" :class="{ active: currentStep === 3 }" @click="currentStep = 3"><b>03</b><span>AB 测试验证<small>改进方案有效性</small></span></button></nav>
<nav class="step-indicator" aria-label="">
<button type="button" class="step-tab" :class="{ active: currentStep === 1 }" @click="currentStep = 1"><b>01</b><span>全链路漏斗诊断<small>转化率与异常节点</small></span></button>
<span class="step-connector" aria-hidden="true"></span>
<button type="button" class="step-tab" :class="{ active: currentStep === 2 }" @click="currentStep = 2"><b>02</b><span>用户评论根因分析<small>词云与情感倾向</small></span></button>
<span class="step-connector" aria-hidden="true"></span>
<button type="button" class="step-tab" :class="{ active: currentStep === 3 }" @click="currentStep = 3"><b>03</b><span>AB 测试验证<small>改进方案有效性</small></span></button>
</nav>
<template v-if="currentStep === 1">
<p class="lead">从用户触达产品的全路径出发搭建从曝光到复购的完整数据漏斗逐层计算各环节转化率并对比行业均值定位产品在哪个环节出现了明显异常流失</p>
@ -92,4 +98,11 @@ onMounted(async () => { try { taskConfig.value = (await getTrainingTaskByKey(TAS
.diagnosis-materials{display:grid;gap:10px;margin:14px 0}.diagnosis-materials__item{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:12px 14px;border:1px solid rgba(76,202,242,.32);border-radius:10px;background:rgba(3,42,61,.45)}.diagnosis-materials__item strong,.diagnosis-materials__item span{display:block}.diagnosis-materials__item strong{color:#e5faff;font-size:15px}.diagnosis-materials__item span{margin-top:4px;color:#93baca;font-size:13px}.diagnosis-materials__actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.diagnosis-step-one-notice{margin:12px 0 0;color:#ff7178;font-size:13px;line-height:1.6}@media (max-width:860px){.diagnosis-materials__item{align-items:flex-start;flex-direction:column}.diagnosis-materials__actions{justify-content:flex-start}}
.ab-materials{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:18px;padding:14px 16px;border-left:3px solid #19c9ff;background:rgba(3,42,61,.45);color:#cde9f2}.ab-materials ol{margin:0;padding-left:20px;line-height:1.8}.ab-materials__actions{display:grid;flex:0 0 178px;gap:8px}@media (max-width:860px){.ab-materials{align-items:stretch;flex-direction:column}.ab-materials__actions{grid-template-columns:repeat(2,minmax(0,1fr));flex-basis:auto}}
.ab-table{width:100%;min-width:860px;border-collapse:collapse}.ab-table th,.ab-table td{border:1px solid rgba(139,213,241,.45)}.ab-table thead th{padding:14px 10px;color:#fff;background:#079ed9;font-size:16px}.ab-table tbody th{width:25%;padding:14px 10px;color:#dff8ff;background:rgba(6,67,94,.34);text-align:center}.ab-table td{padding:9px 11px;text-align:center}.ab-table td:nth-child(2),.ab-table td:nth-child(3){width:18%}.ab-table td:last-child{width:39%}
.step-indicator { display: flex; align-items: center; margin: 0 0 24px; padding: 16px 20px; overflow-x: auto; border: 1px solid rgba(0, 180, 255, .3); border-radius: 18px; background: rgba(1, 18, 31, .62); }
.step-tab { display: flex; flex: 1 0 145px; min-width: 0; align-items: center; gap: 10px; min-height: 66px; padding: 8px; border: 1px solid rgba(68, 141, 177, .35); border-radius: 8px; color: #9ab3d0; background: rgba(3, 27, 43, .52); font: inherit; text-align: left; cursor: pointer; }
.step-tab b { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid currentColor; border-radius: 50%; font-size: 12px; }
.step-tab span { display: grid; gap: 3px; font-weight: 800; }.step-tab small { color: inherit; font-size: 11px; font-weight: 400; }
.step-tab.active { border-color: #53d8fb; color: #fff; background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64)); }
.step-connector { position: relative; flex: 0 1 32px; min-width: 18px; height: 2px; 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); }
</style>

@ -25,6 +25,7 @@
<button type="button" :class="['step-tab', { active: activeStep === index + 1, complete: activeStep > index + 1 }]" @click="setCurrentStep(index + 1)">
<span>{{ String(index + 1).padStart(2, '0') }}</span>{{ step }}
</button>
<span v-if="index < configuredSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
@ -389,4 +390,8 @@ onMounted(async () => {
.strategy-step{display:grid;gap:16px}.subsection-heading{margin:8px 0 0;font-size:18px}.subsection-copy{margin:-8px 0 0;color:var(--muted);line-height:1.6}.category-matrix{position:relative;width:min(100%,820px);height:430px;margin:12px auto 22px;padding:38px 52px 52px 78px}.matrix-grid{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;width:100%;height:100%;border-left:2px solid rgba(185,240,255,.82);border-bottom:2px solid rgba(185,240,255,.82);background:linear-gradient(135deg,rgba(24,169,209,.12),rgba(3,25,38,.16))}.matrix-cell{display:grid;place-content:center;gap:7px;padding:18px;border-right:1px solid rgba(185,240,255,.65);border-bottom:1px solid rgba(185,240,255,.65);text-align:center}.matrix-cell:nth-child(2n){border-right:0}.matrix-cell:nth-child(n+3){border-bottom:0}.matrix-cell strong{font-size:18px}.matrix-cell small{color:#8dddf5}.matrix-y-title,.matrix-x-title,.matrix-y-high,.matrix-y-low,.matrix-x-high,.matrix-x-low{position:absolute;color:#d9f8ff;font-weight:800}.matrix-y-title{top:5px;left:78px}.matrix-x-title{right:0;bottom:15px}.matrix-y-high{top:63px;left:39px}.matrix-y-low{bottom:61px;left:39px}.matrix-x-low{bottom:29px;left:79px}.matrix-x-high{right:44px;bottom:29px}.matrix-inputs{display:grid;grid-template-columns:minmax(210px,.42fr) 1fr;gap:16px}.matrix-inputs label{display:grid;gap:8px;color:#d9f8ff;font-weight:700}.strategy-table{min-width:1120px}.strategy-table td{vertical-align:top}.strategy-table :deep(.el-select){width:100%;min-width:160px}.strategy-table :deep(.el-textarea__inner){min-height:78px}.current-classification{display:inline-block;padding:8px 10px;border-radius:8px;color:#a5efff;background:rgba(17,126,178,.24);white-space:nowrap}
.operations-table{min-width:1050px}.operations-table th{color:#fff;background:linear-gradient(100deg,#08a6df,#098bc1);font-size:16px}.operations-table td{vertical-align:middle}.operations-table td:first-child{width:17%;font-weight:800}.operations-table td:nth-child(2){width:34%}.operations-table td:nth-child(3){width:25%}.operations-table td:last-child{width:24%}.operations-table :deep(.el-textarea__inner){min-height:86px;text-align:left}.operations-table :deep(.el-select){width:100%;min-width:160px}.layer-cell{color:#dff8ff;font-size:16px;background:rgba(15,103,137,.18)!important}
:deep(.el-input__wrapper){background:rgba(1,15,25,.76);box-shadow:0 0 0 1px rgba(94,213,245,.28) inset}:deep(.el-input__inner){color:#fff;text-align:center}@media(max-width:1100px){.optimization{grid-template-columns:1fr}}@media(max-width:720px){.optimization{padding:12px}.training-core{padding:16px;border-radius:20px}.workbench-head,.table-toolbar{flex-direction:column;align-items:stretch}.workbench-actions,.table-toolbar>div,.step-navigation,.training-actions{justify-content:stretch}.workbench-actions>*,.action-button,.nav-button,.training-actions :deep(.training-action){flex:1;width:100%}.category-step,.analysis-step{padding:16px}}
.step-tab { min-width: 0; }
.step-tab.active { background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64)); }
.step-connector { position: relative; flex: 0 1 32px; min-width: 18px; height: 2px; 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); }
</style>

@ -8,7 +8,10 @@
<h1>{{ taskTitle }}</h1>
<p>分别从试产与量产场景评估供应渠道形成可执行的采购与风险管理建议</p>
</div>
<div class="workspace-header__actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<span class="task-badge">实训任务</span>
</div>
</header>
<TrainingTaskBrief
@ -22,11 +25,11 @@
<button :class="['step-tab', { active: currentStep === 1, complete: currentStep > 1 }]" :aria-current="currentStep === 1 ? 'step' : undefined" @click="currentStep = 1">
<span class="step-marker">01</span><span class="step-copy"><b>设置评估权重</b><small>{{ currentStep === 1 ? '当前步骤 →' : '点击切换 →' }}</small></span>
</button>
<i></i>
<span class="step-connector" aria-hidden="true"></span>
<button :class="['step-tab', { active: currentStep === 2, complete: currentStep > 2 }]" :aria-current="currentStep === 2 ? 'step' : undefined" @click="openStepTwo">
<span class="step-marker">02</span><span class="step-copy"><b>渠道评分与对比</b><small>{{ currentStep === 2 ? '当前步骤 →' : '点击切换 →' }}</small></span>
</button>
<i></i>
<span class="step-connector" aria-hidden="true"></span>
<button :class="['step-tab', { active: currentStep === 3 }]" :aria-current="currentStep === 3 ? 'step' : undefined" @click="openStepThree">
<span class="step-marker">03</span><span class="step-copy"><b>采购建议与风险</b><small>{{ currentStep === 3 ? '当前步骤 →' : '点击切换 →' }}</small></span>
</button>
@ -194,6 +197,7 @@ import { computed, onMounted, ref } from "vue";
import { CircleCheck, Delete, Plus } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
@ -525,7 +529,7 @@ onMounted(async () => {
.workspace-shell { box-sizing: border-box; width: min(100%, 1520px); margin: 0 auto; padding: 28px; border: 1px solid rgba(0, 180, 255, .25); border-radius: 36px; background: rgba(8, 18, 28, .6); box-shadow: inset 0 1px 0 rgba(169, 229, 255, .08), 0 22px 48px rgba(0, 0, 0, .22); }
.supplier-page .training-core { box-sizing: border-box; min-width: 0; width: 100%; margin: 0; padding: 28px; border: 1px solid rgba(0, 180, 255, .25); border-radius: 36px; background: rgba(8, 18, 28, .6); box-shadow: inset 0 1px 0 rgba(169, 229, 255, .08), 0 22px 48px rgba(0, 0, 0, .22); }
.supplier-page .workspace-shell { width: 100%; max-width: none; margin: 0; padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; }
.workspace-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 24px; }
.workspace-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 24px; }.workspace-header__actions { display: flex; align-items: center; gap: 10px; }
.eyebrow, .step-caption { margin: 0 0 7px; color: #63dcff; font-weight: 700; letter-spacing: .1em; }
.workspace-header h1, .intro-card h2 { margin: 0; color: #fff; }.workspace-header h1 { margin-top: 12px; font-size: 30px; font-weight: 900; line-height: 1.3; }
.workspace-header p:not(.eyebrow), .intro-card > p:last-child { margin: 10px 0 0; color: #9db4c4; line-height: 1.75; }
@ -559,4 +563,7 @@ onMounted(async () => {
.recommendation-card { margin-top: 20px; }.recommendation-card label { display: block; margin-bottom: 9px; color: #e7f5fb; font-weight: 700; }.recommendation-card :deep(.el-textarea__inner) { min-height: 130px !important; color: #e7f6fd; background: rgba(2, 15, 24, .7); box-shadow: 0 0 0 1px #315f75 inset; }
.risk-heading { align-items: flex-end; }.row-actions { display: flex; gap: 10px; }.panel-tip { margin: -4px 0 16px; color: #9db8c6; line-height: 1.7; }.risk-table { min-width: 1080px; }.risk-table th:nth-child(1) { width: 15%; }.risk-table th:nth-child(2) { width: 29%; }.risk-table th:nth-child(3) { width: 16%; }.risk-table :deep(.el-select) { width: 100%; }.default-risk-row :deep(.el-input__inner), .default-risk-row :deep(.el-textarea__inner), .default-risk-row :deep(.el-select__selected-item) { color: #b7cbd6; font-style: italic; }
@media (max-width: 980px) { .weights-grid { grid-template-columns: 1fr; }.step-nav { grid-template-columns: 1fr; gap: 8px; }.step-nav i { display: none; }.workspace-header { flex-direction: column; }.risk-heading { align-items: flex-start; flex-direction: column; }.split-actions { gap: 12px; }.supplier-page { padding: 12px; }.workspace-shell { padding: 16px; } }
.step-tab.active { background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64)); }
.step-connector { position: relative; height: 2px; overflow: visible; background: rgba(86, 198, 235, .48); }
.step-connector::after { position: absolute; top: 50%; right: -1px; width: 7px; height: 7px; border-top: 1px solid #73b1ce; border-right: 1px solid #73b1ce; content: ''; transform: translateY(-50%) rotate(45deg); }
</style>

File diff suppressed because one or more lines are too long

@ -41,12 +41,7 @@
<span>{{ currentStep === step.no ? "当前步骤" : "点击切换" }}</span>
</span>
</button>
<span
v-if="step.no < pricingSteps.length"
class="step-connector"
:class="{ complete: currentStep > step.no }"
aria-hidden="true"
/>
<span v-if="step.no < pricingSteps.length" :class="{ complete: currentStep > step.no }" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
@ -2422,4 +2417,5 @@ onMounted(async () => {
}
}
}
.step-connector { overflow: visible; }
</style>

File diff suppressed because one or more lines are too long

@ -418,17 +418,35 @@ function normalizeTask(task) {
function normalizeMaterial(materialName, materialUrl) {
const name = String(materialName || "").trim();
const url = String(materialUrl || "").trim();
const url = normalizeMaterialUrl(materialUrl);
return isRealMaterial(name, url) ? { name, url } : { name: "", url: "" };
}
function isRealMaterial(materialName, materialUrl) {
const name = String(materialName || "").trim();
const url = String(materialUrl || "").trim();
if (!name) {
return false;
return Boolean(name && url) && !(name === "案例资料.rar" && url === "/file/example.rar");
}
return !(name === "案例资料.rar" && (!url || url === "/file/example.rar"));
function normalizeMaterialUrl(rawUrl) {
const url = String(rawUrl || "").trim();
if (!url) {
return "";
}
try {
const parsed = new URL(url);
if (
["localhost", "127.0.0.1", "::1"].includes(parsed.hostname) &&
parsed.pathname.startsWith("/file/")
) {
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
}
} catch {
//
}
return url;
}
function parseArray(value, fallback) {
@ -515,7 +533,7 @@ async function handleMaterialChange(file) {
try {
const res = await uploadTrainingTaskMaterial(data);
taskForm.materialName = res?.originalFilename || rawFile.name || file?.name || taskForm.materialName;
taskForm.materialUrl = res?.url || res?.fileName || "";
taskForm.materialUrl = normalizeMaterialUrl(res?.url || res?.fileName || "");
proxy?.$modal?.msgSuccess("案例资料上传成功");
} catch (error) {
proxy?.$modal?.msgError?.("案例资料上传失败");

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

@ -8,7 +8,7 @@
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<section class="task-card">
<section class="task-card promotion-phase">
<header class="workbench-head">
<div>
<p class="section-eyebrow">STEP {{ String(activeStep).padStart(2, "0") }}</p>
@ -283,6 +283,7 @@ onMounted(async () => { try { const result = await getTrainingTaskByKey(TASK_KEY
/* 统一为标准实训页步骤条:编号圆点、状态说明和连接箭头。 */
.step-tabs{display:flex;align-items:center;gap:8px;margin:20px 0 28px;padding:18px 22px;overflow-x:auto;border:1px solid rgba(0,180,255,.3);border-radius:22px;background:rgba(1,18,31,.62)}.step-tabs .step-tab{display:flex;flex:1 1 0;align-items:center;min-width:145px;min-height:66px;gap:10px;padding:8px 12px;border:1px solid rgba(68,141,177,.35);border-radius:12px;color:#9ab3d0;background:rgba(3,27,43,.52);font:inherit;text-align:left;cursor:pointer;transition:.2s ease}.step-tabs .step-tab:hover{border-color:rgba(89,223,255,.82);color:#fff;background:rgba(8,92,131,.52);transform:translateY(-1px)}.step-tabs .step-tab:focus-visible{outline:2px solid #72e8ff;outline-offset:3px}.step-tabs .step-marker{display:grid;flex:0 0 38px;width:38px;height:38px;place-items:center;border:1px solid rgba(101,176,208,.6);border-radius:50%;color:#a6c4d9;background:rgba(3,25,40,.8);font-size:12px;font-weight:900}.step-tabs .step-copy{display:grid;min-width:0;gap:4px}.step-tabs .step-copy b{overflow:hidden;font-size:14px;font-weight:800;text-overflow:ellipsis;white-space:nowrap}.step-tabs .step-copy small{color:#66b6d4;font-size:10px;font-weight:800;letter-spacing:.06em}.step-tabs .step-tab.active{border-color:#53d8fb;color:#fff;background:linear-gradient(135deg,rgba(7,156,207,.85),rgba(13,105,161,.92));box-shadow:inset 0 0 24px rgba(77,213,255,.22)}.step-tabs .step-tab.active .step-marker{border-color:#72e8ff;color:#fff;background:linear-gradient(135deg,#08b7e8,#1375d0);box-shadow:0 0 0 5px rgba(25,179,237,.14),0 0 18px rgba(46,208,255,.58)}.step-tabs .step-tab.complete{color:#dffbff}.step-tabs .step-tab.complete .step-marker{border-color:rgba(84,230,221,.85);color:#062b37;background:#6ef0e1}.step-tabs .step-connector{position:relative;flex:0 0 24px;height:2px;background:rgba(86,198,235,.48)}.step-tabs .step-connector::after{position:absolute;top:50%;right:0;width:7px;height:7px;border-top:1px solid #73b1ce;border-right:1px solid #73b1ce;content:'';transform:translateY(-50%) rotate(45deg)}
@media(max-width:720px){.step-tabs{align-items:stretch;flex-direction:column;padding:14px}.step-tabs .step-tab{flex:0 0 auto;width:100%}.step-tabs .step-connector{flex:0 0 18px;width:2px;height:18px;margin-left:18px}.step-tabs .step-connector::after{top:auto;right:auto;bottom:0;left:50%;transform:translateX(-50%) rotate(135deg)}}
@media (max-width: 900px) { .promotion-phase :deep(.el-button) { width: 100%; } }
</style>
<style scoped lang="scss">
.evaluation-form{padding-top:4px}.evaluation-intro{padding:18px;margin-top:14px;border:1px solid rgba(99,183,222,.36);border-radius:14px;background:rgba(8,42,64,.36)}.evaluation-intro p,.decision-section>p{color:#c8e5f1;line-height:1.75}.overall-roi{display:flex;flex-wrap:wrap;gap:12px;margin-top:14px}.overall-roi span{padding:7px 12px;border:1px solid rgba(99,183,222,.42);border-radius:999px;color:#d7f0fa;background:rgba(3,27,43,.58);font-weight:800}.overall-roi .achieved{border-color:rgba(81,224,178,.72);color:#86f3ca}.overall-roi .underperforming{border-color:rgba(255,152,152,.7);color:#ffb0b0}.evaluation-table-wrap{overflow-x:auto;margin-top:24px;border:1px solid rgba(99,183,222,.46);border-radius:14px}.evaluation-table{width:100%;min-width:1050px;border-collapse:collapse}.evaluation-table th,.evaluation-table td{padding:12px;border-right:1px solid rgba(99,183,222,.35);border-bottom:1px solid rgba(99,183,222,.35);vertical-align:middle}.evaluation-table th{white-space:nowrap;color:#effaff;background:#0c9dd6;font-size:14px}.evaluation-table td{background:rgba(2,21,34,.58)}.evaluation-table th:first-child{min-width:110px}.evaluation-table td:nth-child(2){min-width:100px}.evaluation-table td:nth-child(3){min-width:145px}.evaluation-table td:nth-child(4),.evaluation-table td:nth-child(5){min-width:250px}.evaluation-table :deep(.el-select){width:100%}.evaluation-table :deep(.el-input__wrapper),.evaluation-table :deep(.el-textarea__inner),.decision-section :deep(.el-textarea__inner){box-shadow:0 0 0 1px rgba(111,190,225,.3) inset;color:#fff;background:#102233}.synced-roi{color:#7ce4ff;text-align:center;font-size:16px;font-weight:900}.decision-section{margin-top:30px}.decision-section :deep(.el-textarea__inner){min-height:170px!important;padding:12px 14px;line-height:1.75}@media (max-width:720px){.overall-roi{align-items:stretch;flex-direction:column}}

@ -838,7 +838,28 @@ function normalizeMaterial(materialName, materialUrl) {
if (!name || !url || (name === "案例资料.rar" && url === "/file/example.rar")) {
return null;
}
return { name, url };
return { name, url: normalizeMaterialUrl(materialUrl) };
}
function normalizeMaterialUrl(rawUrl) {
const url = String(rawUrl || "").trim();
if (!url) {
return "";
}
try {
const parsed = new URL(url);
if (
["localhost", "127.0.0.1", "::1"].includes(parsed.hostname) &&
parsed.pathname.startsWith("/file/")
) {
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
}
} catch {
//
}
return url;
}
function parseArray(value) {

@ -29,7 +29,7 @@ assert(
"teacher training task editor should use the common upload API for case materials"
);
assert(
/async function handleMaterialChange/.test(teacherPage) && /taskForm\.materialUrl\s*=\s*res\?\.(url|fileName)/.test(teacherPage),
/async function handleMaterialChange/.test(teacherPage) && /taskForm\.materialUrl\s*=\s*normalizeMaterialUrl\(res\?\.(url|fileName)/.test(teacherPage),
"teacher case material upload should persist the returned materialUrl"
);
assert(
@ -40,6 +40,31 @@ assert(
/function handleMaterialDownload/.test(studentPage) && /暂无案例文档/.test(studentPage),
"student task material button should report empty state when no case material exists"
);
assert.match(
studentPage,
/function normalizeMaterialUrl\(rawUrl\)/,
"comprehensive training must normalize legacy localhost case-material URLs before download"
);
assert.match(
studentPage,
/return \{ name, url: normalizeMaterialUrl\(materialUrl\) \};/,
"comprehensive training must use the normalized case-material URL before download"
);
assert.match(
teacherPage,
/function normalizeMaterialUrl\(rawUrl\)/,
"teacher task editor must normalize legacy localhost case-material URLs before rendering"
);
assert.match(
teacherPage,
/const url = normalizeMaterialUrl\(materialUrl\);/,
"teacher task editor must use the normalized legacy URL before rendering"
);
assert.match(
teacherPage,
/taskForm\.materialUrl = normalizeMaterialUrl\(res\?\.url \|\| res\?\.fileName \|\| ""\);/,
"newly uploaded teacher case materials must store deployment-relative URLs"
);
assert(
/TrainingMaterialButton/.test(newProductSurveyPage),
"new product survey page should render the case material button"

Loading…
Cancel
Save