|
|
<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>
|
|
|
</section>
|
|
|
|
|
|
<section class="task-action-strip">
|
|
|
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="validateAndSaveStepThree"
|
|
|
:on-export="exportOutcome" />
|
|
|
</section>
|
|
|
|
|
|
<section class="after-sales-workbench">
|
|
|
<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">互联网产品的售后服务与传统产品有显著区别。传统产品售后主要集中在硬件故障维修,而互联网产品特别是智能化的产品,还需要处理联网问题、云端连接故障等软件层面的售后需求,且可以通过远程诊断、OTA升级等方式降低售后成本。故障类型分类是售后服务流程设计的第一步,产品团队收集了售后典型场景,需要进行分类。请将右侧被打乱的故障现象拖入对应的故障类型;完成全部分类并判题正确后才能进入下一步。
|
|
|
</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" :disabled="checking" @click="resetFaults"><el-icon>
|
|
|
<RefreshLeft />
|
|
|
</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>
|
|
|
</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">根据步骤二的分级处理流程设计,制定面向消费者的完整售后政策。售后政策是消费者权益的保障承诺,需覆盖退换货、质保、维修服务、响应时效、补偿方案等核心维度,可直接用于产品说明书附录及电商平台详情页“售后保障”模块。</p>
|
|
|
|
|
|
<section class="policy-section" aria-labelledby="policy-framework-title">
|
|
|
<div class="section-heading"><span>01</span>
|
|
|
<div>
|
|
|
<h3 id="policy-framework-title">填写售后政策框架表</h3>
|
|
|
<p>请结合实训资料中的产品信息、行业法规(如《消费者权益保护法》《网络购买商品七日无理由退货暂行办法》)及竞品售后政策参考,完成以下售后政策框架表。</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<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="section-heading"><span>02</span>
|
|
|
<div>
|
|
|
<h3 id="policy-summary-title">填写电商详情页售后信息摘要</h3>
|
|
|
<p>将售后政策框架表中的核心信息提炼为消费者友好的简明摘要,用于电商平台详情页“售后保障”模块展示。要求语言简洁、重点突出,消费者3秒内可获取关键售后信息。</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<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>
|
|
|
</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>
|
|
|
</section>
|
|
|
|
|
|
<div class="step-navigation">
|
|
|
<div class="step-nav-left">
|
|
|
<button v-if="currentStep > 1" type="button" class="btn-nav btn-ghost" :disabled="saving"
|
|
|
@click="previousStep">
|
|
|
<el-icon>
|
|
|
<ArrowLeft />
|
|
|
</el-icon>
|
|
|
上一步
|
|
|
</button>
|
|
|
<button type="button" class="btn-nav btn-save" :disabled="saving" @click="saveCurrentProgress">
|
|
|
<el-icon>
|
|
|
<CircleCheck />
|
|
|
</el-icon>
|
|
|
保存
|
|
|
</button>
|
|
|
</div>
|
|
|
<div class="step-nav-right">
|
|
|
<span class="step-progress">Step {{ String(currentStep).padStart(2, "0") }} / {{
|
|
|
String(trainingSteps.length).padStart(2, "0") }}</span>
|
|
|
<button v-if="currentStep < trainingSteps.length" type="button" class="btn-nav btn-primary"
|
|
|
:disabled="saving" @click="nextStep">
|
|
|
下一步
|
|
|
<el-icon>
|
|
|
<ArrowRight />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</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, CircleCheck, RefreshLeft, Service } from "@element-plus/icons-vue"
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"
|
|
|
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.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(true)
|
|
|
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 saving = computed(() => checking.value || savingStepTwo.value || savingStepThree.value)
|
|
|
|
|
|
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 saveCurrentProgress() {
|
|
|
if (currentStep.value === 1) {
|
|
|
if (unassignedFaults.value.length) { proxy?.$modal?.msgWarning("请先将全部故障现象完成归类"); return }
|
|
|
return checkAndSave()
|
|
|
}
|
|
|
if (currentStep.value === 2) return validateAndSaveStepTwo()
|
|
|
return validateAndSaveStepThree()
|
|
|
}
|
|
|
function previousStep() { if (currentStep.value > 1) currentStep.value = normalizeStep(currentStep.value - 1) }
|
|
|
async function nextStep() {
|
|
|
if (currentStep.value >= trainingSteps.value.length) return
|
|
|
if (currentStep.value === 1) {
|
|
|
if (!stepOnePassed.value) { proxy?.$modal?.msgWarning("请先完成故障分类并判题通过"); return }
|
|
|
await goNextStep()
|
|
|
return
|
|
|
}
|
|
|
if (await validateAndSaveStepTwo()) currentStep.value = normalizeStep(currentStep.value + 1)
|
|
|
}
|
|
|
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 false; 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 false }
|
|
|
await persist("SAVE"); proxy?.$modal?.msgSuccess("服务分级处理方案已保存"); return true
|
|
|
} catch (error) { proxy?.$modal?.msgError(error?.message || "保存失败,请稍后重试"); return false } 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-training-shell,页面不再重复声明 */
|
|
|
|
|
|
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: var(--student-fs-body-sm);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.task-brief-text {
|
|
|
display: block;
|
|
|
}
|
|
|
|
|
|
.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: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
ul {
|
|
|
display: grid;
|
|
|
gap: 4px;
|
|
|
margin: 0;
|
|
|
padding-left: 18px;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-action-strip {
|
|
|
margin: 0;
|
|
|
padding: 10px 16px;
|
|
|
|
|
|
:deep(.training-task-toolbar) {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
justify-content: flex-end;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== workbench:透明化 ===== */
|
|
|
.after-sales-workbench {
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.section-eyebrow {
|
|
|
margin: 0;
|
|
|
color: var(--student-accent);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.12em;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
/* ===== 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: var(--student-fs-caption);
|
|
|
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);
|
|
|
}
|
|
|
|
|
|
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: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
font-family: ui-monospace, "SF Mono", Consolas, monospace;
|
|
|
}
|
|
|
|
|
|
.lane-head h4 {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h2);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.lane-head>span {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.fault-lane>p,
|
|
|
.unassigned-panel>p {
|
|
|
min-height: 0;
|
|
|
margin: 8px 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
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: var(--student-fs-body-sm);
|
|
|
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: var(--student-fs-body-sm);
|
|
|
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: 18px;
|
|
|
line-height: 1;
|
|
|
}
|
|
|
|
|
|
.fault-card p {
|
|
|
grid-column: 2;
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
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: var(--student-fs-caption);
|
|
|
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: var(--student-fs-body);
|
|
|
}
|
|
|
|
|
|
/* ===== 结果反馈 ===== */
|
|
|
.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: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.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: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
font-family: ui-monospace, "SF Mono", Consolas, monospace;
|
|
|
}
|
|
|
|
|
|
.service-table em {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-style: italic;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.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: var(--student-fs-caption);
|
|
|
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: var(--student-fs-caption);
|
|
|
line-height: 1.5;
|
|
|
white-space: pre-line;
|
|
|
}
|
|
|
|
|
|
/* ===== Step 03:政策设计 ===== */
|
|
|
.policy-design-card {
|
|
|
display: grid;
|
|
|
gap: 20px;
|
|
|
}
|
|
|
|
|
|
.policy-section {
|
|
|
display: grid;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
.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;
|
|
|
}
|
|
|
|
|
|
.step-navigation {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.btn-nav {
|
|
|
width: 100%;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 底部步骤导航按钮 ===== */
|
|
|
.step-navigation {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 10px;
|
|
|
margin-top: 16px;
|
|
|
padding: 12px 16px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
flex-wrap: nowrap;
|
|
|
}
|
|
|
|
|
|
.step-nav-right {
|
|
|
margin-left: auto;
|
|
|
}
|
|
|
|
|
|
.btn-primary {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid var(--student-accent);
|
|
|
border-radius: 8px;
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.2), 0 2px 8px rgba(37, 99, 235, 0.15);
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent-deep);
|
|
|
background: var(--student-accent-deep);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.3), 0 4px 12px rgba(29, 78, 216, 0.22);
|
|
|
}
|
|
|
|
|
|
&:active:not(:disabled) {
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-ghost {
|
|
|
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-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-save {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid #e5e7eb;
|
|
|
border-radius: 8px;
|
|
|
color: #4b5563;
|
|
|
background: #f9fafb;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: #d1d5db;
|
|
|
background: #f3f4f6;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-primary:disabled,
|
|
|
.btn-ghost:disabled,
|
|
|
.btn-save:disabled,
|
|
|
.btn-nav:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
transform: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.step-progress {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-family: var(--student-font-data);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.05em;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
/* ===== 步骤内小节标题(蓝底序号,与 diagnosis.vue 统一) ===== */
|
|
|
.section-heading {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
gap: 12px;
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
>span {
|
|
|
display: grid;
|
|
|
flex: 0 0 28px;
|
|
|
width: 28px;
|
|
|
height: 28px;
|
|
|
place-items: center;
|
|
|
border-radius: 6px;
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent, #2563eb);
|
|
|
font-size: var(--student-fs-body, 14px);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
h3 {
|
|
|
margin: 0;
|
|
|
color: var(--student-text, #1f2937);
|
|
|
font-size: var(--student-fs-h1, 18px);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text-secondary, #6b7280);
|
|
|
font-size: var(--student-fs-body, 14px);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
}
|
|
|
</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 {
|
|
|
|
|
|
|
|
|
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: var(--student-fs-body) !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1.3 !important;
|
|
|
white-space: nowrap !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: var(--student-fs-body-sm) !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: nowrap !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: var(--student-fs-body) !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: var(--student-fs-h2) !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: var(--student-fs-body) !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: var(--student-fs-h2) !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>
|