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/product/after-sales-service.vue

1410 lines
53 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="after-sales-page student-training-shell">
<main class="after-sales-core">
<section class="task-header">
<div class="task-head-main">
<h1 class="task-title">{{ taskTitle }}</h1>
<p class="task-brief">
<span class="task-brief-text">{{ taskBackground }}</span>
<button type="button" class="task-brief-toggle" @click="briefOpen = !briefOpen">
{{ briefOpen ? "收起" : "展开目标与要求" }}
</button>
<span v-if="briefOpen" class="task-brief-more">
<b>实训目标</b>
<ul><li v-for="goal in taskGoals" :key="goal">{{ goal }}</li></ul>
<b>实训要求</b>
<p>{{ taskRequirement }}</p>
</span>
</p>
</div>
<div class="after-sales-toolbar">
<TrainingMaterialButton
:material-name="taskConfig?.materialName"
:material-url="taskConfig?.materialUrl"
:training-data-name="taskConfig?.trainingDataName"
:training-data-url="taskConfig?.trainingDataUrl"
:guide-manual-name="taskConfig?.guideManualName"
:guide-manual-url="taskConfig?.guideManualUrl"
:ai-manual-name="taskConfig?.aiManualName"
:ai-manual-url="taskConfig?.aiManualUrl"
/>
<button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon>导出实训成果</button>
</div>
</section>
<section class="after-sales-workbench">
<div class="workbench-head">
<div>
<p class="section-eyebrow">After-sales diagnostic routing</p>
<h2>售后故障类型分类</h2>
</div>
</div>
<nav class="step-indicator" aria-label="售后服务流程设计步骤">
<template v-for="(step, index) in trainingSteps" :key="`${step}-${index}`">
<button type="button" :class="['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]" @click="switchStep(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">{{ currentStep === index + 1 ? '当前步骤' : '点击切换' }} →</span></span>
</button>
<span v-if="index < trainingSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section v-if="currentStep === 1" class="step-card">
<div class="step-title-row">
<div><p class="section-eyebrow">Step 01</p><h3>{{ trainingSteps[0] }}</h3></div>
<span class="step-chip">拖拽分类判题</span>
</div>
<p class="step-desc">互联网产品的售后服务除硬件维修外,还需处理配网、云端连接与软件控制等问题。请将右侧被打乱的故障现象拖入对应的故障类型;完成全部分类并判题正确后才能进入下一步。</p>
<div class="classification-workspace">
<section class="fault-lanes" aria-label="故障类型分类区">
<article
v-for="category in CATEGORIES"
:key="category.id"
class="fault-lane"
:class="{ 'fault-lane--over': dragOverCategory === category.id }"
@dragover.prevent="dragOverCategory = category.id"
@dragleave="dragOverCategory = ''"
@drop.prevent="dropIntoCategory(category.id)"
>
<header class="lane-head">
<div><span class="lane-index">{{ category.index }}</span><h4>{{ category.name }}</h4></div>
<span>{{ faultsByCategory(category.id).length }}/2</span>
</header>
<p>{{ category.description }}</p>
<div class="lane-drop-zone">
<article
v-for="fault in faultsByCategory(category.id)"
:key="fault.id"
class="fault-card"
:class="{ dragging: dragFaultId === fault.id }"
:draggable="!checking"
@dragstart="onDragStart(fault.id)"
@dragend="onDragEnd"
>
<span class="fault-handle" title="拖拽分类">⠿</span>
<p>{{ fault.text }}</p>
<label class="fault-select-label">
<span>分类</span>
<el-select :model-value="assignments[fault.id] || ''" :disabled="checking" size="small" class="fault-select" @update:model-value="(val) => setCategory(fault.id, val)">
<el-option value="" label="待分类" />
<el-option v-for="cat in CATEGORIES" :key="cat.id" :value="cat.id" :label="cat.name" />
</el-select>
</label>
</article>
<span v-if="!faultsByCategory(category.id).length" class="drop-hint">将故障现象拖到这里</span>
</div>
</article>
</section>
<aside class="unassigned-panel" :class="{ 'unassigned-panel--over': dragOverCategory === 'unassigned' }" @dragover.prevent="dragOverCategory = 'unassigned'" @dragleave="dragOverCategory = ''" @drop.prevent="dropIntoCategory('')">
<div class="unassigned-head"><span class="section-eyebrow">待分类故障现象</span><strong>{{ unassignedFaults.length }}</strong></div>
<p>卡片初始顺序已打乱。可直接拖拽,也可用卡片内的下拉框辅助调整。</p>
<div class="unassigned-list">
<article
v-for="fault in unassignedFaults"
:key="fault.id"
class="fault-card"
:class="{ dragging: dragFaultId === fault.id }"
:draggable="!checking"
@dragstart="onDragStart(fault.id)"
@dragend="onDragEnd"
>
<span class="fault-handle" title="拖拽分类">⠿</span>
<p>{{ fault.text }}</p>
<label class="fault-select-label">
<span>分类</span>
<el-select :model-value="assignments[fault.id] || ''" :disabled="checking" size="small" class="fault-select" @update:model-value="(val) => setCategory(fault.id, val)">
<el-option value="" label="待分类" />
<el-option v-for="cat in CATEGORIES" :key="cat.id" :value="cat.id" :label="cat.name" />
</el-select>
</label>
</article>
<span v-if="!unassignedFaults.length" class="unassigned-empty">全部故障现象已完成归类</span>
</div>
</aside>
</div>
<p v-if="checkResult" class="result-message" :class="{ success: checkResult.allCorrect }">{{ checkResult.message || (checkResult.allCorrect ? '分类正确,已解锁下一步。' : `当前 ${checkResult.correctCount}/${checkResult.totalCount} 个分类正确,请继续调整。`) }}</p>
<div class="task-actions">
<button type="button" class="btn-nav btn-submit" :disabled="checking || unassignedFaults.length > 0" @click="checkAndSave">{{ checking ? '判题中…' : '检查分类并保存' }}</button>
<button type="button" class="btn-nav" :disabled="checking" @click="resetFaults"><el-icon><RefreshLeft /></el-icon>重新打乱</button>
<button type="button" class="btn-nav btn-primary" :disabled="!stepOnePassed" @click="goNextStep">进入第 2 步<el-icon><ArrowRight /></el-icon></button>
</div>
</section>
<section v-else-if="currentStep === 2" class="step-card service-design-card">
<div class="step-title-row">
<div><p class="section-eyebrow">Step 02</p><h3>{{ trainingSteps[1] }}</h3></div>
<span class="step-chip">方案设计与规则沉淀</span>
</div>
<p class="step-desc">根据 4 类故障类型,设计标准化的分级处理流程。请结合案例资料中的售后政策和常见故障案例,完成处理流程表。斜体内容为填写示例,可使用"带入示例"快速复制后再按自己的方案修改。</p>
<div class="service-table-wrap">
<table class="service-table">
<thead>
<tr><th>故障类型</th><th>具体场景</th><th>处理流程</th><th>核心规则</th></tr>
</thead>
<tbody>
<tr v-for="row in SERVICE_ROWS" :key="row.id">
<th scope="row"><span class="service-row-index">{{ row.index }}</span>{{ row.type }}</th>
<td><em>{{ row.scene }}</em></td>
<td>
<textarea v-model="serviceAnswers[row.id].process" :aria-label="`${row.type}处理流程`" placeholder="请填写处理流程" @input="clearStepTwoGrade" />
<button v-if="row.processExample" type="button" class="example-button" @click="applyExample(row.id)">带入示例</button>
<em v-if="row.processExample" class="example-copy">示例:{{ row.processExample }}</em>
</td>
<td>
<textarea v-model="serviceAnswers[row.id].rule" :aria-label="`${row.type}核心规则`" placeholder="请填写核心规则" @input="clearStepTwoGrade" />
<em v-if="row.ruleExample" class="example-copy">示例:{{ row.ruleExample }}</em>
</td>
</tr>
</tbody>
</table>
</div>
<p v-if="stepTwoResult" class="result-message" :class="{ success: stepTwoResult.valid }">{{ stepTwoResult.message }}</p>
<div class="task-actions">
<button type="button" class="btn-nav" :disabled="savingStepTwo" @click="switchStep(1)"><el-icon><ArrowLeft /></el-icon>返回第 1 步</button>
<button type="button" class="btn-nav btn-submit" :disabled="savingStepTwo" @click="validateAndSaveStepTwo">{{ savingStepTwo ? '保存中…' : '保存并进入第 3 步' }}<el-icon><ArrowRight /></el-icon></button>
</div>
</section>
<section v-else-if="currentStep === 3" class="step-card policy-design-card">
<div class="step-title-row">
<div><p class="section-eyebrow">Step 03</p><h3>{{ trainingSteps[2] }}</h3></div>
<span class="step-chip">消费者权益与售后承诺</span>
</div>
<p class="step-desc">根据第 2 步的分级处理方案,制定可用于产品说明书和电商详情页的售后政策。斜体文字是填写示例,可复制后按案例信息调整,不参与标准答案判定。</p>
<section class="policy-section" aria-labelledby="policy-framework-title">
<div class="policy-section-heading"><span>(一)</span><h4 id="policy-framework-title">填写售后政策框架表</h4></div>
<p>请结合案例资料中的产品信息、行业法规与竞品售后政策,明确退换货、质保、维修、响应时效和补偿方案。</p>
<div class="service-table-wrap">
<table class="service-table policy-table">
<thead><tr><th>政策维度</th><th>政策内容</th><th>适用条件与说明</th></tr></thead>
<tbody>
<tr v-for="row in POLICY_ROWS" :key="row.id">
<th scope="row"><span class="service-row-index">{{ row.index }}</span>{{ row.dimension }}</th>
<td>
<textarea v-model="policyAnswers.framework[row.id].content" :aria-label="`${row.dimension}政策内容`" placeholder="请填写政策内容" @input="clearStepThreeGrade" />
<button v-if="row.contentExample" type="button" class="example-button" @click="applyPolicyExample(row.id)">带入示例</button>
<em v-if="row.contentExample" class="example-copy">示例:{{ row.contentExample }}</em>
</td>
<td>
<textarea v-model="policyAnswers.framework[row.id].condition" :aria-label="`${row.dimension}适用条件与说明`" placeholder="请填写适用条件与说明" @input="clearStepThreeGrade" />
<em v-if="row.conditionExample" class="example-copy">示例:{{ row.conditionExample }}</em>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="policy-section" aria-labelledby="policy-summary-title">
<div class="policy-section-heading"><span>(二)</span><h4 id="policy-summary-title">填写电商详情页售后信息摘要</h4></div>
<p>将政策核心信息提炼为消费者友好的简明摘要,用于详情页"售后保障"模块展示。</p>
<div class="service-table-wrap">
<table class="service-table policy-summary-table">
<thead><tr><th>售后信息项</th><th>详情页展示内容</th></tr></thead>
<tbody>
<tr v-for="row in POLICY_SUMMARY_ROWS" :key="row.id">
<th scope="row">{{ row.label }}</th>
<td>
<textarea v-model="policyAnswers.summary[row.id]" :aria-label="`${row.label}详情页展示内容`" placeholder="请填写面向消费者的简明承诺" @input="clearStepThreeGrade" />
<button v-if="row.example" type="button" class="example-button" @click="applySummaryExample(row.id)">带入示例</button>
<em v-if="row.example" class="example-copy">示例:{{ row.example }}</em>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<p v-if="stepThreeResult" class="result-message" :class="{ success: stepThreeResult.valid }">{{ stepThreeResult.message }}</p>
<div class="task-actions">
<button type="button" class="btn-nav" :disabled="savingStepThree" @click="switchStep(2)"><el-icon><ArrowLeft /></el-icon>返回第 2 步</button>
<button type="button" class="btn-nav btn-submit" :disabled="savingStepThree" @click="validateAndSaveStepThree">{{ savingStepThree ? '提交中…' : '校验并提交任务' }}<el-icon><ArrowRight /></el-icon></button>
</div>
</section>
<section v-else class="step-card placeholder-card">
<p class="section-eyebrow">Step {{ String(currentStep).padStart(2, '0') }}</p>
<h3>{{ trainingSteps[currentStep - 1] }}</h3>
<p>第 1 步已通过,后续步骤将在对应实训页面继续完成。</p>
<div class="task-actions"><button type="button" class="btn-nav" @click="switchStep(1)"><el-icon><ArrowLeft /></el-icon>返回第 1 步</button></div>
</section>
</section>
</main>
<TrainingAiSidebar
study-tip="先根据故障现象定位排查层级:本地硬件、配网操作、云端连接或需要升级处理的严重故障。"
reference-title="故障分类提示"
reference-text="· 本地问题优先确认设备本体与基础控制<br />· 配网问题聚焦 App、设备发现与连接过程<br />· 云端问题关注远程控制稳定性与掉线<br />· 严重故障需要优先停止使用并升级处理"
:progress-items="progressItems"
/>
</div>
</template>
<script setup>
import { computed, getCurrentInstance, onMounted, ref } from "vue";
import { ArrowLeft, ArrowRight, Download, RefreshLeft, Service } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import { checkAfterSalesServiceStepOne, checkAfterSalesServiceStepTwo, checkAfterSalesServiceStepThree, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingPage } from "@/views/training/studentTrainingPages";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
const TASK_KEY = "after-sales-service";
const { proxy } = getCurrentInstance();
const basePage = getStudentTrainingPage(TASK_KEY);
const taskConfig = ref(null);
const briefOpen = ref(false);
const currentStep = ref(1);
const checking = ref(false);
const dragFaultId = ref("");
const dragOverCategory = ref("");
const assignments = ref({});
const stepOnePassed = ref(false);
const checkResult = ref(null);
const savingStepTwo = ref(false);
const stepTwoPassed = ref(false);
const stepTwoResult = ref(null);
const savingStepThree = ref(false);
const stepThreePassed = ref(false);
const stepThreeResult = ref(null);
const CATEGORIES = [
{ id: "local-hardware-simple", index: "01", name: "本地硬件简单故障", description: "设备本体或基础控制环节的常规问题" },
{ id: "operation-network", index: "02", name: "操作/配网类问题", description: "设备发现、添加和连接配置过程的问题" },
{ id: "cloud-connection", index: "03", name: "云端连接类故障", description: "远程控制、云端通信与在线稳定性问题" },
{ id: "severe-fault", index: "04", name: "严重故障", description: "存在安全风险或需升级处理的设备故障" },
];
const FAULTS = [
{ id: "voice-wake-no-response", text: "用户使用语音唤醒,设备无反应,但手动开关灯正常" },
{ id: "voice-control-cannot-switch", text: "唤醒词识别正常,但语音控制设备开机和关机无法实现" },
{ id: "app-cannot-find-device", text: "配网时 APP 搜索不到设备" },
{ id: "network-setup-timeout", text: "APP 添加设备时提示「配网超时」" },
{ id: "remote-control-intermittent", text: "开关正常,但 APP 远程控制时好时坏" },
{ id: "frequent-disconnection", text: "配网成功但每隔几小时就断连" },
{ id: "indicator-no-response", text: "插电后指示灯不亮,完全无反应" },
{ id: "water-damage-no-response", text: "产品进水后通电无反应" },
];
const SERVICE_ROWS = [
{
id: "operation-network", index: "01", type: "操作/配网类问题", scene: "配网失败、APP 找不到设备、唤醒后连不上网",
processExample: "① 发送《一步配网教程》视频\n② 客服远程指导重置配网\n③ 若 30 分钟内未解决,转技术支持",
ruleExample: "远程解决为主,不产生物流成本",
},
{
id: "cloud-connection", index: "02", type: "云端连接类故障", scene: "配网成功但经常断连、云端识别超时、APP 远程控制失效",
processExample: "① 收集设备日志与网络环境\n② 技术支持排查云端服务与账号状态\n③ 修复后回访确认远程控制恢复",
ruleExample: "优先远程诊断;确认平台侧问题后统一告知处理进度",
},
{
id: "local-hardware-simple", index: "03", type: "本地硬件简单故障", scene: "语音识别正常但无法控制相关功能(质保内)",
processExample: "① 远程确认功能设置与固件版本\n② 指导恢复默认设置或 OTA 升级\n③ 无法恢复时登记检测/换新申请",
ruleExample: "质保期内优先远程修复,符合条件后再安排售后服务",
},
{
id: "severe-fault", index: "04", type: "严重故障", scene: "通电无反应、模组烧坏、进水损坏(质保内)",
processExample: "① 提示立即停止使用并收集故障凭证\n② 售后审核安全风险与质保资格\n③ 安排寄修或换新,并跟踪闭环",
ruleExample: "安全风险优先;质保内符合条件可寄修或换新",
},
{
id: "out-of-warranty", index: "05", type: "超出质保期", scene: "任何故障",
processExample: "① 明确告知超出质保范围\n② 提供付费维修、配件购买或以旧换新方案\n③ 用户确认报价后执行服务",
ruleExample: "收费项目与报价须在服务前确认,避免产生争议",
},
];
const POLICY_ROWS = [
{
id: "return-policy", index: "01", dimension: "退换货政策",
contentExample: "7天无理由退货,15天质量问题免费换新",
conditionExample: "商品完好、配件齐全、包装完整;退款至原支付账户,3-5个工作日到账",
},
{ id: "warranty-policy", index: "02", dimension: "质保政策" },
{ id: "repair-service", index: "03", dimension: "维修服务" },
{ id: "response-sla", index: "04", dimension: "响应时效" },
{ id: "compensation-plan", index: "05", dimension: "补偿方案" },
];
const POLICY_SUMMARY_ROWS = [
{ id: "warranty-period", label: "质保期", example: "整机12个月质保,灯珠24个月质保,7天无理由退货" },
{ id: "response-commitment", label: "响应承诺" },
{ id: "featured-service", label: "特色服务" },
];
const fallbackSteps = ["故障类型分类", "服务分级与处理方案", "售后服务流程设计"];
const trainingSteps = computed(() => {
const configuredSteps = parseTrainingArray(taskConfig.value?.steps, fallbackSteps);
return fallbackSteps.map((fallbackStep, index) => configuredSteps[index] || fallbackStep);
});
const taskTitle = computed(() => taskConfig.value?.taskName || basePage.title || "售后服务流程设计");
const taskBackground = computed(() => taskConfig.value?.background || basePage.background);
const taskRequirement = computed(() => taskConfig.value?.requirements || basePage.task);
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives, basePage.goals));
const orderedFaults = ref(shuffleFaults());
const serviceAnswers = ref(createEmptyServiceAnswers());
const policyAnswers = ref(createEmptyPolicyAnswers());
const unassignedFaults = computed(() => orderedFaults.value.filter((fault) => !assignments.value[fault.id]));
const progressItems = computed(() => trainingSteps.value.map((text, index) => ({ text, status: index + 1 < currentStep.value ? "done" : index + 1 === currentStep.value ? "doing" : "" })));
onMounted(async () => { await loadTaskConfig(); await loadSavedProgress(); });
function shuffleFaults() { return [...FAULTS].sort(() => Math.random() - 0.5); }
function normalizeStep(value) { return Math.min(Math.max(Number(value) || 1, 1), Math.max(trainingSteps.value.length, 1)); }
function faultsByCategory(categoryId) { return orderedFaults.value.filter((fault) => assignments.value[fault.id] === categoryId); }
function onDragStart(faultId) { if (!checking.value) dragFaultId.value = faultId; }
function onDragEnd() { dragFaultId.value = ""; dragOverCategory.value = ""; }
function setCategory(faultId, categoryId) { assignments.value = { ...assignments.value, [faultId]: categoryId || undefined }; clearGrade(); }
function dropIntoCategory(categoryId) { if (!dragFaultId.value || checking.value) return; setCategory(dragFaultId.value, categoryId); onDragEnd(); }
function clearGrade() { stepOnePassed.value = false; checkResult.value = null; }
function resetFaults() { orderedFaults.value = shuffleFaults(); assignments.value = {}; clearGrade(); }
function createEmptyServiceAnswers() { return Object.fromEntries(SERVICE_ROWS.map((row) => [row.id, { process: "", rule: "" }])); }
function createEmptyPolicyAnswers() {
return {
framework: Object.fromEntries(POLICY_ROWS.map((row) => [row.id, { content: "", condition: "" }])),
summary: Object.fromEntries(POLICY_SUMMARY_ROWS.map((row) => [row.id, ""])),
};
}
function buildStepOneAnswer() { return { assignments: assignments.value, orderedFaultIds: orderedFaults.value.map((fault) => fault.id), passed: stepOnePassed.value, checkResult: checkResult.value, updatedAt: new Date().toISOString() }; }
function buildStepTwoAnswer() { return { rows: SERVICE_ROWS.map((row) => ({ id: row.id, process: serviceAnswers.value[row.id]?.process || "", rule: serviceAnswers.value[row.id]?.rule || "" })), passed: stepTwoPassed.value, validationResult: stepTwoResult.value, updatedAt: new Date().toISOString() }; }
function buildStepThreeAnswer() {
return {
policies: POLICY_ROWS.map((row) => ({ id: row.id, content: policyAnswers.value.framework[row.id]?.content || "", condition: policyAnswers.value.framework[row.id]?.condition || "" })),
summaries: POLICY_SUMMARY_ROWS.map((row) => ({ id: row.id, content: policyAnswers.value.summary[row.id] || "" })),
passed: stepThreePassed.value,
validationResult: stepThreeResult.value,
updatedAt: new Date().toISOString(),
};
}
async function persist(saveAction = "SAVE") { await saveStudentTrainingAnswer(TASK_KEY, { saveAction, currentStep: currentStep.value, submitted: saveAction === "SUBMIT", step1Answer: JSON.stringify(buildStepOneAnswer()), step2Answer: JSON.stringify(buildStepTwoAnswer()), step3Answer: JSON.stringify(buildStepThreeAnswer()) }); }
async function loadTaskConfig() { try { const response = await getTrainingTaskByKey(TASK_KEY); taskConfig.value = response?.data || null; currentStep.value = normalizeStep(currentStep.value); } catch (error) { taskConfig.value = null; } }
async function loadSavedProgress() {
try {
const response = await getStudentTrainingAnswer(TASK_KEY); const raw = response?.data?.step1Answer; if (!raw) return;
const saved = JSON.parse(raw); const validIds = new Set(FAULTS.map((fault) => fault.id));
if (Array.isArray(saved?.orderedFaultIds) && saved.orderedFaultIds.length === FAULTS.length && new Set(saved.orderedFaultIds).size === FAULTS.length && saved.orderedFaultIds.every((id) => validIds.has(id))) {
orderedFaults.value = saved.orderedFaultIds.map((id) => FAULTS.find((fault) => fault.id === id));
}
assignments.value = Object.fromEntries(Object.entries(saved?.assignments || {}).filter(([id, categoryId]) => validIds.has(id) && CATEGORIES.some((category) => category.id === categoryId)));
stepOnePassed.value = Boolean(saved?.passed); checkResult.value = saved?.checkResult || null;
const rawStepTwo = response?.data?.step2Answer; if (rawStepTwo) {
const savedStepTwo = JSON.parse(rawStepTwo); const validServiceIds = new Set(SERVICE_ROWS.map((row) => row.id));
serviceAnswers.value = Object.fromEntries(SERVICE_ROWS.map((row) => {
const savedRow = Array.isArray(savedStepTwo?.rows) ? savedStepTwo.rows.find((item) => item?.id === row.id) : null;
return [row.id, { process: validServiceIds.has(row.id) ? String(savedRow?.process || "") : "", rule: validServiceIds.has(row.id) ? String(savedRow?.rule || "") : "" }];
}));
stepTwoPassed.value = Boolean(savedStepTwo?.passed); stepTwoResult.value = savedStepTwo?.validationResult || null;
}
const rawStepThree = response?.data?.step3Answer; if (rawStepThree) {
const savedStepThree = JSON.parse(rawStepThree);
policyAnswers.value = {
framework: Object.fromEntries(POLICY_ROWS.map((row) => {
const savedPolicy = Array.isArray(savedStepThree?.policies) ? savedStepThree.policies.find((item) => item?.id === row.id) : null;
return [row.id, { content: String(savedPolicy?.content || ""), condition: String(savedPolicy?.condition || "") }];
})),
summary: Object.fromEntries(POLICY_SUMMARY_ROWS.map((row) => {
const savedSummary = Array.isArray(savedStepThree?.summaries) ? savedStepThree.summaries.find((item) => item?.id === row.id) : null;
return [row.id, String(savedSummary?.content || "")];
})),
};
stepThreePassed.value = Boolean(savedStepThree?.passed); stepThreeResult.value = savedStepThree?.validationResult || null;
}
currentStep.value = normalizeStep(response?.data?.currentStep || saved?.currentStep);
} catch (error) { /* Keep a fresh shuffled exercise available when historical progress cannot be read. */ }
}
async function checkAndSave() {
if (checking.value || unassignedFaults.value.length) return;
checking.value = true;
try {
const items = orderedFaults.value.map((fault) => ({ faultId: fault.id, categoryId: assignments.value[fault.id] }));
const response = await checkAfterSalesServiceStepOne(items);
checkResult.value = response?.data || null; stepOnePassed.value = Boolean(checkResult.value?.allCorrect);
await persist("SAVE");
if (stepOnePassed.value) proxy?.$modal?.msgSuccess("故障分类正确,已保存并解锁下一步"); else proxy?.$modal?.msgWarning("故障分类尚未完全正确,请继续调整");
} catch (error) { proxy?.$modal?.msgError(error?.message || "判题失败,请稍后重试"); } finally { checking.value = false; }
}
async function goNextStep() { if (!stepOnePassed.value) return; currentStep.value = normalizeStep(currentStep.value + 1); await persist("SAVE"); }
function applyExample(rowId) { const row = SERVICE_ROWS.find((item) => item.id === rowId); if (!row) return; serviceAnswers.value = { ...serviceAnswers.value, [rowId]: { process: row.processExample || "", rule: row.ruleExample || "" } }; clearStepTwoGrade(); }
function clearStepTwoGrade() { stepTwoPassed.value = false; stepTwoResult.value = null; }
async function validateAndSaveStepTwo() {
if (savingStepTwo.value) return; savingStepTwo.value = true;
try {
const rows = buildStepTwoAnswer().rows; const response = await checkAfterSalesServiceStepTwo({ rows });
stepTwoResult.value = response?.data || null; stepTwoPassed.value = Boolean(stepTwoResult.value?.valid);
if (!stepTwoPassed.value) { proxy?.$modal?.msgWarning(stepTwoResult.value?.message || "请完成五类故障的处理流程和核心规则"); return; }
currentStep.value = normalizeStep(3); await persist("SAVE"); proxy?.$modal?.msgSuccess("服务分级处理方案已保存,已进入第 3 步");
} catch (error) { proxy?.$modal?.msgError(error?.message || "保存失败,请稍后重试"); } finally { savingStepTwo.value = false; }
}
function clearStepThreeGrade() { stepThreePassed.value = false; stepThreeResult.value = null; }
function applyPolicyExample(rowId) {
const row = POLICY_ROWS.find((item) => item.id === rowId); if (!row) return;
policyAnswers.value = { ...policyAnswers.value, framework: { ...policyAnswers.value.framework, [rowId]: { content: row.contentExample || "", condition: row.conditionExample || "" } } };
clearStepThreeGrade();
}
function applySummaryExample(rowId) {
const row = POLICY_SUMMARY_ROWS.find((item) => item.id === rowId); if (!row) return;
policyAnswers.value = { ...policyAnswers.value, summary: { ...policyAnswers.value.summary, [rowId]: row.example || "" } };
clearStepThreeGrade();
}
async function validateAndSaveStepThree() {
if (savingStepThree.value) return; savingStepThree.value = true;
try {
const answer = buildStepThreeAnswer(); const response = await checkAfterSalesServiceStepThree(answer);
stepThreeResult.value = response?.data || null; stepThreePassed.value = Boolean(stepThreeResult.value?.valid);
if (!stepThreePassed.value) { proxy?.$modal?.msgWarning(stepThreeResult.value?.message || "请完成售后政策框架与详情页摘要"); return; }
currentStep.value = normalizeStep(3); await persist("SUBMIT"); proxy?.$modal?.msgSuccess("售后政策已校验通过,任务提交成功");
} catch (error) { proxy?.$modal?.msgError(error?.message || "保存失败,请稍后重试"); } finally { savingStepThree.value = false; }
}
function switchStep(step) {
if (step > 1 && !stepOnePassed.value) { proxy?.$modal?.msgWarning("请先完成故障分类并判题通过"); return; }
if (step > 2 && !stepTwoPassed.value) { proxy?.$modal?.msgWarning("请先完成服务分级与处理方案"); return; }
if (step > 3 && !stepThreePassed.value) { proxy?.$modal?.msgWarning("请先完成售后政策设计"); return; }
currentStep.value = normalizeStep(step);
}
function exportOutcome() { const blob = new Blob([JSON.stringify({ currentStep: currentStep.value, step1: buildStepOneAnswer(), step2: buildStepTwoAnswer(), step3: buildStepThreeAnswer() }, null, 2)], { type: "application/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${taskTitle.value}-实训成果.json`; link.click(); URL.revokeObjectURL(url); }
</script>
<style scoped lang="scss">
/* ===== 页面级令牌(教育蓝极简主题) ===== */
.after-sales-page {
/* 主色:教育蓝 */
--student-accent: #2563eb;
--student-accent-deep: #1d4ed8;
--student-accent-light: #3b82f6;
--student-accent-soft: #eff6ff;
/* 语义色 */
--student-danger: #ef4444;
--student-success: #2ba471;
/* 三层次标识(仅小色条,不做行底色) */
--student-layer-1: #1d4ed8;
--student-layer-2: #3b82f6;
--student-layer-3: #93c5fd;
/* 结构色 */
--student-panel-border: #e5e7eb;
--student-card-bg: #ffffff;
--student-panel-bg: #fafbfc;
--student-text: #1f2937;
--student-text-secondary: #6b7280;
/* 字号梯度:紧凑 */
--student-fs-display: 17px;
--student-fs-h1: 15px;
--student-fs-h2: 14px;
--student-fs-h3: 13px;
--student-fs-body: 13px;
--student-fs-body-sm: 12px;
--student-fs-caption: 11px;
--student-fs-button: 13px;
--student-fs-button-lg: 13px;
--student-fs-data: 14px;
min-width: 0;
color: var(--student-text);
background: #ffffff;
}
/* 主核心容器:透明化,无蓝边无蓝底 */
.after-sales-core {
display: grid;
gap: 10px;
align-content: start;
min-width: 0;
border: 0;
background: transparent;
box-shadow: none;
}
/* ===== task-header:白卡 + 左侧教育蓝竖条 ===== */
.task-header {
position: relative;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin: 0;
padding: 16px 16px 16px 20px;
border: 1px solid var(--student-panel-border);
border-radius: 12px;
background: #ffffff;
overflow: hidden;
&::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 4px;
background: var(--student-accent);
}
}
.task-head-main {
display: grid;
justify-items: start;
gap: 8px;
min-width: 0;
flex: 1 1 auto;
}
.task-title {
margin: 0;
color: var(--student-text);
font-size: var(--student-fs-display);
font-weight: 700;
line-height: 1.35;
}
.task-brief {
margin: 0;
color: var(--student-text-secondary);
font-size: 12px;
line-height: 1.6;
}
.task-brief-text {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.task-brief-toggle {
padding: 0;
border: 0;
color: var(--student-accent-deep);
background: transparent;
font: inherit;
font-weight: 700;
cursor: pointer;
&:hover { text-decoration: underline; color: var(--student-accent-deep); }
}
.task-brief-more {
display: grid;
gap: 6px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--student-panel-border);
b { color: var(--student-text); font-size: 12px; }
ul { display: grid; gap: 4px; margin: 0; padding-left: 18px; }
p { margin: 0; }
}
.after-sales-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
flex: 0 0 auto;
justify-content: flex-end;
max-width: 100%;
}
/* ===== workbench:透明化 ===== */
.after-sales-workbench {
display: grid;
gap: 10px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.workbench-head {
display: flex;
align-items: flex-start;
gap: 16px;
margin: 0;
padding: 0;
border-bottom: 0;
}
.section-eyebrow {
margin: 0;
color: var(--student-accent);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.workbench-head h2 {
margin: 4px 0 0;
color: var(--student-text);
font-size: var(--student-fs-h1);
font-weight: 700;
line-height: 1.35;
}
/* ===== step-card:透明化 ===== */
.step-card {
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
.step-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.step-title-row h3 {
margin: 4px 0 0;
color: var(--student-text);
font-size: var(--student-fs-h1);
font-weight: 700;
line-height: 1.35;
}
.step-chip {
border: 1px solid #bfdbfe;
border-radius: 999px;
padding: 4px 10px;
color: var(--student-accent);
background: var(--student-accent-soft);
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.step-desc {
margin: 14px 0;
color: var(--student-text-secondary);
font-size: var(--student-fs-body);
line-height: 1.6;
}
/* ===== 按钮四级层级(教育蓝极简) ===== */
.outline-action,
.btn-nav {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 36px;
height: 36px;
padding: 0 14px;
border: 1px solid rgba(37, 99, 235, 0.35);
border-radius: 8px;
color: var(--student-accent);
background: #ffffff;
font-size: var(--student-fs-button);
font-weight: 700;
font-family: inherit;
cursor: pointer;
text-shadow: none;
transition: border-color 0.16s ease, background 0.16s ease,
color 0.16s ease, transform 0.16s ease,
box-shadow 0.16s ease;
}
.outline-action:hover:not(:disabled),
.btn-nav:hover:not(:disabled) {
border-color: var(--student-accent);
background: var(--student-accent-soft);
color: var(--student-accent);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
}
.btn-nav.btn-primary,
.btn-nav.btn-submit {
border-color: var(--student-accent);
color: #ffffff;
background: var(--student-accent);
font-weight: 700;
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28);
}
.btn-nav.btn-primary:hover:not(:disabled),
.btn-nav.btn-submit:hover:not(:disabled) {
border-color: var(--student-accent-deep);
background: var(--student-accent-deep);
color: #ffffff;
transform: translateY(-1px);
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32);
}
.btn-save {
border: 1px solid #e5e7eb;
color: #374151;
background: #f3f4f6;
box-shadow: none;
}
.btn-save:hover:not(:disabled) {
border-color: #d1d5db;
background: #e9ecef;
color: #374151;
transform: none;
box-shadow: none;
}
button:disabled {
cursor: not-allowed;
opacity: 0.5;
transform: none;
box-shadow: none;
}
.task-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
/* ===== Step 01:拖拽分类区 ===== */
.classification-workspace {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(260px, 0.45fr);
gap: 16px;
align-items: start;
}
.fault-lanes {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.fault-lane,
.unassigned-panel {
border: 1px solid var(--student-panel-border);
border-radius: 12px;
background: #ffffff;
transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
}
.fault-lane {
min-height: 228px;
padding: 14px;
}
.fault-lane--over,
.unassigned-panel--over {
border-color: var(--student-accent);
background: var(--student-accent-soft);
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.08);
}
.lane-head,
.unassigned-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.lane-head > div {
display: flex;
align-items: center;
gap: 9px;
}
.lane-index {
display: grid;
place-items: center;
width: 26px;
height: 26px;
border: 1px solid var(--student-panel-border);
border-radius: 6px;
color: var(--student-accent);
background: var(--student-accent-soft);
font-size: 11px;
font-weight: 700;
font-family: ui-monospace, "SF Mono", Consolas, monospace;
}
.lane-head h4 {
margin: 0;
color: var(--student-text);
font-size: 14px;
font-weight: 700;
}
.lane-head > span {
color: var(--student-text-secondary);
font-size: 12px;
font-weight: 700;
}
.fault-lane > p,
.unassigned-panel > p {
min-height: 0;
margin: 8px 0;
color: var(--student-text-secondary);
font-size: 12px;
line-height: 1.55;
}
.lane-drop-zone,
.unassigned-list {
display: grid;
gap: 8px;
}
.drop-hint,
.unassigned-empty {
display: grid;
place-items: center;
min-height: 90px;
border: 1px dashed var(--student-panel-border);
border-radius: 10px;
color: var(--student-text-secondary);
font-size: 12px;
text-align: center;
}
.unassigned-panel {
position: sticky;
top: 18px;
padding: 14px;
}
.unassigned-head strong {
display: grid;
place-items: center;
width: 24px;
height: 24px;
border-radius: 6px;
color: #ffffff;
background: var(--student-accent);
font-size: 12px;
font-weight: 700;
}
.unassigned-list {
max-height: 560px;
overflow-y: auto;
padding-right: 2px;
}
/* fault-card(由 defineComponent 渲染,scoped 选择器加 [data-v-xxx] 后仍能命中) */
.fault-card {
display: grid;
grid-template-columns: 18px minmax(0, 1fr);
align-items: start;
gap: 8px;
padding: 9px 10px;
border: 1px solid var(--student-panel-border);
border-radius: 10px;
background: #ffffff;
cursor: grab;
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
}
.fault-card:hover,
.fault-card.dragging {
border-color: var(--student-accent);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.08);
transform: translateY(-1px);
color: var(--student-accent);
}
.fault-handle {
color: var(--student-accent);
font-size: 16px;
line-height: 1;
}
.fault-card p {
grid-column: 2;
margin: 0;
color: var(--student-text);
font-size: 13px;
line-height: 1.55;
}
.fault-select-label {
grid-column: 2;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
color: var(--student-text-secondary);
font-size: 11px;
white-space: nowrap;
}
.fault-select-label select {
width: 126px;
max-width: none;
flex: 0 0 126px;
padding: 4px 22px 4px 7px;
border: 1px solid var(--student-panel-border);
border-radius: 6px;
color: var(--student-text);
background: #ffffff;
font: inherit;
font-size: 11px;
}
/* ===== 结果反馈 ===== */
.result-message {
margin: 16px 0 0;
padding: 10px 14px;
border: 1px solid rgba(180, 83, 9, 0.3);
border-radius: 10px;
color: #92400e;
background: rgba(245, 158, 11, 0.08);
font-size: 12px;
}
.result-message.success {
border-color: rgba(74, 157, 110, 0.35);
color: #065f46;
background: rgba(74, 157, 110, 0.08);
}
/* ===== 表格(Step 02 / Step 03) ===== */
.service-table-wrap {
overflow-x: auto;
}
.service-table {
width: 100%;
min-width: 990px;
border-collapse: collapse;
table-layout: fixed;
}
.service-table th,
.service-table td {
padding: 12px 14px;
border-right: 1px solid var(--student-panel-border);
border-bottom: 1px solid var(--student-panel-border);
vertical-align: top;
}
.service-table tr:last-child th,
.service-table tr:last-child td { border-bottom: 0; }
.service-table th:last-child,
.service-table td:last-child { border-right: 0; }
.service-table thead th {
padding: 12px 14px;
color: var(--student-text);
background: #fafbfc;
font-size: var(--student-fs-body-sm);
font-weight: 800;
text-align: left;
}
.service-table tbody th {
width: 15%;
vertical-align: middle;
color: var(--student-text);
background: transparent;
font-size: var(--student-fs-body);
text-align: left;
font-weight: 700;
}
.service-table tbody td {
width: 28.33%;
color: var(--student-text);
font-size: var(--student-fs-body);
line-height: 1.6;
}
.service-row-index {
display: inline-block;
margin-right: 6px;
padding: 1px 6px;
border: 1px solid var(--student-panel-border);
border-radius: 4px;
color: var(--student-accent);
background: var(--student-accent-soft);
font-size: 11px;
font-weight: 700;
font-family: ui-monospace, "SF Mono", Consolas, monospace;
}
.service-table em {
color: var(--student-text-secondary);
font-style: italic;
font-size: 12px;
}
.service-table textarea {
display: block;
box-sizing: border-box;
width: 100%;
min-height: 80px;
padding: 8px 10px;
border: 1px solid var(--student-panel-border);
border-radius: 6px;
outline: none;
resize: vertical;
color: var(--student-text);
background: transparent;
font: inherit;
font-size: var(--student-fs-body);
line-height: 1.6;
transition: border-color 0.16s ease;
}
.service-table textarea::placeholder {
color: var(--student-text-secondary);
}
.service-table textarea:focus {
outline: 2px solid rgba(37, 99, 235, 0.2);
outline-offset: -2px;
border-color: var(--student-accent);
}
.example-button {
margin-top: 8px;
padding: 3px 10px;
border: 1px solid rgba(37, 99, 235, 0.35);
border-radius: 6px;
color: var(--student-accent);
background: #ffffff;
font: inherit;
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease;
}
.example-button:hover {
border-color: var(--student-accent);
background: var(--student-accent-soft);
}
.example-copy {
display: block;
margin-top: 6px;
color: var(--student-text-secondary);
font-size: 11px;
line-height: 1.5;
white-space: pre-line;
}
/* ===== Step 03:政策设计 ===== */
.policy-design-card {
display: grid;
gap: 20px;
}
.policy-section {
display: grid;
gap: 8px;
}
.policy-section-heading {
display: flex;
align-items: center;
gap: 9px;
}
.policy-section-heading span {
display: grid;
place-items: center;
width: 26px;
height: 26px;
border: 1px solid var(--student-panel-border);
border-radius: 6px;
color: var(--student-accent);
background: var(--student-accent-soft);
font-size: 12px;
font-weight: 700;
}
.policy-section-heading h4 {
margin: 0;
color: var(--student-text);
font-size: var(--student-fs-h2);
font-weight: 700;
}
.policy-section > p {
margin: 0;
color: var(--student-text-secondary);
font-size: var(--student-fs-body);
line-height: 1.6;
}
.policy-table { min-width: 1040px; }
.policy-table tbody th { width: 18%; }
.policy-table tbody td { width: 41%; }
.policy-summary-table { min-width: 760px; }
.policy-summary-table tbody th { width: 20%; }
.policy-summary-table tbody td { width: 80%; }
.policy-summary-table textarea { min-height: 70px; }
/* ===== placeholder-card ===== */
.placeholder-card {
min-height: 200px;
}
.placeholder-card > p:not(.section-eyebrow) {
margin: 12px 0;
color: var(--student-text-secondary);
line-height: 1.6;
}
/* ===== 响应式 ===== */
@media (max-width: 1180px) {
.after-sales-page { padding: 16px; }
}
@media (max-width: 850px) {
.classification-workspace { grid-template-columns: 1fr; }
.unassigned-panel { position: static; }
.fault-lanes { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.after-sales-page { padding: 12px; }
.task-header { flex-direction: column; align-items: flex-start; }
.after-sales-toolbar { width: 100%; justify-content: flex-start; }
.fault-lanes { grid-template-columns: 1fr; }
}
</style>
<style lang="scss">
/* ============================================================
* 步骤导航:分段连接胶囊(教育蓝主题)
* 覆盖全局 !important 规则
* ============================================================ */
#app .studentLayout section.app-main:has(.after-sales-page) .step-indicator {
display: flex !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: center !important;
gap: 0 !important;
width: 100% !important;
margin: 4px 0 16px !important;
padding: 0 !important;
border: none !important;
border-radius: 0 !important;
background: transparent !important;
box-shadow: none !important;
.step-connector { display: none !important; }
.step-tab {
flex: 1 1 0 !important;
max-width: 240px !important;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: center !important;
gap: 8px !important;
min-width: 0 !important;
height: auto !important;
min-height: 40px !important;
margin: 0 !important;
padding: 8px 14px !important;
border: 1px solid #e5e7eb !important;
border-right: none !important;
border-radius: 0 !important;
background: #ffffff !important;
box-shadow: none !important;
color: #6b7280 !important;
font-size: 13px !important;
font-weight: 600 !important;
line-height: 1.3 !important;
white-space: normal !important;
text-align: center !important;
cursor: pointer;
transition: all 0.16s ease !important;
&:first-child {
border-radius: 8px 0 0 8px !important;
}
&:last-child {
border-radius: 0 8px 8px 0 !important;
border-right: 1px solid #e5e7eb !important;
}
/* 编号小徽章 */
.step-marker {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
flex: 0 0 auto !important;
min-width: 22px !important;
height: 22px !important;
padding: 0 5px !important;
border: none !important;
border-radius: 6px !important;
background: #f3f4f6 !important;
color: #6b7280 !important;
font-family: ui-monospace, "SF Mono", Consolas, monospace !important;
font-size: 12px !important;
font-weight: 700 !important;
line-height: 1 !important;
transition: all 0.16s ease !important;
}
.step-copy {
display: flex !important;
flex-direction: column !important;
gap: 0 !important;
min-width: 0 !important;
}
.step-name {
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: normal !important;
line-height: 1.3 !important;
color: inherit !important;
}
.step-action { display: none !important; }
/* 未到达 hover */
&:hover:not(.active):not(.complete) {
border-color: #2563eb !important;
background: #eff6ff !important;
color: #2563eb !important;
z-index: 1 !important;
.step-marker { background: #2563eb !important; color: #ffffff !important; }
}
/* 已完成 */
&.complete:not(.active) {
border-color: #e5e7eb !important;
color: #374151 !important;
.step-marker { background: rgba(43, 164, 113, 0.12) !important; color: #16915f !important; }
&:hover {
border-color: #2ba471 !important;
background: #f0fdf6 !important;
z-index: 1 !important;
.step-marker { background: #2ba471 !important; color: #ffffff !important; }
}
}
/* 当前步骤:教育蓝实底 */
&.active {
border-color: #2563eb !important;
background: #2563eb !important;
color: #ffffff !important;
font-weight: 700 !important;
z-index: 2 !important;
.step-marker { background: #ffffff !important; color: #2563eb !important; }
&:hover {
background: #1d4ed8 !important;
border-color: #1d4ed8 !important;
}
}
}
}
/* ===== 窄屏 900px:横向滑动 ===== */
@media (max-width: 900px) {
#app .studentLayout section.app-main:has(.after-sales-page) .step-indicator {
justify-content: flex-start !important;
gap: 8px !important;
overflow-x: auto !important;
}
#app .studentLayout section.app-main:has(.after-sales-page) .step-indicator .step-tab {
flex: 0 0 140px !important;
max-width: none !important;
}
}
/* ============================================================
* 共用组件换肤:TrainingMaterialButton 资料按钮 → 教育蓝
* ============================================================ */
.after-sales-page .training-material-button.el-button {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 6px !important;
min-height: 34px !important;
height: 34px !important;
padding: 0 14px !important;
border: 1px solid rgba(37, 99, 235, 0.35) !important;
border-radius: 8px !important;
color: #2563eb !important;
background: #ffffff !important;
font-size: 13px !important;
font-weight: 600 !important;
box-shadow: none !important;
transition:
border-color 0.16s ease,
background 0.16s ease,
color 0.16s ease,
box-shadow 0.16s ease !important;
.el-icon {
font-size: 14px !important;
color: inherit !important;
}
&:hover,
&:focus {
border-color: #2563eb !important;
color: #2563eb !important;
background: #eff6ff !important;
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
}
}
/* ============================================================
* 顶部导出按钮(outline-action)→ 教育蓝描边
* ============================================================ */
#app .studentLayout section.app-main:has(.after-sales-page) .outline-action {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 6px !important;
min-height: 34px !important;
height: 34px !important;
padding: 0 14px !important;
border: 1px solid rgba(37, 99, 235, 0.35) !important;
border-radius: 8px !important;
color: #2563eb !important;
background: #ffffff !important;
font-size: 13px !important;
font-weight: 600 !important;
box-shadow: none !important;
transition:
border-color 0.16s ease,
background 0.16s ease,
color 0.16s ease,
box-shadow 0.16s ease !important;
.el-icon {
font-size: 14px !important;
color: inherit !important;
}
&:hover {
border-color: #2563eb !important;
color: #2563eb !important;
background: #eff6ff !important;
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
transform: translateY(-1px) !important;
}
}
/* ===== 顶部工具栏间距 ===== */
.after-sales-page .training-task-toolbar,
.after-sales-page .after-sales-toolbar {
gap: 8px;
}
</style>