|
|
|
|
|
<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"><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 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}}
|
|
|
|
|
|
</style>
|