|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="diagnosis student-training-shell">
|
|
|
|
|
|
<main class="training-core">
|
|
|
|
|
|
<section class="task-header"><div class="task-badge"><el-icon><FirstAidKit /></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 workbench-head--actions-only"><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="产品问题诊断步骤"><template v-for="(step,index) in configuredSteps" :key="`${step}-${index}`"><button type="button" :class="['step-tab',{active:activeStep===index+1,complete:activeStep>index+1}]" :aria-current="activeStep===index+1?'step':undefined" :aria-label="`第${index+1}步:${step}`" @click="setCurrentStep(index+1)"><span class="step-marker">{{ String(index+1).padStart(2,'0') }}</span><span class="step-copy"><strong :title="step">{{ step }}</strong><span class="step-action">{{ activeStep===index+1?'当前步骤':'点击切换 →' }}</span></span></button><span v-if="index<configuredSteps.length-1" class="step-connector" aria-hidden="true"></span></template></nav>
|
|
|
|
|
|
<section class="step-card">
|
|
|
|
|
|
<div class="step-title-row"><div><p v-if="hasConfiguredSteps" class="section-eyebrow">Step {{ String(activeStep).padStart(2, '0') }}</p><h3>{{ activeStepName }}</h3></div></div>
|
|
|
|
|
|
<nav class="analysis-tabs" role="tablist" aria-label="产品问题诊断工具"><button v-for="(item,index) in modules" :key="item.name" type="button" class="analysis-tab" :class="{ active: activeModule === index }" :aria-selected="activeModule === index" :disabled="item.disabled" role="tab" @click="setActiveModule(index)">{{ item.name }}</button></nav>
|
|
|
|
|
|
<div class="business-workspace"><component :is="activeComponent" :key="workspaceKey" ref="businessComponent" /></div>
|
|
|
|
|
|
</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" :loading="saving" @click="submitTask">提交任务</el-button><el-button class="training-action" :loading="saving" @click="resetTask">清空填写</el-button></div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
<TrainingAiSidebar study-tip="当前任务建议:先选择合适的诊断工具,定位产品表现中的核心问题,再根据计算或分析结果形成迭代建议。" :progress-items="progressItems" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { computed, defineAsyncComponent, onMounted, ref } from "vue";
|
|
|
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, Download, FirstAidKit } 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 { parseTrainingArray } from "@/views/training/taskKeyMap";
|
|
|
|
|
|
import { isModuleUnique } from "@/utils/index.js";
|
|
|
|
|
|
|
|
|
|
|
|
const TASK_KEY = "product-diagnosis-iteration";
|
|
|
|
|
|
const taskConfig = ref(null); const activeStep = ref(1); const activeModule = ref(0); const saving = ref(false); const workspaceKey = ref(0); const businessComponent = ref(null);
|
|
|
|
|
|
const modules = ref([
|
|
|
|
|
|
{ name: "五维四率法", component: defineAsyncComponent(() => import("./five.vue")), disabled: false },
|
|
|
|
|
|
{ name: "KANO 模型", component: defineAsyncComponent(() => import("./kano.vue")), disabled: false },
|
|
|
|
|
|
{ name: "市场机会矩阵", component: defineAsyncComponent(() => import("./market.vue")), disabled: false },
|
|
|
|
|
|
{ name: "流量数据分析法", component: defineAsyncComponent(() => import("./traffic.vue")), disabled: false },
|
|
|
|
|
|
{ name: "用户评论分析", component: defineAsyncComponent(() => import("../digitalMarketingAlgorithms/components/emotion-analysis.vue")), disabled: false },
|
|
|
|
|
|
]);
|
|
|
|
|
|
const taskTitle = computed(() => taskConfig.value?.taskName || "产品问题诊断与迭代");
|
|
|
|
|
|
const taskBackground = computed(() => taskConfig.value?.background || "通过多种分析方法定位产品、流量、需求和用户反馈中的关键问题,形成可落地的优化迭代方案。");
|
|
|
|
|
|
const taskRequirement = computed(() => taskConfig.value?.requirements || "选择适用的诊断工具完成数据分析、问题定位和改进建议输出。");
|
|
|
|
|
|
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives, ["掌握产品问题诊断的核心方法", "能够基于数据定位影响表现的关键因素", "形成可执行的产品迭代建议"]));
|
|
|
|
|
|
const configuredSteps = computed(() => parseTrainingArray(taskConfig.value?.steps)); const hasConfiguredSteps = computed(() => configuredSteps.value.length > 0); const activeStepName = computed(() => configuredSteps.value[activeStep.value - 1] || "产品问题诊断"); const activeComponent = computed(() => modules.value[activeModule.value]?.component); const progressItems = computed(() => configuredSteps.value.map((text,index) => ({ text, status:index + 1 < activeStep.value ? "done" : index + 1 === activeStep.value ? "doing" : "" })));
|
|
|
|
|
|
function normalizeActiveStep(value) { return configuredSteps.value.length ? Math.min(Math.max(Number(value) || 1, 1), configuredSteps.value.length) : 1; }
|
|
|
|
|
|
function buildProgress() { return { activeStep: activeStep.value, activeModule: activeModule.value, updatedAt: new Date().toISOString() }; }
|
|
|
|
|
|
function applyProgress(value) { if (!value || typeof value !== "object") return; activeStep.value = normalizeActiveStep(value.activeStep); activeModule.value = Math.min(Math.max(Number(value.activeModule) || 0, 0), modules.value.length - 1); }
|
|
|
|
|
|
async function persistProgress(saveAction = "SAVE") { await saveStudentTrainingAnswer(TASK_KEY, { saveAction, currentStep: activeStep.value, step1Answer: JSON.stringify(buildProgress()) }); }
|
|
|
|
|
|
function setCurrentStep(step) { activeStep.value = normalizeActiveStep(step); activeModule.value = Math.min(activeStep.value - 1, modules.value.length - 1); persistProgress("SAVE").catch(() => {}); }
|
|
|
|
|
|
function setActiveModule(index) { if (modules.value[index]?.disabled) return; activeModule.value = index; persistProgress("SAVE").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 { businessComponent.value?.submitTask?.(); await persistProgress("SAVE"); } finally { saving.value = false; } }
|
|
|
|
|
|
async function resetTask() { if (saving.value) return; saving.value = true; try { businessComponent.value?.resetTask?.(); activeStep.value = 1; activeModule.value = 0; workspaceKey.value += 1; await persistProgress("RESET"); } 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); }
|
|
|
|
|
|
onMounted(async () => { try { modules.value.forEach((item) => { try { item.disabled = !isModuleUnique([item.name]); } catch (error) { item.disabled = false; } }); const res = await getTrainingTaskByKey(TASK_KEY); taskConfig.value = res?.data || null; activeStep.value = normalizeActiveStep(activeStep.value); } catch (error) { taskConfig.value = null; } try { const res = await getStudentTrainingAnswer(TASK_KEY); if (res?.data?.step1Answer) applyProgress(JSON.parse(res.data.step1Answer)); } catch (error) {} });
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.diagnosis{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;justify-content:space-between;align-items:flex-start;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{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,.analysis-tab{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}.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.active,.analysis-tab.active{border-color:rgba(92,224,255,.7);color:#fff;background:linear-gradient(95deg,#0a6b9e,#0a5c86)}.step-card{padding:18px 22px 22px}.step-title-row{margin-bottom:14px}.step-title-row h3{margin:4px 0 0;color:#fff;font-size:22px;font-weight:900}.analysis-tabs{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}.analysis-tab:disabled{cursor:not-allowed;opacity:.45}.business-workspace{min-width:0;overflow:hidden;border:1px solid rgba(0,180,255,.22);border-radius:18px;background:rgba(1,14,26,.72)}.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}.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}
|
|
|
|
|
|
.business-workspace :deep(.training-intro){display:none!important}.business-workspace :deep(.training-step-area){margin:0!important;padding:18px!important;border:0!important;border-radius:0!important;color:#d7ecff!important;background:transparent!important;box-shadow:none!important}.business-workspace :deep(.taskfooter){display:none!important}.business-workspace :deep(.five),.business-workspace :deep(.kano),.business-workspace :deep(.market),.business-workspace :deep(.traffic),.business-workspace :deep(.main-top),.business-workspace :deep(.app-main){color:#d7ecff!important;background:transparent!important}.business-workspace :deep(.el-scrollbar){height:auto!important}.business-workspace :deep(.el-table){--el-table-border-color:rgba(77,167,220,.2);--el-table-header-bg-color:rgba(8,55,82,.98);--el-table-header-text-color:#eaffff;--el-table-row-hover-bg-color:rgba(11,84,122,.45);--el-table-bg-color:rgba(1,14,26,.72);--el-table-tr-bg-color:rgba(1,14,26,.48);--el-table-text-color:#d7f1ff;overflow:hidden;border:1px solid rgba(0,180,255,.22);border-radius:16px;background:rgba(1,14,26,.72)!important}.business-workspace :deep(.el-table th.el-table__cell){color:#eaffff!important;background:rgba(8,55,82,.98)!important;font-weight:900}.business-workspace :deep(.el-table tr),.business-workspace :deep(.el-table td.el-table__cell){color:#d7f1ff!important;background:rgba(1,14,26,.48)!important}.business-workspace :deep(.el-input__wrapper),.business-workspace :deep(.el-textarea__inner),.business-workspace :deep(.el-select__wrapper){border:1px solid rgba(45,95,126,.95)!important;border-radius:14px!important;box-shadow:none!important;color:#eef5ff!important;background:#0f1a24!important}.business-workspace :deep(.el-input__inner),.business-workspace :deep(.el-textarea__inner){color:#eef5ff!important}.business-workspace :deep(.el-button){min-height:40px;padding:0 18px;border:1px solid rgba(92,224,255,.7)!important;border-radius:999px;color:#fff!important;font-size:14px;font-weight:800;background:linear-gradient(95deg,#0b84bd,#096491)!important}.business-workspace :deep(.caseBacktitle){display:inline-flex;align-items:center;min-height:28px;padding:4px 12px;border:1px solid rgba(106,226,255,.45);border-radius:999px;color:#71dfff;background:rgba(0,127,184,.25);font-size:12px;font-weight:800}.business-workspace :deep(.caseBacktitle img){display:none}.business-workspace :deep(.df),.business-workspace :deep(.left),.business-workspace :deep(.right){color:#d7ecff!important;background:transparent!important}
|
|
|
|
|
|
@media(max-width:1100px){.diagnosis{grid-template-columns:1fr}}@media(max-width:900px){.diagnosis{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}}
|
|
|
|
|
|
.diagnosis .step-indicator { display:flex; align-items:center; gap:0; width:calc(100% + 24px); min-height:0; margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:18px; background:linear-gradient(110deg,rgba(4,39,58,.9),rgba(2,20,33,.94)); box-shadow:inset 0 1px 0 rgba(169,229,255,.08); }
|
|
|
|
|
|
.diagnosis .step-tab { display:flex; flex:1 0 145px; min-width:0; align-items:center; min-height:66px; padding:8px 10px; border:1px solid rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); text-align:left; }
|
|
|
|
|
|
.diagnosis .step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,0.78),rgba(5,62,102,0.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }
|
|
|
|
|
|
.diagnosis .step-marker { display:inline-flex; flex:0 0 38px; align-items:center; justify-content:center; width:38px; height:38px; border:1px solid rgba(113,223,255,.55); border-radius:50%; color:#bdefff; background:rgba(3,27,43,.72); font-size:13px; font-weight:900; }.diagnosis .step-copy { display:grid; min-width:0; gap:4px; margin-left:10px; }.diagnosis .step-copy strong { display:-webkit-box; overflow:hidden; color:inherit; font-size:14px; line-height:1.25; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.diagnosis .step-action { color:#7ec7e7; font-size:12px; font-weight:700; }
|
|
|
|
|
|
.diagnosis .step-tab.active .step-marker { border-color:#72e8ff; color:#fff; background:linear-gradient(135deg,#08b7e8,#1375d0); box-shadow:0 0 0 5px rgba(25,179,237,.14),0 0 18px rgba(46,208,255,.58); }.diagnosis .step-tab.complete { color:#dffbff; }.diagnosis .step-tab.complete .step-marker { border-color:rgba(84,230,221,.85); color:#062b37; background:#6ef0e1; box-shadow:0 0 12px rgba(79,231,218,.35); }
|
|
|
|
|
|
.diagnosis .step-connector { position:relative; flex:0 1 40px; min-width:18px; height:2px; margin:0 8px; overflow:visible; background:rgba(77,167,220,.54); }.diagnosis .step-connector::after { position:absolute; top:50%; right:-1px; width:6px; height:6px; border-top:1px solid currentColor; border-right:1px solid currentColor; color:#78cfe9; content:""; transform:translateY(-50%) rotate(45deg); }.diagnosis .step-tab.complete + .step-connector { background:linear-gradient(90deg,#6ef0e1,#1cb8e9); box-shadow:0 0 9px rgba(53,218,235,.46); }.diagnosis .step-tab.complete + .step-connector::after { color:#63e6f1; }
|
|
|
|
|
|
@media(max-width:900px){.diagnosis .step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; }.diagnosis .step-tab { flex:0 0 auto; width:100%; }.diagnosis .step-connector { flex:0 0 20px; min-width:0; width:2px; height:20px; margin:0 0 0 29px; }.diagnosis .step-connector::after { top:auto; right:50%; bottom:-1px; transform:translateX(50%) rotate(135deg); }}
|
|
|
|
|
|
.diagnosis .workbench-head--actions-only { justify-content:flex-end; margin-bottom:18px; padding-bottom:0; border-bottom:0; }
|
|
|
|
|
|
</style>
|