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

570 lines
34 KiB
Vue

This file contains ambiguous Unicode characters!

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

<template>
<div class="supplier-page student-training-shell" v-loading="loading">
<main class="training-core">
<section class="workspace-shell">
<header class="workspace-header">
<div>
<p class="eyebrow">SUPPLIER CHANNEL EVALUATION</p>
<h1>{{ 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
:background="taskBackground"
:goals="taskGoals"
:requirements="taskRequirement"
:show-material="false"
/>
<nav class="step-nav" aria-label="">
<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>
<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>
<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>
</nav>
<template v-if="currentStep === 1">
<section class="intro-card">
<p class="step-caption">Step 01</p>
<h2>设置两种采购场景的评估权重</h2>
<p>试产更关注交付周期和采购灵活性;量产更关注采购成本和品质可靠性。每个场景的权重合计必须为 100%。</p>
</section>
<section class="weights-grid">
<article class="weight-card">
<div class="card-title">
<div><span>场景 A</span><h3>小批量试产100台</h3></div>
<strong :class="{ danger: smallWeightTotal !== 100 }">{{ smallWeightTotal }}%</strong>
</div>
<table class="training-table">
<thead><tr><th>评估维度</th><th>维度说明</th><th>权重(%</th></tr></thead>
<tbody>
<tr v-for="row in rows" :key="row.key">
<td>{{ row.name }}</td><td>{{ row.description }}</td>
<td><el-input-number v-model="row.smallBatchWeight" :min="0" :max="100" :precision="0" controls-position="right" /></td>
</tr>
</tbody>
</table>
</article>
<article class="weight-card">
<div class="card-title">
<div><span>场景 B</span><h3>大规模量产10000台</h3></div>
<strong :class="{ danger: largeWeightTotal !== 100 }">{{ largeWeightTotal }}%</strong>
</div>
<table class="training-table">
<thead><tr><th>评估维度</th><th>维度说明</th><th>权重(%</th></tr></thead>
<tbody>
<tr v-for="row in rows" :key="row.key">
<td>{{ row.name }}</td><td>{{ row.description }}</td>
<td><el-input-number v-model="row.largeBatchWeight" :min="0" :max="100" :precision="0" controls-position="right" /></td>
</tr>
</tbody>
</table>
</article>
</section>
<footer class="page-actions">
<el-button type="primary" size="large" @click="saveStepOne">保存并进入渠道评分 <el-icon><CircleCheck /></el-icon></el-button>
</footer>
</template>
<template v-else-if="currentStep === 2">
<section class="intro-card compact">
<p class="step-caption">Step 02</p>
<h2>为供应渠道评分并比较结果</h2>
<p>每项评分为 010 分,渠道名称和评分均由学生填写;系统会按第一步已保存的权重自动计算加权总分。</p>
</section>
<section v-for="section in scoreSections" :key="section.key" class="score-section">
<div class="section-heading">
<div><span>{{ section.label }}</span><h3>{{ section.title }}</h3></div>
<small>加权总分 = Σ(维度评分 × 权重)</small>
</div>
<div class="table-scroll">
<table class="training-table score-table">
<thead>
<tr><th>渠道</th><th v-for="dimension in rows" :key="dimension.key">{{ dimension.name }}</th><th>加权总分</th></tr>
</thead>
<tbody>
<tr v-for="row in section.rows" :key="row.id">
<td><el-input v-model.trim="row.channel" maxlength="40" placeholder="填写渠道名称" :disabled="section.key === 'large'" @input="section.key === 'small' && syncChannelNames()" /></td>
<td v-for="dimension in rows" :key="dimension.key">
<el-input-number v-model="scoreFor(row, dimension.key).score" :min="0" :max="10" :precision="0" controls-position="right" />
</td>
<td class="total-score">{{ weightedScore(row, section.weights) }}</td>
</tr>
</tbody>
</table>
</div>
</section>
<footer class="page-actions split-actions">
<el-button size="large" @click="currentStep = 1">上一步</el-button>
<el-button type="primary" size="large" @click="saveStepTwo">保存评分结果</el-button>
</footer>
</template>
<template v-else>
<section class="intro-card compact">
<p class="step-caption">Step 03</p>
<h2>渠道优选、采购建议与供应风险</h2>
<p>最优和次优渠道由第二步的加权评分自动排名生成,学生需要分析差异、形成采购建议,并完成供应风险评估。</p>
</section>
<section class="analysis-panel">
<div class="section-heading">
<div><span>(一)渠道优选与场景分析</span><h3>根据评分结果形成综合采购建议</h3></div>
<small>排名结果为只读数据</small>
</div>
<div class="table-scroll">
<table class="training-table ranking-table">
<thead><tr><th>采购场景</th><th>最优渠道排名1</th><th>次优渠道排名2</th><th>分差与分析</th></tr></thead>
<tbody>
<tr v-for="analysis in scenarioAnalyses" :key="analysis.scenario">
<td class="scene-name">{{ analysis.label }}</td>
<td><div class="rank-result"><strong>{{ rankAt(analysis.scenario, 0).channel || '--' }}</strong><span>{{ formatScore(rankAt(analysis.scenario, 0).score) }} 分</span></div></td>
<td><div class="rank-result"><strong>{{ rankAt(analysis.scenario, 1).channel || '--' }}</strong><span>{{ formatScore(rankAt(analysis.scenario, 1).score) }} 分</span></div></td>
<td><el-input v-model.trim="analysis.differenceAnalysis" type="textarea" :rows="3" maxlength="2000" show-word-limit placeholder="结合分差、采购侧重点与渠道能力进行分析" /></td>
</tr>
</tbody>
</table>
</div>
<div class="recommendation-card">
<label for="procurement-recommendation">综合采购建议</label>
<el-input id="procurement-recommendation" v-model.trim="procurementRecommendation" type="textarea" :rows="5" maxlength="3000" show-word-limit placeholder="说明试产与量产阶段的采购组合、切换原则及执行建议" />
</div>
</section>
<section class="analysis-panel">
<div class="section-heading risk-heading">
<div><span>(二)供应风险评估</span><h3>识别风险并制定应对措施</h3></div>
<div class="row-actions">
<el-button :icon="Plus" @click="addRiskRow">增加一行</el-button>
<el-button :icon="Delete" @click="removeRiskRow">删除一行</el-button>
</div>
</div>
<p class="panel-tip">默认的 4 条风险示例不可删除;可新增至 10 条,删除操作仅作用于最后一条新增风险。斜体内容为可直接修改或复制的填写示例。</p>
<div class="table-scroll">
<table class="training-table risk-table">
<thead><tr><th>风险维度</th><th>风险说明</th><th>风险等级(高/中/低)</th><th>应对措施</th></tr></thead>
<tbody>
<tr v-for="(risk, index) in riskRows" :key="risk.id" :class="{ 'default-risk-row': risk.locked }">
<td><el-input v-model.trim="risk.dimension" maxlength="80" placeholder="填写风险维度" /></td>
<td><el-input v-model.trim="risk.description" type="textarea" :rows="3" maxlength="1000" placeholder="填写风险说明" /></td>
<td><el-select v-model="risk.level" placeholder="请选择"><el-option label="高" value="高" /><el-option label="中" value="中" /><el-option label="低" value="低" /></el-select></td>
<td><el-input v-model.trim="risk.measure" type="textarea" :rows="3" maxlength="2000" placeholder="填写应对措施" /></td>
</tr>
</tbody>
</table>
</div>
</section>
<footer class="page-actions split-actions">
<el-button size="large" @click="currentStep = 2">上一步</el-button>
<el-button type="primary" size="large" @click="saveStepThree"> 3 </el-button>
</footer>
</template>
</section>
</main>
<TrainingAiSidebar
:task-key="TASK_KEY"
:has-saved-answer="hasSavedAnswer"
:progress-items="progressItems"
:study-tip="currentStudyTip"
reference-title=""
reference-text="· <br />· 量产阶段重点关注采购成本与质量可靠性<br />· 风险建议应包含明确的应对措施"
assist-text="建议先区分试产与量产的评估侧重点再基于评分结果形成采购建议和供应风险应对方案"
/>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from "vue";
import { CircleCheck, Delete, Plus } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import {
getStudentTrainingAnswer,
saveStudentTrainingAnswer,
validateSupplierEvaluationStepOne,
validateSupplierEvaluationStepTwo,
validateSupplierEvaluationStepThree,
} from "@/api/studentTrainingAnswer";
const TASK_KEY = "supplier-evaluation";
const loading = ref(false);
const currentStep = ref(1);
const stepOneSaved = ref(false);
const stepTwoSaved = ref(false);
const taskConfig = ref({});
const taskTitle = computed(() => taskConfig.value?.taskName || "供应渠道评估");
const taskBackground = computed(() => taskConfig.value?.background || "供应渠道会直接影响产品的采购成本交付稳定性与质量风险需要分别针对试产和量产场景建立评估标准 ");
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives ?? taskConfig.value?.goals, ["根据试产与量产场景设置供应渠道评估权重", "完成渠道评分采购建议与供应风险分析"]));
const taskRequirement = computed(() => taskConfig.value?.requirements || taskConfig.value?.requirement || "完成各步骤校验并保存渠道评分采购建议和供应风险应与案例资料及评估结果保持一致");
const hasSavedAnswer = computed(() => stepOneSaved.value || stepTwoSaved.value || Boolean(procurementRecommendation.value.trim()));
const currentStudyTip = computed(() => {
const tips = {
1: "先区分试产与量产的采购重点确保每个场景的权重合计为 100%",
2: "逐项填写渠道评分重点检查渠道名称和加权评分是否与已保存权重一致",
3: "根据排序结果形成采购建议并为每类供应风险制定可执行的应对措施",
};
return tips[currentStep.value] || tips[1];
});
const progressItems = computed(() => [
{ text: "设置评估权重", status: stepOneSaved.value ? "done" : currentStep.value === 1 ? "doing" : "" },
{ text: "渠道评分与对比", status: stepTwoSaved.value ? "done" : currentStep.value === 2 ? "doing" : "" },
{ text: "采购建议与风险", status: currentStep.value === 3 ? "doing" : "" },
]);
const rows = ref([
{ key: "procurementCost", name: "采购成本", description: "元器件/模组的单台采购总成本含运费税费", smallBatchWeight: 20, largeBatchWeight: 35 },
{ key: "deliveryCycle", name: "交货周期", description: "从下单到收货的时间影响试产/生产进度", smallBatchWeight: 30, largeBatchWeight: 15 },
{ key: "qualityReliability", name: "品质可靠性", description: "元器件是否为正品有无翻新/故障规格匹配度", smallBatchWeight: 20, largeBatchWeight: 30 },
{ key: "procurementFlexibility", name: "采购灵活性", description: "是否支持小批量拆卖零散采购退换货", smallBatchWeight: 20, largeBatchWeight: 10 },
{ key: "serviceSupport", name: "服务支持", description: "是否提供技术文档售后支持退换货是否方便", smallBatchWeight: 10, largeBatchWeight: 10 },
]);
const DEFAULT_RISKS = [
{ dimension: "独家供应风险", description: "仅依赖单一渠道供应中断时无替代方案", level: "", measure: "建立2-3家备选渠道试产阶段分散下单验证品质量产阶段保留备选供应商每季度询价" },
{ dimension: "交期延误风险", description: "供应商交期不稳定影响生产排期", level: "", measure: "建立2-3家备选供应商试产阶段零散采购量产阶段多家供应商共同供货" },
{ dimension: "品质波动风险", description: "元器件批次间品质不一致影响产品一致性", level: "", measure: "建立2-3家备选供应商不间断引入新供应商每批次元器件到货后进行严格质检再入库" },
{ dimension: "价格波动风险", description: "原材料价格波动或供应商调价导致采购成本变化", level: "", measure: "建立2-3家备选供应商尝试引入新供应商每季度询价研判原料价格走势提前备货" },
];
function normalizeScores(source = {}) {
const savedScores = Array.isArray(source)
? new Map(source.map((item) => [item?.dimension, item?.score]))
: null;
return rows.value.reduce((result, item) => {
result[item.key] = Number(savedScores ? savedScores.get(item.name) : source[item.key]) || 0;
return result;
}, {});
}
function createScoreRows(savedRows = []) {
const source = Array.isArray(savedRows) ? savedRows : [];
return Array.from({ length: 5 }, (_, index) => {
const saved = source[index] || {};
return {
id: `${Date.now()}-${index}-${Math.random().toString(16).slice(2)}`,
channel: String(saved.channel || ""),
scores: normalizeScores(saved.scores),
weightedTotal: saved.weightedTotal ?? null,
};
});
}
function createScenarioAnalyses(savedRows = []) {
const source = Array.isArray(savedRows) ? savedRows : [];
const byScenario = new Map(source.map((item) => [item?.scenario, item]));
return [
{ scenario: "smallBatch", label: "小批量试产100", differenceAnalysis: String(byScenario.get("smallBatch")?.differenceAnalysis || "") },
{ scenario: "largeBatch", label: "大规模量产10000", differenceAnalysis: String(byScenario.get("largeBatch")?.differenceAnalysis || "") },
];
}
function createRiskRows(savedRows = []) {
const source = Array.isArray(savedRows) && savedRows.length ? savedRows : DEFAULT_RISKS;
return source.map((item, index) => ({
id: `${Date.now()}-${index}-${Math.random().toString(16).slice(2)}`,
dimension: String(item?.dimension || ""),
description: String(item?.description || ""),
level: ["", "", ""].includes(item?.level) ? item.level : "",
measure: String(item?.measure || ""),
locked: index < DEFAULT_RISKS.length,
}));
}
const smallBatchRows = ref(createScoreRows());
const largeBatchRows = ref(createScoreRows());
const scenarioAnalyses = ref(createScenarioAnalyses());
const procurementRecommendation = ref("");
const riskRows = ref(createRiskRows());
const smallWeightTotal = computed(() => rows.value.reduce((sum, row) => sum + Number(row.smallBatchWeight || 0), 0));
const largeWeightTotal = computed(() => rows.value.reduce((sum, row) => sum + Number(row.largeBatchWeight || 0), 0));
const smallWeights = computed(() => rows.value.reduce((result, row) => ({ ...result, [row.key]: Number(row.smallBatchWeight || 0) }), {}));
const largeWeights = computed(() => rows.value.reduce((result, row) => ({ ...result, [row.key]: Number(row.largeBatchWeight || 0) }), {}));
const scoreSections = computed(() => [
{ key: "small", label: "场景 A", title: "小批量试产100渠道评分", rows: smallBatchRows.value, weights: smallWeights.value },
{ key: "large", label: "场景 B", title: "大规模量产10000渠道评分", rows: largeBatchRows.value, weights: largeWeights.value },
]);
function scoreFor(row, key) {
if (!Object.prototype.hasOwnProperty.call(row.scores, key)) row.scores[key] = 0;
return {
get score() { return row.scores[key]; },
set score(value) { row.scores[key] = value; row.weightedTotal = null; },
};
}
function toNumber(value) {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : 0;
}
function weightedScore(row, weights) {
const score = rows.value.reduce((sum, item) => sum + toNumber(row.scores[item.key]) * toNumber(weights[item.key]) / 100, 0);
return score.toFixed(2);
}
function channelRanking(scenario) {
const source = scenario === "smallBatch" ? smallBatchRows.value : largeBatchRows.value;
const weights = scenario === "smallBatch" ? smallWeights.value : largeWeights.value;
return source
.map((row) => {
const stored = Number(row.weightedTotal);
return { channel: String(row.channel || "").trim(), score: Number.isFinite(stored) ? stored : Number(weightedScore(row, weights)) };
})
.filter((item) => item.channel)
.sort((left, right) => right.score - left.score || left.channel.localeCompare(right.channel, "zh-CN"));
}
function rankAt(scenario, index) {
return channelRanking(scenario)[index] || {};
}
function formatScore(value) {
const score = Number(value);
return Number.isFinite(score) ? score.toFixed(2) : "--";
}
function syncChannelNames() {
largeBatchRows.value.forEach((row, index) => {
row.channel = String(smallBatchRows.value[index]?.channel || "").trim();
});
}
function buildStepOnePayload() {
return {
items: rows.value.map((row) => ({
dimension: row.name,
smallBatchWeight: Number(row.smallBatchWeight || 0),
largeBatchWeight: Number(row.largeBatchWeight || 0),
})),
};
}
function buildStepTwoPayload() {
const serializeChannels = (source) => source.map((row) => ({
channel: String(row.channel || "").trim(),
scores: rows.value.map((dimension) => ({
dimension: dimension.name,
score: toNumber(row.scores[dimension.key]),
})),
}));
return {
smallBatchChannels: serializeChannels(smallBatchRows.value),
largeBatchChannels: serializeChannels(largeBatchRows.value),
};
}
function buildStepThreePayload() {
return {
scenarioAnalyses: scenarioAnalyses.value.map((item) => ({
scenario: item.scenario,
bestChannel: rankAt(item.scenario, 0).channel || "",
secondChannel: rankAt(item.scenario, 1).channel || "",
differenceAnalysis: item.differenceAnalysis,
})),
procurementRecommendation: procurementRecommendation.value,
risks: riskRows.value.map((item) => ({ dimension: item.dimension, description: item.description, level: item.level, measure: item.measure })),
};
}
function parseJson(value) {
if (!value) return null;
if (typeof value === "object") return value;
try { return JSON.parse(value); } catch { return null; }
}
function applyStepOne(answer) {
const result = parseJson(answer?.step1Answer);
if (!Array.isArray(result?.items)) return false;
const source = new Map(result.items.map((item) => [item.dimension, item]));
rows.value.forEach((row) => {
const saved = source.get(row.name);
if (!saved) return;
row.smallBatchWeight = toNumber(saved.smallBatchWeight);
row.largeBatchWeight = toNumber(saved.largeBatchWeight);
});
return true;
}
function applyStepTwo(answer) {
const result = parseJson(answer?.step2Answer);
if (!Array.isArray(result?.smallBatchChannels) || !Array.isArray(result?.largeBatchChannels)) return false;
smallBatchRows.value = createScoreRows(result.smallBatchChannels);
largeBatchRows.value = createScoreRows(result.largeBatchChannels);
syncChannelNames();
return true;
}
function applyStepThree(answer) {
const result = parseJson(answer?.step3Answer);
if (!Array.isArray(result?.scenarioAnalyses) || !Array.isArray(result?.risks)) return false;
scenarioAnalyses.value = createScenarioAnalyses(result.scenarioAnalyses);
procurementRecommendation.value = String(result.procurementRecommendation || "");
riskRows.value = createRiskRows(result.risks);
return true;
}
async function saveStepOne() {
try {
const result = await validateSupplierEvaluationStepOne(buildStepOnePayload());
if (!result?.valid) throw new Error(result?.message || "权重校验未通过");
await saveStudentTrainingAnswer({ taskKey: TASK_KEY, currentStep: 1, saveAction: "SAVE", progressStatus: "IN_PROGRESS", step1Answer: JSON.stringify({ items: result.items }) });
stepOneSaved.value = true;
currentStep.value = 2;
ElMessage.success("权重已保存请继续完成渠道评分");
} catch (error) {
ElMessage.error(error?.message || "保存失败请检查权重填写");
}
}
async function saveStepTwo() {
try {
syncChannelNames();
const result = await validateSupplierEvaluationStepTwo(buildStepTwoPayload());
if (!result?.valid) throw new Error(result?.message || "渠道评分校验未通过");
smallBatchRows.value = createScoreRows(result.smallBatchChannels);
largeBatchRows.value = createScoreRows(result.largeBatchChannels);
await saveStudentTrainingAnswer({ taskKey: TASK_KEY, currentStep: 2, saveAction: "SAVE", progressStatus: "IN_PROGRESS", step2Answer: JSON.stringify({ weights: result.weights, smallBatchChannels: result.smallBatchChannels, largeBatchChannels: result.largeBatchChannels }) });
stepTwoSaved.value = true;
syncChannelNames();
ElMessage.success("渠道评分已保存可进入第 3 步形成采购建议");
} catch (error) {
ElMessage.error(error?.message || "保存失败请检查渠道和评分");
}
}
async function saveStepThree() {
try {
const result = await validateSupplierEvaluationStepThree(buildStepThreePayload());
if (!result?.valid) throw new Error(result?.message || " 3 步校验未通过");
const step3Answer = { scenarioAnalyses: result.scenarioAnalyses, procurementRecommendation: result.procurementRecommendation, risks: result.risks };
await saveStudentTrainingAnswer({ taskKey: TASK_KEY, currentStep: 3, saveAction: "SAVE", progressStatus: "IN_PROGRESS", step3Answer: JSON.stringify(step3Answer) });
applyStepThree({ step3Answer });
ElMessage.success("采购建议与供应风险已保存");
} catch (error) {
ElMessage.error(error?.message || "保存失败请检查填写内容");
}
}
function openStepTwo() {
if (!stepOneSaved.value) {
ElMessage.warning("请先完成并保存第一步权重设置");
return;
}
currentStep.value = 2;
}
function openStepThree() {
if (!stepOneSaved.value || !stepTwoSaved.value) {
ElMessage.warning("请先完成并保存前两步内容");
return;
}
currentStep.value = 3;
}
function addRiskRow() {
if (riskRows.value.length >= 10) {
ElMessage.warning("供应风险最多填写 10 ");
return;
}
riskRows.value.push({ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, dimension: "", description: "", level: "", measure: "", locked: false });
}
function removeRiskRow() {
const index = [...riskRows.value].map((row, index) => ({ row, index })).reverse().find((item) => !item.row.locked)?.index;
if (index === undefined) {
ElMessage.warning("默认的 4 条供应风险不可删除");
return;
}
riskRows.value.splice(index, 1);
}
onMounted(async () => {
loading.value = true;
try {
taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || {};
} catch (_) {
taskConfig.value = {};
}
try {
const answer = await getStudentTrainingAnswer(TASK_KEY);
stepOneSaved.value = applyStepOne(answer);
stepTwoSaved.value = applyStepTwo(answer);
applyStepThree(answer);
if (Number(answer?.currentStep) === 3 && stepOneSaved.value && stepTwoSaved.value) currentStep.value = 3;
else if (Number(answer?.currentStep) === 2 && stepOneSaved.value) currentStep.value = 2;
} catch (error) {
ElMessage.error(error?.message || "页面数据加载失败");
} finally {
loading.value = false;
}
});
</script>
<style scoped>
.supplier-page { min-height: 100%; padding: 24px; color: #dce9f2; background: #06111d; }
.workspace-shell { box-sizing: border-box; width: min(100%, 1520px); margin: 0 auto; padding: 28px; border: 1px solid rgba(0, 180, 255, .25); border-radius: 36px; background: rgba(8, 18, 28, .6); box-shadow: inset 0 1px 0 rgba(169, 229, 255, .08), 0 22px 48px rgba(0, 0, 0, .22); }
.supplier-page .training-core { box-sizing: border-box; min-width: 0; width: 100%; margin: 0; padding: 28px; border: 1px solid rgba(0, 180, 255, .25); border-radius: 36px; background: rgba(8, 18, 28, .6); box-shadow: inset 0 1px 0 rgba(169, 229, 255, .08), 0 22px 48px rgba(0, 0, 0, .22); }
.supplier-page .workspace-shell { width: 100%; max-width: none; margin: 0; padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; }
.workspace-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 24px; }.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; }
.task-badge { display: inline-flex; align-items: center; min-height: 28px; padding: 4px 14px; border: 1px solid rgba(99, 217, 255, .55); border-radius: 999px; color: #dff5ff; background: rgba(17, 126, 178, .36); white-space: nowrap; }
.step-nav { display: grid; grid-template-columns: minmax(0, 1fr) 40px minmax(0, 1fr) 40px minmax(0, 1fr); align-items: center; gap: 8px; margin: 0 0 30px; padding: 18px 22px; border: 1px solid rgba(0, 180, 255, .3); border-radius: 22px; background: rgba(1, 18, 31, .62); }
.step-nav i { position: relative; height: 2px; background: rgba(86, 198, 235, .48); }.step-nav i::after { position: absolute; top: 50%; right: 0; width: 7px; height: 7px; border-top: 1px solid #73b1ce; border-right: 1px solid #73b1ce; content: ''; transform: translateY(-50%) rotate(45deg); }
.step-tab { display: flex; align-items: center; min-width: 0; min-height: 66px; gap: 10px; padding: 8px 12px; border: 1px solid rgba(68, 141, 177, .35); border-radius: 12px; color: #9ab3d0; background: rgba(3, 27, 43, .52); cursor: pointer; font: inherit; text-align: left; transition: .2s ease; }
.step-tab:hover { border-color: rgba(89, 223, 255, .82); color: #fff; background: rgba(8, 92, 131, .52); transform: translateY(-1px); }.step-tab:focus-visible { outline: 2px solid #72e8ff; outline-offset: 3px; }
.step-marker { display: grid; flex: 0 0 38px; width: 38px; height: 38px; place-items: center; border: 1px solid rgba(101, 176, 208, .6); border-radius: 50%; color: #a6c4d9; background: rgba(3, 25, 40, .8); font-size: 12px; font-weight: 900; }.step-copy { display: grid; min-width: 0; gap: 4px; }.step-copy b { overflow: hidden; font-size: 14px; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; }.step-copy small { color: #66b6d4; font-size: 10px; font-weight: 800; letter-spacing: .06em; }
.step-tab.active { color: white; border-color: #53d8fb; background: linear-gradient(135deg, rgba(7, 156, 207, .85), rgba(13, 105, 161, .92)); box-shadow: inset 0 0 24px rgba(77, 213, 255, .22); }.step-tab.active .step-marker { border-color: #72e8ff; color: #fff; background: linear-gradient(135deg, #08b7e8, #1375d0); box-shadow: 0 0 0 5px rgba(25, 179, 237, .14), 0 0 18px rgba(46, 208, 255, .58); }.step-tab.complete { color: #dffbff; }.step-tab.complete .step-marker { border-color: rgba(84, 230, 221, .85); color: #062b37; background: #6ef0e1; }
.intro-card, .analysis-panel { padding: 24px; border: 1px solid rgba(82, 174, 226, .42); border-radius: 26px; background: rgba(0, 25, 42, .64); }
.intro-card.compact { margin-bottom: 20px; }
.weights-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; margin-top: 20px; }
.weight-card, .score-section { padding: 20px; border: 1px solid rgba(71, 167, 209, .28); border-radius: 14px; background: rgba(4, 23, 36, .68); }
.card-title, .section-heading { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-bottom: 16px; }
.card-title span, .section-heading span { color: #52cef6; font-size: 13px; font-weight: 700; letter-spacing: .07em; }
.card-title h3, .section-heading h3 { margin: 4px 0 0; color: #fff; font-size: 20px; }
.card-title strong { color: #6de4ff; font-size: 25px; }.card-title strong.danger { color: #ff8888; }
.training-table { width: 100%; border-collapse: collapse; table-layout: fixed; overflow: hidden; border: 1px solid #2b5c73; }
.training-table th { padding: 13px 10px; color: #effbff; background: #0c91c8; font-weight: 700; }
.training-table td { padding: 10px; border: 1px solid #2b5c73; color: #cfe2ec; vertical-align: middle; text-align: center; }
.training-table tbody tr:nth-child(even) { background: rgba(15, 48, 66, .42); }
.training-table :deep(.el-input__wrapper), .training-table :deep(.el-textarea__inner), .training-table :deep(.el-select__wrapper) { background: rgba(2, 15, 24, .7); box-shadow: 0 0 0 1px #315f75 inset; }
.training-table :deep(.el-input__inner), .training-table :deep(.el-textarea__inner), .training-table :deep(.el-select__selected-item), .training-table :deep(.el-input-number__decrease), .training-table :deep(.el-input-number__increase) { color: #e7f6fd; }
.training-table :deep(.el-textarea__inner) { min-height: 70px !important; resize: vertical; }
.page-actions { display: flex; justify-content: center; padding-top: 28px; }.split-actions { justify-content: space-between; }
.page-actions :deep(.el-button--primary), .row-actions :deep(.el-button) { border-color: #18b6ec; background: #0e96ca; }
.score-section { margin-bottom: 20px; }.section-heading small { color: #93adbd; }.table-scroll { overflow-x: auto; }
.score-table { min-width: 960px; }.score-table th:first-child, .score-table td:first-child { width: 175px; }.score-table :deep(.el-input-number) { width: 100%; }.total-score { color: #6de4ff !important; font-size: 17px; font-weight: 700; }
.analysis-panel + .analysis-panel { margin-top: 20px; }.ranking-table { min-width: 940px; }.ranking-table th:first-child { width: 18%; }.ranking-table th:nth-child(2), .ranking-table th:nth-child(3) { width: 19%; }.scene-name { color: #71dcfb !important; font-weight: 700; }.rank-result { display: flex; flex-direction: column; gap: 5px; }.rank-result strong { color: #f3fbff; }.rank-result span { color: #62cef1; font-size: 13px; }
.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>