|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="optimization student-training-shell">
|
|
|
|
|
|
<main class="training-core">
|
|
|
|
|
|
<section class="task-header"><div class="task-badge"><el-icon><TrendCharts /></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="!hasConfiguredSteps || activeStep <= 2" 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>
|
|
|
|
|
|
<div 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" role="tab" @click="setActiveModule(index)">{{ item.name }}</button></div>
|
|
|
|
|
|
<div class="business-workspace"><component :is="activeComponent" :key="workspaceKey" ref="businessComponent" /></div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
<section v-show="hasConfiguredSteps && activeStep > 2" class="step-card"><div class="step-title-row"><div><p class="section-eyebrow">Step {{ String(activeStep).padStart(2,'0') }}</p><h3>{{ activeStepName }}</h3></div></div><p class="step-desc">请复核帕累托/ABC 分析与购物篮分析结果,提炼高价值商品组合、货架陈列或促销策略后再提交成果。</p><div class="strategy-card"><strong>分析输出建议</strong><span>明确核心商品、关联销售组合、优化动作及预期业务价值。</span></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="当前任务建议:先完成 ABC 分类识别核心商品,再通过购物篮分析确定可联合销售的商品组合。" :progress-items="progressItems" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { computed, defineAsyncComponent, onMounted, ref } from "vue";
|
|
|
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, Download, TrendCharts } 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";
|
|
|
|
|
|
|
|
|
|
|
|
const TASK_KEY = "category-optimization";
|
|
|
|
|
|
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 = [{ name: "帕累托/ABC 分析", component: defineAsyncComponent(() => import("../portraitModel/components/abcAnalysis.vue")) }, { name: "购物篮分析", component: defineAsyncComponent(() => import("../portraitModel/components/basketAnalysis.vue")) }];
|
|
|
|
|
|
const activeComponent = computed(() => modules[activeModule.value].component);
|
|
|
|
|
|
const taskTitle = computed(() => taskConfig.value?.taskName || "品类结构优化");
|
|
|
|
|
|
const taskBackground = computed(() => taskConfig.value?.background || "通过调整和优化产品品类结构,提高平台运营效率、满足消费者需求,并增强市场竞争力。");
|
|
|
|
|
|
const taskRequirement = computed(() => taskConfig.value?.requirements || "通过 ABC 分析对商品进行分类管理,通过购物篮分析优化商品组合。" );
|
|
|
|
|
|
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives, ["理解 ABC 分析与购物篮分析方法", "能够识别核心商品与高价值商品组合", "形成可执行的品类结构优化建议"]));
|
|
|
|
|
|
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":"" })));
|
|
|
|
|
|
function normalizeActiveStep(value) { if (!configuredSteps.value.length) return 1; return Math.min(Math.max(Number(value)||1,1),configuredSteps.value.length); }
|
|
|
|
|
|
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.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); persistProgress("SAVE").catch(()=>{}); }
|
|
|
|
|
|
function setActiveModule(index) { 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?.submitData?.();await persistProgress("SAVE");}finally{saving.value=false;} }
|
|
|
|
|
|
async function resetTask(){ if(saving.value)return; saving.value=true; try{businessComponent.value?.retrain?.();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{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">
|
|
|
|
|
|
.optimization{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,.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,.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{margin-top:32px;padding:18px 22px 22px}.step-indicator+.step-card,.workbench-head+.step-card{margin-top:0}.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}.business-workspace{min-width:0;overflow:hidden;border:1px solid rgba(0,180,255,.22);border-radius:18px;background:rgba(1,14,26,.72)}.strategy-card{display:grid;gap:6px;margin-top:18px;padding:16px;border:1px solid rgba(0,180,255,.22);border-radius:16px;background:rgba(1,14,26,.72)}.strategy-card strong{color:#71dfff}.strategy-card span{color:#d7ecff;font-size:14px;line-height:1.7}.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}@media(max-width:1100px){.optimization{grid-template-columns:1fr}}@media(max-width:900px){.optimization{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>
|