|
|
|
|
|
<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>
|