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

202 lines
19 KiB
Vue

<template>
<div class="after-sales-page student-training-shell">
<main class="training-core">
<section class="task-header">
<div class="task-badge"><el-icon><Service /></el-icon> · </div>
<h1 class="task-title">{{ taskTitle }}</h1>
</section>
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<section class="task-card">
<div class="workbench-head">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p class="workbench-description">{{ taskRequirement }}</p>
</div>
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome"><el-icon><Download /></el-icon></button>
</div>
</div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="售后服务流程设计步骤">
<button
v-for="(step, index) in configuredSteps"
:key="`${step}-${index}`"
type="button"
class="step-tab"
:class="{ active: activeStep === index + 1 }"
:aria-current="activeStep === index + 1 ? 'step' : undefined"
@click="setCurrentStep(index + 1)"
>
{{ step }}
</button>
</nav>
<section v-show="isSectionVisible(1)" class="step-card">
<StepHeading :step="activeStep" :title="activeStepName" />
<div class="step-content">
<h4>售后服务闭环</h4>
<p>售后服务并非单次响应而是从用户问题进入问题识别责任分流解决交付回访评价到问题沉淀的完整闭环设计时应优先保证入口清晰响应可预期升级有边界结果可追踪</p>
<div class="service-flow" aria-label="">
<article v-for="item in serviceFlow" :key="item.title"><strong>{{ item.title }}</strong><span>{{ item.description }}</span></article>
</div>
<div class="hint-card"><strong>填写提示</strong>先明确产品和典型售后场景再定义每类问题的处理时效与责任角色最后补全回访和产品改进机制</div>
</div>
</section>
<section v-show="isSectionVisible(2)" class="step-card">
<StepHeading :step="activeStep" :title="activeStepName" />
<div class="step-content">
<label class="field-block"><span>产品名称</span><el-input v-model="form.productName" placeholder="填写智能产品名称" /></label>
<label class="field-block"><span>服务场景</span><el-input v-model="form.serviceScenario" type="textarea" :autosize="{ minRows: 7, maxRows: 10 }" placeholder="列出安装、联网、使用、故障、退换货等典型售后场景" /></label>
</div>
</section>
<section v-show="isSectionVisible(3)" class="step-card">
<StepHeading :step="activeStep" :title="activeStepName" />
<div class="step-content">
<label class="field-block"><span>服务标准</span><el-input v-model="form.serviceSla" type="textarea" :autosize="{ minRows: 6, maxRows: 9 }" placeholder="说明响应时间、处理时限、升级规则和责任角色" /></label>
<div class="table-panel">
<div class="panel-title"><h4>售后流程设计表</h4><p>为每一项写明学生设计的服务规则或动作</p></div>
<div class="table-wrap">
<table>
<thead><tr><th>项目</th><th>说明</th><th>学生填写</th></tr></thead>
<tbody><tr v-for="row in workRows" :key="row.name"><th scope="row">{{ row.name }}</th><td>{{ row.description }}</td><td><el-input v-model="row.answer" type="textarea" :autosize="{ minRows: 3, maxRows: 6 }" placeholder="填写本项分析内容" /></td></tr></tbody>
</table>
</div>
</div>
</div>
</section>
<section v-show="isSectionVisible(4)" class="step-card">
<StepHeading :step="activeStep" :title="activeStepName" />
<div class="step-content">
<label class="field-block"><span>闭环机制</span><el-input v-model="form.serviceLoop" type="textarea" :autosize="{ minRows: 8, maxRows: 12 }" placeholder="说明回访、评价、问题复盘和产品改进沉淀方式" /></label>
<div class="hint-card"><strong>成果检查</strong>确认服务入口问题分类处理动作时效标准和数据沉淀均已填写且能够对应到真实的用户服务场景</div>
</div>
</section>
<section v-show="hasConfiguredSteps && activeStep > 4" class="step-card">
<StepHeading :step="activeStep" :title="activeStepName" />
<p class="step-desc">请检查前述服务流程与成果填写是否完整再保存或提交实训成果</p>
</section>
<div v-if="hasConfiguredSteps" class="step-navigation">
<button type="button" class="btn-nav" :disabled="activeStep === 1" @click="goPreviousStep"><el-icon><ArrowLeft /></el-icon></button>
<button type="button" class="btn-nav btn-save" :disabled="saving" @click="saveCurrentProgress"><el-icon><CircleCheck /></el-icon></button>
<button type="button" class="btn-nav btn-primary" :disabled="activeStep === configuredSteps.length" @click="goNextStep"><el-icon><ArrowRight /></el-icon></button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask"></el-button>
<el-button class="training-action training-action--reset" :loading="saving" @click="resetTask"></el-button>
</div>
</section>
</main>
<TrainingAiSidebar
study-tip="当前任务建议:从用户问题入口开始,设计可分流、可升级、可追踪的售后服务闭环。"
reference-title="售后流程设计要点"
reference-text="· 入口明确电话、在线客服、App、门店或工单系统<br />· 分流清晰:硬件、软件、网络、使用指导、退换货<br />· 闭环沉淀:回访评价、高频问题复盘、产品持续改进"
:progress-items="progressItems"
/>
</div>
</template>
<script setup>
import { computed, defineComponent, getCurrentInstance, h, onMounted, ref } from "vue";
import { ArrowLeft, ArrowRight, CircleCheck, Download, Service } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
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 saving = ref(false);
const activeStep = ref(1);
const form = ref(createForm());
const workRows = ref(createRows());
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 configuredSteps = computed(() => parseTrainingArray(taskConfig.value?.steps));
const hasConfiguredSteps = computed(() => configuredSteps.value.length > 0);
const activeStepName = computed(() => configuredSteps.value[activeStep.value - 1] || "售后服务流程设计");
const progressItems = computed(() => configuredSteps.value.map((text, index) => ({ text, status: index + 1 < activeStep.value ? "done" : index + 1 === activeStep.value ? "doing" : "" })));
const serviceFlow = [
{ title: "问题受理", description: "统一入口与工单记录" },
{ title: "诊断分流", description: "按问题类型明确责任" },
{ title: "解决交付", description: "远程、维修或换新处理" },
{ title: "回访沉淀", description: "评价复盘并反哺产品" },
];
const StepHeading = defineComponent({
props: { step: { type: Number, required: true }, title: { type: String, required: true } },
setup(props) {
return () => h("div", { class: "step-title-row" }, [h("div", [h("p", { class: "section-eyebrow" }, `Step ${String(props.step).padStart(2, "0")}`), h("h3", props.title)])]);
},
});
function createForm() { return { productName: "", serviceScenario: "", serviceSla: "", serviceLoop: "" }; }
function createRows() { return (basePage.tableRows || []).map(([name, description]) => ({ name, description, answer: "" })); }
function normalizeActiveStep(value) { if (!configuredSteps.value.length) return 1; return Math.min(Math.max(Number(value) || 1, 1), configuredSteps.value.length); }
function isSectionVisible(section) { return !hasConfiguredSteps.value || activeStep.value === section; }
onMounted(async () => { await loadTaskConfig(); await loadSavedProgress(); });
async function loadTaskConfig() {
try { const res = await getTrainingTaskByKey(TASK_KEY); taskConfig.value = res?.data || null; activeStep.value = normalizeActiveStep(activeStep.value); }
catch (error) { taskConfig.value = null; activeStep.value = 1; }
}
function buildProgress() { return { activeStep: activeStep.value, form: form.value, workRows: workRows.value, updatedAt: new Date().toISOString() }; }
function applyProgress(value) {
if (!value || typeof value !== "object") return;
activeStep.value = normalizeActiveStep(value.activeStep);
form.value = { ...createForm(), ...(value.form || {}) };
workRows.value = createRows().map((row, index) => ({ ...row, answer: value.workRows?.[index]?.answer || "" }));
}
async function loadSavedProgress() {
try { const res = await getStudentTrainingAnswer(TASK_KEY); const raw = res?.data?.step1Answer; if (raw) applyProgress(JSON.parse(raw)); }
catch (error) { /* Keep the task usable when historical answers are unavailable. */ }
}
async function persistProgress(saveAction = "SAVE", silent = false) {
await saveStudentTrainingAnswer(TASK_KEY, { saveAction, currentStep: activeStep.value, step1Answer: JSON.stringify(buildProgress()) });
if (!silent) proxy?.$modal?.msgSuccess(saveAction === "SUBMIT" ? "提交成功" : "保存成功");
}
function setCurrentStep(step) { activeStep.value = normalizeActiveStep(step); persistProgress("SAVE", true).catch(() => {}); }
function goPreviousStep() { setCurrentStep(activeStep.value - 1); }
function goNextStep() { setCurrentStep(activeStep.value + 1); }
async function saveCurrentProgress() { if (saving.value) return; saving.value = true; try { await persistProgress(); } finally { saving.value = false; } }
async function submitTask() { if (saving.value) return; saving.value = true; try { await persistProgress("SUBMIT"); } finally { saving.value = false; } }
async function resetTask() { if (saving.value) return; saving.value = true; try { form.value = createForm(); workRows.value = createRows(); activeStep.value = 1; await persistProgress("RESET"); proxy?.$modal?.msgSuccess("清空填写成功"); } finally { saving.value = false; } }
function exportOutcome() { const blob = new Blob([JSON.stringify(buildProgress(), 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 { display: grid; grid-template-columns: minmax(720px, 1fr) 340px; gap: 24px; min-height: calc(100vh - 76px); padding: 24px 24px 32px; color: #eef5ff; background: #06111d; }
.training-core { min-width: 0; padding: 28px; border: 1px solid rgba(0,180,255,.25); border-radius: 36px; background: rgba(8,18,28,.6); box-shadow: inset 0 1px 0 rgba(169,229,255,.08), 0 22px 48px rgba(0,0,0,.22); }
.task-header { margin-bottom: 24px; }.task-badge { display:inline-flex; align-items:center; gap:8px; min-height:28px; padding:4px 14px; border:1px solid rgba(99,217,255,.55); border-radius:999px; color:#dff5ff; background:rgba(17,126,178,.36); font-size:12px; font-weight:700; }.task-title { margin:12px 0 0; color:#fff; font-size:30px; font-weight:900; line-height:1.3; }
.task-card,.step-card { border:1px solid rgba(82,174,226,.42); border-radius:26px; background:rgba(0,25,42,.64); }.task-card { margin-top:28px; padding:18px 22px 22px; }.workbench-head { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; margin-bottom:22px; padding-bottom:18px; border-bottom:1px solid rgba(0,180,255,.2); }.section-eyebrow { margin:0; color:#71dfff; font-size:12px; font-weight:800; line-height:1.4; }.workbench-head h3 { margin:4px 0 8px; color:#fff; font-size:26px; font-weight:800; line-height:1.3; }.workbench-description,.step-content p,.step-desc { margin:0; color:#d7ecff; font-size:15px; line-height:1.8; }.workbench-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:10px; flex-shrink:0; }
.outline-action,.btn-nav { display:inline-flex; align-items:center; justify-content:center; gap:8px; min-height:40px; padding:0 18px; border:1px solid rgba(99,217,255,.55); border-radius:999px; color:#e9fbff; background:rgba(17,126,178,.36); font:inherit; font-size:14px; font-weight:800; cursor:pointer; }.outline-action:hover,.outline-action:focus-visible,.btn-nav:hover:not(:disabled),.btn-nav:focus-visible { border-color:rgba(116,235,255,.85); background:rgba(18,146,204,.5); outline:none; transform:translateY(-1px); }
.step-indicator { display:flex; gap:6px; min-height:58px; margin:0 0 32px; padding:6px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:999px; background:rgba(0,20,35,.65); }.step-tab { flex:1 0 max-content; min-height:46px; padding:0 18px; border:1px solid transparent; border-radius:999px; color:#9ab3d0; background:transparent; font:inherit; font-size:14px; font-weight:800; cursor:pointer; }.step-tab:hover,.step-tab:focus-visible { border-color:rgba(99,217,255,.55); color:#e9fbff; outline:none; }.step-tab.active { border-color:rgba(92,224,255,.7); color:#fff; background:linear-gradient(95deg,#0a6b9e,#0a5c86); }
.step-card { margin-top:32px; padding:18px 22px 22px; }.step-indicator + .step-card,.workbench-head + .step-card { margin-top:0; }.step-card + .step-card { margin-top:16px; }.step-title-row { margin-bottom:14px; }.step-title-row h3 { margin:4px 0 0; color:#fff; font-size:22px; font-weight:900; line-height:1.3; }.step-content h4 { margin:0 0 12px; color:#fff; font-size:22px; font-weight:900; line-height:1.3; }
.service-flow { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; margin:20px 0; }.service-flow article,.hint-card { border:1px solid rgba(0,180,255,.22); border-radius:18px; background:rgba(1,14,26,.72); }.service-flow article { display:grid; gap:6px; min-height:112px; padding:16px; }.service-flow strong { color:#e9fbff; font-size:15px; }.service-flow span { color:#b7d8e9; font-size:13px; line-height:1.65; }.hint-card { margin-top:20px; padding:14px 16px; color:#d7ecff; font-size:14px; line-height:1.75; }.hint-card strong { color:#71dfff; }
.field-block { display:block; margin:16px 0 0; }.field-block span { display:block; margin-bottom:8px; color:#dff5ff; font-size:14px; font-weight:800; line-height:1.6; }.field-block :deep(.el-input__wrapper),.field-block :deep(.el-textarea__inner),.table-wrap :deep(.el-textarea__inner) { border:1px solid rgba(45,95,126,.95); border-radius:14px; box-shadow:none; background:#0f1a24; }.field-block :deep(.el-input__wrapper) { min-height:42px; padding:0 14px; }.field-block :deep(.el-input__inner),.field-block :deep(.el-textarea__inner),.table-wrap :deep(.el-textarea__inner) { color:#eef5ff; font-size:14px; }.field-block :deep(.el-textarea__inner),.table-wrap :deep(.el-textarea__inner) { min-height:110px !important; padding:12px 14px; line-height:1.65; }.field-block :deep(.el-input__wrapper.is-focus),.field-block :deep(.el-textarea__inner:focus),.table-wrap :deep(.el-textarea__inner:focus) { border-color:rgba(92,224,255,.7); box-shadow:0 0 0 3px rgba(0,174,255,.13); }
.table-panel { margin-top:22px; }.panel-title { margin-bottom:12px; }.panel-title h4 { margin:0 0 4px; color:#fff; font-size:18px; font-weight:900; }.panel-title p { color:#9ab3d0; font-size:14px; }.table-wrap { overflow-x:auto; border:1px solid rgba(0,180,255,.22); border-radius:18px; background:rgba(1,14,26,.72); }.table-wrap table { width:100%; min-width:760px; border-collapse:collapse; }.table-wrap th,.table-wrap td { padding:12px; border-right:1px solid rgba(77,167,220,.2); border-bottom:1px solid rgba(77,167,220,.2); vertical-align:top; }.table-wrap thead th { color:#eefaff; background:rgba(8,55,82,.98); font-size:14px; font-weight:800; }.table-wrap tbody th { width:18%; color:#dff5ff; font-size:14px; }.table-wrap td { color:#d7ecff; font-size:14px; line-height:1.7; }.table-wrap tr:last-child th,.table-wrap tr:last-child td { border-bottom:0; }.table-wrap th:last-child,.table-wrap td:last-child { border-right:0; }
.step-navigation { display:flex; justify-content:center; gap:14px; margin-top:22px; }.btn-nav.btn-save,.btn-nav.btn-primary { border-color:rgba(92,224,255,.7); color:#fff; background:linear-gradient(95deg,#0b84bd,#096491); }.btn-nav:disabled { cursor:not-allowed; opacity:.45; transform:none; }.training-actions { display:flex; flex-wrap:wrap; justify-content:center; gap:52px; margin-top:36px; }.training-actions :deep(.training-action) { min-width:172px; height:64px; padding:0 28px; border:1px solid #2c83aa; border-radius:6px; color:#dff7ff; background:rgba(13,64,88,.62); font-size:24px; font-weight:900; }.training-actions :deep(.training-action:hover:not(.is-disabled)) { border-color:#46b5e7; color:#fff; background:rgba(18,85,116,.78); }
@media (max-width:1100px) { .after-sales-page { grid-template-columns:1fr; } }
@media (max-width:900px) { .after-sales-page { padding:14px; }.training-core { padding:18px; border-radius:24px; }.workbench-head,.workbench-actions,.step-navigation { flex-direction:column; align-items:stretch; }.workbench-actions,.outline-action,.btn-nav,.training-actions :deep(.training-action) { width:100%; }.step-indicator { flex-direction:column; border-radius:26px; }.step-tab { flex:none; }.service-flow { grid-template-columns:1fr; } }
</style>