< template >
< div class = "product-process student-training-shell" >
< main class = "process-core" >
< section class = "task-header" >
< div class = "task-head-main" >
< h1 class = "task-title" > { { taskTitle } } < / h1 >
< div class = "task-brief" >
< span class = "task-brief-text" > { { taskBackground } } < / span >
< button type = "button" class = "task-brief-toggle" @ click = "briefOpen = !briefOpen" >
{ { briefOpen ? "收起" : "展开目标与要求" } }
< / button >
< span v-if ="briefOpen" class="task-brief-more" >
< b > 实训目标 < / b >
< ul >
< li v-for ="goal in taskGoals" :key ="goal" > {{ goal }} < / li >
< / ul >
< b > 实训要求 < / b >
< p > { { taskRequirement } } < / p >
< / span >
< / div >
< / div >
< / section >
< section class = "task-action-strip" >
< TrainingTaskToolbar
: task - key = "TASK_KEY"
: material - name = "taskConfig?.materialName"
: material - url = "taskConfig?.materialUrl"
: training - data - name = "taskConfig?.trainingDataName"
: training - data - url = "taskConfig?.trainingDataUrl"
: guide - manual - name = "taskConfig?.guideManualName"
: guide - manual - url = "taskConfig?.guideManualUrl"
: ai - manual - name = "taskConfig?.aiManualName"
: ai - manual - url = "taskConfig?.aiManualUrl"
/ >
< / section >
< section class = "process-workbench" >
< nav v-if ="trainingSteps.length" class="step-indicator" aria-label="产品开发主要流程步骤" >
< button
v - for = "(step, index) in trainingSteps"
: key = "`${step}-${index}`"
type = "button"
: class = "['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]"
: aria - label = "`切换至第 ${index + 1} 步:${step}`"
: aria - current = "currentStep === index + 1 ? 'step' : undefined"
: title = "step"
@ click = "currentStep = index + 1"
>
< span class = "step-bar" aria -hidden = " true " > < / span >
< span class = "step-no" aria -hidden = " true " > { { String ( index + 1 ) . padStart ( 2 , '0' ) } } < / span >
< span class = "step-name" > { { step } } < / span >
< / button >
< / nav >
< section class = "process-step-card" >
< template v-if ="currentStep === 1" >
< div class = "step-title-row" >
< div > < p class = "section-eyebrow" > Step 01 < / p > < h3 > { { trainingSteps [ 0 ] } } < / h3 > < / div >
< span class = "step-chip" > 关键任务识别 < / span >
< / div >
< p class = "process-intro" > 互联网产品开发在拆解任务之前 , 团队必须先明确做什么样的产品 、 开发过程中有哪些卡点 。 这是后续排期的前提 。 请判断哪些开发环节需要特别关注 : 关键任务会影响全项目进度与最终落地效果 , 非关键任务可在核心任务约束下灵活协调 。 < / p >
< div class = "training-table-wrap" >
< table class = "process-table" >
< thead > < tr > < th > 阶段 < / th > < th > 任务编号 < / th > < th > 具体工作任务 < / th > < th > 是否关键任务 < / th > < / tr > < / thead >
< tbody >
< tr v-for ="row in processRows" :key="row.taskNo" :class="feedbackClass(row.taskNo)" >
< td class = "stage-cell" > { { row . stage } } < / td >
< td class = "task-no-cell" > { { row . taskNo } } < / td >
< td class = "task-cell" > { { row . task } } < / td >
< td class = "select-cell" >
< select v-model ="row.classification" :disabled="checking" @change="clearFeedback(row.taskNo)" >
< option value = "" disabled > 请选择 < / option >
< option v-for ="option in CLASSIFICATION_OPTIONS" :key="option" :value ="option" > {{ option }} < / option >
< / select >
< p v-if ="feedbackByTaskNo[row.taskNo]" class="classification-feedback" > {{ feedbackByTaskNo [ row.taskNo ] .message }} < / p >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< p v-if ="checkResult" class="classification-summary" :class="{ 'is-success': checkResult.allCorrect }" > 本 次 判 题 : {{ checkResult.correctCount }} / {{ checkResult.totalCount }} 项 分 类 正 确 < / p >
< div class = "task-actions" >
< button type = "button" class = "btn-nav btn-submit" :disabled ="checking" @click ="saveAndGoToStepTwo" > {{ checking ? ' 判 题 中 … ' : ' 保 存 并 进 入 第 2 步 ' }} < / button >
< button type = "button" class = "btn-nav" :disabled ="checking" @click ="resetTraining" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n >
< / div >
< / template >
< template v -else -if = " currentStep = = = 2 " >
< div class = "step-title-row" >
< div > < p class = "section-eyebrow" > Step 02 < / p > < h3 > { { trainingSteps [ 1 ] } } < / h3 > < / div >
< div class = "step-title-actions" >
< button type = "button" class = "outline-action schedule-export-action" @click ="exportScheduleList" > < el -icon > < Download / > < / e l - i c o n > 导 出 列 表 < / b u t t o n >
< span class = "step-chip" > 项目排期练习 < / span >
< / div >
< / div >
< p class = "process-intro" > 将开发一款互联网产品的模糊目标 , 拆解为可执行的具体工作任务 。 根据案例信息 , 填写参考工期和项目依赖关系 。 < / p >
< p v-if ="!hasCompletedStepOne" class="step-warning" > 请 先 完 成 第 1 步 并 判 题 通 过 ; 关 键 任 务 结 果 会 自 动 带 入 本 表 。 < / p >
< div class = "training-table-wrap" >
< table class = "process-table schedule-table" >
< thead > < tr > < th > 阶段 < / th > < th > 任务编号 < / th > < th > 具体工作任务 < / th > < th > 是否关键任务 < / th > < th > 开始时间 < / th > < th > 完成时间 < / th > < th > 项目依赖关系 < / th > < / tr > < / thead >
< tbody >
< tr v-for ="row in processRows" :key="row.taskNo" >
< td class = "stage-cell" > { { row . stage } } < / td >
< td class = "task-no-cell" > { { row . taskNo } } < / td >
< td class = "task-cell" > { { row . task } } < / td >
< td class = "classification-cell" > { { row . classification || '—' } } < / td >
< td > < input v -model = " row.startDate " type = "date" : disabled = "!hasCompletedStepOne || checking" / > < / td >
< td > < input v -model = " row.endDate " type = "date" : disabled = "!hasCompletedStepOne || checking" / > < / td >
< td > < input v -model .trim = " row.dependency " class = "dependency-input" : disabled = "!hasCompletedStepOne || checking" placeholder = "示例:开始-开始( SS) " maxlength = "100" / > < / td >
< / tr >
< / tbody >
< / table >
< / div >
< div class = "task-actions" >
< button type = "button" class = "btn-nav btn-submit" : disabled = "checking || !hasCompletedStepOne" @click ="saveAndGoToStepThree" > {{ checking ? ' 校 验 中 … ' : ' 保 存 并 进 入 第 3 步 ' }} < / button >
< button type = "button" class = "btn-nav" :disabled ="checking" @click ="resetStepTwo" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 本 步 < / b u t t o n >
< / div >
< / template >
< template v -else -if = " currentStep = = = 3 " >
< div class = "step-title-row" >
< div > < p class = "section-eyebrow" > Step 03 < / p > < h3 > { { trainingSteps [ 2 ] } } < / h3 > < / div >
< span class = "step-chip" > 甘特图绘制 < / span >
< / div >
< p class = "process-intro" > 根据上表数据 , 使用工具绘制甘特图 , 并上传 。 < / p >
< p v-if ="!stepTwoPassed" class="step-warning" > 请 先 保 存 第 2 步 项 目 排 期 , 再 上 传 甘 特 图 。 < / p >
< section class = "gantt-upload-panel" >
< label class = "gantt-upload-trigger" : class = "{ disabled: !stepTwoPassed || uploadingGantt }" >
< input type = "file" accept = "image/png,image/jpeg,image/webp,image/gif" : disabled = "!stepTwoPassed || uploadingGantt" @change ="uploadGanttImage" / >
< el -icon > < Upload / > < / e l - i c o n >
< strong > { { uploadingGantt ? '上传中…' : ganttImage . url ? '替换甘特图' : '上传甘特图' } } < / strong >
< span > 支持 PNG 、 JPG 、 JPEG 、 WEBP 、 GIF < / span >
< / label >
< figure v-if ="ganttImage.url" class="gantt-preview" >
< img :src ="resolveGanttImageUrl(ganttImage.url)" alt = "已上传的甘特图" @click ="previewGanttImage" / >
< figcaption > { { ganttImage . name } } < / figcaption >
< / figure >
< / section >
< section class = "gantt-guide" >
< h4 > 推荐工具如下 , 自行选择作图工具 : < / h4 >
< ol >
< li > Excel / WPS表格 , 利用堆积条形图制作 , 可参考 《 实训操作说明 》 完成 。 < button type = "button" @click ="downloadPracticeGuide" > 下 载 实 训 操 作 说 明 < / button > < / li >
< li > 下载Excel模板 , 填写关键信息 , 完成甘特图绘制 。 < button type = "button" @click ="downloadGanttTemplate" > 下 载 甘 特 图 绘 制 模 板 < / button > < / li >
< li > 导出步骤二信息表 , 上传到通用AI绘图模型 , 通过大模型绘制甘特图 。 参考提示词 : 请根据产品WBS任务拆解与排期表生成甘特图用于后续项目管理 。 < / li >
< / ol >
< / section >
< div class = "task-actions" >
< button type = "button" class = "btn-nav btn-submit" : disabled = "uploadingGantt || !stepTwoPassed" @click ="saveAndGoToStepFour" > 保 存 并 进 入 第 4 步 < el -icon > < ArrowRight / > < / e l - i c o n > < / b u t t o n >
< button type = "button" class = "btn-nav" :disabled ="uploadingGantt" @click ="resetStepThree" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 本 步 < / b u t t o n >
< / div >
< / template >
< template v -else -if = " currentStep = = = 4 " >
< div class = "step-title-row" >
< div > < p class = "section-eyebrow" > Step 04 < / p > < h3 > { { trainingSteps [ 3 ] } } < / h3 > < / div >
< span class = "step-chip" > 并行协同复盘 < / span >
< / div >
< p v-if ="!stepThreePassed" class="step-warning" > 请 先 上 传 并 保 存 第 3 步 甘 特 图 , 再 完 成 本 步 复 盘 。 < / p >
< section class = "process-reflection" : class = "{ disabled: !stepThreePassed }" >
< label class = "reflection-field" >
< span > ( 1 ) 观察甘特图 , 思考电商运营在产品开发过程中 , 可以提前介入同步完成哪些工作 , 以缩短运营周期 、 提高效率 。 ( 如 “ 预热蓄水 ” “ 内容种草 ” 等 ) < / span >
< textarea v -model .trim = " stepFourForm.parallelWorkAnalysis " : disabled = "!stepThreePassed || checking" placeholder = "请结合甘特图说明可同步推进的电商运营工作、介入节点及预期效率提升" > < / textarea >
< / label >
< div class = "reflection-question" > ( 2 ) 在互联网产品开发管理中 , 你认为哪些环节可以借助AI工具提升效率 ? 请列举至少2个具体应用场景 。 < / div >
< div class = "scenario-grid" >
< label class = "reflection-field" > < span > 场景1 : < / span > < textarea v -model .trim = " stepFourForm.aiScenarioOne " : disabled = "!stepThreePassed || checking" placeholder = "例如:使用 AI 汇总竞品评价与用户需求, 辅助生成PRD初稿" > < / textarea > < / label >
< label class = "reflection-field" > < span > 场景2 : < / span > < textarea v -model .trim = " stepFourForm.aiScenarioTwo " : disabled = "!stepThreePassed || checking" placeholder = "例如:使用 AI 生成电商详情页文案、主图脚本或运营素材" > < / textarea > < / label >
< / div >
< / section >
< / template >
< div v -else class = "step-placeholder" > < p > 第 { { currentStep } } 步内容待配置 < / p > < span > 第 1 步答案已保存 , 可从上方步骤栏返回查看 。 < / span > < / div >
< / section >
< / section >
< / main >
< TrainingAiSidebar
study - tip = "请根据产品开发流程中任务对总进度和最终落地效果的影响,判断其是否为关键任务。"
reference - title = "关键任务判断提示"
reference - text = "· 关键任务:直接影响产品定义、核心技术验证、量产交付或上市节奏<br />· 非关键任务:可在核心任务约束下进行资源协调,不直接阻塞整体排期"
: progress - items = "progressItems"
> < / TrainingAiSidebar >
< / div >
< / template >
< script setup >
import { ArrowRight , CircleCheck , Operation , RefreshLeft , Upload } from "@element-plus/icons-vue" ;
import * as XLSX from "xlsx" ;
import { checkProductDevelopmentProcessStepOne , checkProductDevelopmentProcessStepTwo , checkProductDevelopmentProcessStepThree , checkProductDevelopmentProcessStepFour , getStudentTrainingAnswer , saveStudentTrainingAnswer , uploadStudentTrainingFile } from "@/api/studentTrainingAnswer" ;
import { getTrainingTaskByKey } from "@/api/trainingTask" ;
import { parseTrainingArray } from "@/views/training/taskKeyMap" ;
import { isExternal } from "@/utils/validate" ;
import useUserStore from "@/store/modules/user" ;
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue" ;
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue" ;
const { proxy } = getCurrentInstance ( ) ;
const userStore = useUserStore ( ) ;
const TASK _KEY = "product-development-process" ;
const GANTT _PRACTICE _GUIDE _URL = "/file/training-materials/product-development-process/3.甘特图制作实训操作说明.docx" ;
const GANTT _PRACTICE _GUIDE _NAME = "3.甘特图制作实训操作说明.docx" ;
const GANTT _TEMPLATE _URL = "/file/training-materials/product-development-process/3.项目进度管理表-项目进度甘特图模版.xlsx" ;
const GANTT _TEMPLATE _NAME = "3.项目进度管理表-项目进度甘特图模版.xlsx" ;
const CLASSIFICATION _OPTIONS = [ "关键任务" , "非关键任务" ] ;
const DEFAULT _STEPS = [ "产品开发关键任务识别" , "任务依赖关系分析" , "开发排期制定" , "流程复盘优化" ] ;
const PROCESS _TASKS = [
[ "需求锚定" , "1.1" , "竞品调研与用户需求分析" ] , [ "" , "1.2" , "产品概念确认与核心卖点提炼" ] ,
[ "产品定义" , "2.1" , "产品需求文档( PRD) 撰写与评审" ] , [ "" , "2.2" , "外观ID设计" ] , [ "" , "2.3" , "功能规格确认" ] ,
[ "开发验证" , "3.1" , "硬件方案选型与原理图设计" ] , [ "" , "3.2" , "软件架构及软硬件协同设计" ] , [ "" , "3.3" , "结构设计评审" ] , [ "" , "3.4" , "功能手板制作与外观评审" ] , [ "" , "3.5" , "工程验证测试与设计验证测试" ] , [ "" , "3.6" , "PVT小批量试产" ] ,
[ "供应链对接" , "4.1" , "供应商评估与选定" ] , [ "" , "4.2" , "模具正式开模与试模修模" ] , [ "" , "4.3" , "产品认证及质量检测" ] , [ "" , "4.4" , "BOM成本核算与采购合同签订" ] ,
[ "上市推广" , "5.1" , "产品量产及入库" ] , [ "" , "5.2" , "电商详情页全渠道上线" ] , [ "" , "5.3" , "正式开售与首销爆发" ] , [ "" , "5.4" , "首批用户评价监控与差评预警" ] ,
] ;
const defaultTask = {
title : "产品开发主要流程" ,
background : "产品开发需要识别影响整体交付、量产和上市节奏的关键环节,并据此安排资源与排期。" ,
goals : [ "理解产品开发的主要阶段与任务" , "识别影响项目整体进度的关键任务" , "形成后续排期和资源协调的判断依据" ] ,
requirement : "请按任务对整体开发进度和最终落地效果的影响,判断其为关键任务或非关键任务。" ,
} ;
const taskConfig = ref ( null ) ;
const processRows = ref ( createProcessRows ( ) ) ;
const currentStep = ref ( 1 ) ;
const briefOpen = ref ( false ) ;
const checking = ref ( false ) ;
const draftReady = ref ( false ) ;
const checkResult = ref ( null ) ;
const stepOnePassed = ref ( false ) ;
const stepTwoPassed = ref ( false ) ;
const stepThreePassed = ref ( false ) ;
const ganttImage = ref ( { url : "" , name : "" } ) ;
const uploadingGantt = ref ( false ) ;
const stepFourForm = ref ( { parallelWorkAnalysis : "" , aiScenarioOne : "" , aiScenarioTwo : "" } ) ;
const taskTitle = computed ( ( ) => taskConfig . value ? . taskName || defaultTask . title ) ;
const taskBackground = computed ( ( ) => taskConfig . value ? . background || defaultTask . background ) ;
const taskGoals = computed ( ( ) => parseArray ( taskConfig . value ? . objectives , defaultTask . goals ) ) ;
const taskRequirement = computed ( ( ) => taskConfig . value ? . requirements || defaultTask . requirement ) ;
const trainingSteps = computed ( ( ) => parseTrainingArray ( taskConfig . value ? . steps , DEFAULT _STEPS ) ) ;
const feedbackByTaskNo = computed ( ( ) => Object . fromEntries ( ( checkResult . value ? . items || [ ] ) . map ( ( item ) => [ item . taskNo , item ] ) ) ) ;
const hasCompletedStepOne = computed ( ( ) => stepOnePassed . value ) ;
const progressItems = computed ( ( ) => [
{ text : ` 进行中: ${ trainingSteps . value [ Math . max ( 0 , currentStep . value - 1 ) ] || taskTitle . value } ` , status : "doing" } ,
{ text : "完成本步后进入第 2 步" , status : "" } ,
{ text : "可选:导出实训成果" , status : "" } ,
] ) ;
onMounted ( async ( ) => {
await loadTaskConfig ( ) ;
loadDraft ( ) ;
await loadSavedAnswer ( ) ;
draftReady . value = true ;
} ) ;
watch ( ( ) => JSON . stringify ( buildDraft ( ) ) , persistDraft ) ;
function createProcessRows ( ) {
return PROCESS _TASKS . map ( ( [ stage , taskNo , task ] ) => ( { stage , taskNo , task , classification : "" , startDate : "" , endDate : "" , dependency : "" } ) ) ;
}
function parseArray ( value , fallback ) {
if ( Array . isArray ( value ) ) return value . length ? value : fallback ;
if ( ! value ) return fallback ;
try {
const parsed = JSON . parse ( value ) ;
return Array . isArray ( parsed ) && parsed . length ? parsed . map ( ( item ) => String ( item ? . name ? ? item ) . trim ( ) ) . filter ( Boolean ) : fallback ;
} catch ( error ) {
const list = String ( value ) . split ( /[; ;|]/ ) . map ( ( item ) => item . trim ( ) ) . filter ( Boolean ) ;
return list . length ? list : fallback ;
}
}
async function loadTaskConfig ( ) {
try { taskConfig . value = ( await getTrainingTaskByKey ( TASK _KEY ) ) ? . data || null ; } catch ( error ) { taskConfig . value = null ; }
}
async function loadSavedAnswer ( ) {
try {
const answer = ( await getStudentTrainingAnswer ( TASK _KEY ) ) ? . data ;
if ( answer ? . step1Answer ) {
restoreOutcome ( answer . step1Answer ) ;
stepOnePassed . value = processRows . value . every ( ( row ) => CLASSIFICATION _OPTIONS . includes ( row . classification ) ) ;
clearDraft ( ) ;
}
if ( answer ? . step2Answer ) restoreStepTwoOutcome ( answer . step2Answer ) ;
if ( answer ? . step2Answer ) stepTwoPassed . value = true ;
if ( answer ? . step3Answer ) {
restoreStepThreeOutcome ( answer . step3Answer ) ;
stepThreePassed . value = true ;
}
if ( answer ? . step4Answer ) restoreStepFourOutcome ( answer . step4Answer ) ;
const savedStep = Number ( answer ? . currentStep ) ;
if ( Number . isInteger ( savedStep ) && savedStep > 0 ) currentStep . value = savedStep ;
} catch ( error ) {
// Keep the browser draft available while the answer endpoint is unavailable.
}
}
function restoreOutcome ( value ) {
try {
const parsed = typeof value === "string" ? JSON . parse ( value ) : value ;
const savedRows = Array . isArray ( parsed ? . items ) ? parsed . items : [ ] ;
const byTaskNo = Object . fromEntries ( savedRows . map ( ( item ) => [ item . taskNo , item ] ) ) ;
processRows . value = createProcessRows ( ) . map ( ( row ) => ( { ... row , classification : byTaskNo [ row . taskNo ] ? . classification || "" } ) ) ;
} catch ( error ) {
processRows . value = createProcessRows ( ) ;
}
}
function restoreStepTwoOutcome ( value ) {
try {
const parsed = typeof value === "string" ? JSON . parse ( value ) : value ;
const byTaskNo = Object . fromEntries ( ( Array . isArray ( parsed ? . items ) ? parsed . items : [ ] ) . map ( ( item ) => [ item . taskNo , item ] ) ) ;
processRows . value = processRows . value . map ( ( row ) => ( {
... row ,
startDate : byTaskNo [ row . taskNo ] ? . startDate || "" ,
endDate : byTaskNo [ row . taskNo ] ? . endDate || "" ,
dependency : byTaskNo [ row . taskNo ] ? . dependency || "" ,
} ) ) ;
} catch ( error ) {
// Ignore a malformed historical second-step answer.
}
}
function restoreStepThreeOutcome ( value ) {
try {
const parsed = typeof value === "string" ? JSON . parse ( value ) : value ;
ganttImage . value = { url : parsed ? . ganttImageUrl || "" , name : parsed ? . ganttImageName || "" } ;
} catch ( error ) {
ganttImage . value = { url : "" , name : "" } ;
}
}
function restoreStepFourOutcome ( value ) {
try {
const parsed = typeof value === "string" ? JSON . parse ( value ) : value ;
stepFourForm . value = {
parallelWorkAnalysis : parsed ? . parallelWorkAnalysis || "" ,
aiScenarioOne : parsed ? . aiScenarioOne || "" ,
aiScenarioTwo : parsed ? . aiScenarioTwo || "" ,
} ;
} catch ( error ) {
stepFourForm . value = { parallelWorkAnalysis : "" , aiScenarioOne : "" , aiScenarioTwo : "" } ;
}
}
function getLocalDraftKey ( ) {
const info = userStore . userInfo || { } ;
return ` ${ TASK _KEY } :draft: ${ info . userId || info . user _id || info . username || info . userName || "anonymous" } ` ;
}
function buildOutcome ( ) {
return { title : taskTitle . value , items : processRows . value . map ( ( { taskNo , classification } ) => ( { taskNo , classification } ) ) } ;
}
function buildStepTwoOutcome ( ) {
return {
title : taskTitle . value ,
items : processRows . value . map ( ( { taskNo , classification , startDate , endDate , dependency } ) => ( { taskNo , classification , startDate , endDate , dependency } ) ) ,
} ;
}
function getStepTwoValidationMessage ( ) {
for ( const row of processRows . value ) {
if ( ! String ( row . dependency || "" ) . trim ( ) ) return ` 请填写任务 ${ row . taskNo } 的项目依赖关系 ` ;
if ( ! String ( row . startDate || "" ) . trim ( ) ) return ` 请填写任务 ${ row . taskNo } 的开始时间 ` ;
if ( ! String ( row . endDate || "" ) . trim ( ) ) return ` 请填写任务 ${ row . taskNo } 的完成时间 ` ;
if ( row . startDate > row . endDate ) return ` 任务 ${ row . taskNo } 的完成时间不能早于开始时间 ` ;
}
return "" ;
}
function buildStepThreeOutcome ( ) {
return { title : taskTitle . value , ganttImageUrl : ganttImage . value . url , ganttImageName : ganttImage . value . name } ;
}
function buildStepFourOutcome ( ) {
return { ... stepFourForm . value } ;
}
function buildDraft ( ) { return { step1 : buildOutcome ( ) , step2 : buildStepTwoOutcome ( ) , step3 : buildStepThreeOutcome ( ) , step4 : buildStepFourOutcome ( ) , currentStep : currentStep . value } ; }
function persistDraft ( ) {
if ( ! draftReady . value ) return ;
try { localStorage . setItem ( getLocalDraftKey ( ) , JSON . stringify ( buildDraft ( ) ) ) ; } catch ( error ) { /* Browser draft is optional. */ }
}
function loadDraft ( ) {
try {
const raw = localStorage . getItem ( getLocalDraftKey ( ) ) ;
if ( raw ) {
const draft = JSON . parse ( raw ) ;
restoreOutcome ( draft ? . step1 || draft ) ;
if ( draft ? . step2 ) restoreStepTwoOutcome ( draft . step2 ) ;
if ( draft ? . step3 ) restoreStepThreeOutcome ( draft . step3 ) ;
if ( draft ? . step4 ) restoreStepFourOutcome ( draft . step4 ) ;
if ( Number ( draft ? . currentStep ) > 0 ) currentStep . value = Number ( draft . currentStep ) ;
}
} catch ( error ) { clearDraft ( ) ; }
}
function clearDraft ( ) {
try { localStorage . removeItem ( getLocalDraftKey ( ) ) ; } catch ( error ) { /* Ignore storage failures. */ }
}
function feedbackClass ( taskNo ) {
const feedback = feedbackByTaskNo . value [ taskNo ] ;
return feedback ? ( feedback . correct ? "is-correct" : "is-incorrect" ) : "" ;
}
function clearFeedback ( taskNo ) {
if ( ! feedbackByTaskNo . value [ taskNo ] ) return ;
checkResult . value = { ... checkResult . value , items : checkResult . value . items . filter ( ( item ) => item . taskNo !== taskNo ) } ;
}
async function saveAndGoToStepTwo ( ) {
if ( checking . value ) return ;
checking . value = true ;
try {
const response = await checkProductDevelopmentProcessStepOne ( buildOutcome ( ) . items ) ;
checkResult . value = response ? . data || null ;
if ( ! checkResult . value ? . allCorrect ) {
proxy ? . $modal ? . msgError ? . ( "存在分类错误,请根据提示修改后再进入第 2 步" ) ;
return ;
}
await saveStudentTrainingAnswer ( TASK _KEY , { step1Answer : JSON . stringify ( buildOutcome ( ) ) , currentStep : 2 , submitted : false , saveAction : "SAVE" } ) ;
clearDraft ( ) ;
stepOnePassed . value = true ;
currentStep . value = 2 ;
proxy ? . $modal ? . msgSuccess ( "已保存,已进入第 2 步" ) ;
} catch ( error ) {
proxy ? . $modal ? . msgError ? . ( error ? . message || "请完成全部任务分类后再判题" ) ;
} finally {
checking . value = false ;
}
}
async function resetTraining ( ) {
processRows . value = createProcessRows ( ) ;
currentStep . value = 1 ;
checkResult . value = null ;
stepOnePassed . value = false ;
stepTwoPassed . value = false ;
stepThreePassed . value = false ;
ganttImage . value = { url : "" , name : "" } ;
stepFourForm . value = { parallelWorkAnalysis : "" , aiScenarioOne : "" , aiScenarioTwo : "" } ;
clearDraft ( ) ;
try {
checking . value = true ;
await saveStudentTrainingAnswer ( TASK _KEY , { step1Answer : JSON . stringify ( buildOutcome ( ) ) , currentStep : 1 , submitted : false , saveAction : "RESET" } ) ;
} catch ( error ) {
// The local reset remains effective if the backend is temporarily unavailable.
} finally {
checking . value = false ;
}
proxy ? . $modal ? . msgSuccess ( "已清空填写内容" ) ;
}
async function saveAndGoToStepThree ( ) {
if ( checking . value || ! hasCompletedStepOne . value ) return ;
const validationMessage = getStepTwoValidationMessage ( ) ;
if ( validationMessage ) {
proxy ? . $modal ? . msgWarning ? . ( validationMessage ) ;
return ;
}
checking . value = true ;
try {
await checkProductDevelopmentProcessStepTwo ( buildStepTwoOutcome ( ) . items ) ;
await saveStudentTrainingAnswer ( TASK _KEY , { step2Answer : JSON . stringify ( buildStepTwoOutcome ( ) ) , currentStep : 3 , submitted : false , saveAction : "SAVE" } ) ;
clearDraft ( ) ;
stepTwoPassed . value = true ;
currentStep . value = 3 ;
proxy ? . $modal ? . msgSuccess ( "已保存,已进入第 3 步" ) ;
} catch ( error ) {
proxy ? . $modal ? . msgError ? . ( error ? . message || "请完整填写任务工期和项目依赖关系" ) ;
} finally {
checking . value = false ;
}
}
function resetStepTwo ( ) {
processRows . value = processRows . value . map ( ( row ) => ( { ... row , startDate : "" , endDate : "" , dependency : "" } ) ) ;
proxy ? . $modal ? . msgSuccess ( "已清空第 2 步填写内容" ) ;
}
async function uploadGanttImage ( event ) {
const file = event . target . files ? . [ 0 ] ;
if ( ! file || ! stepTwoPassed . value ) return ;
if ( ! /^image\/(png|jpeg|webp|gif)$/i . test ( file . type ) ) {
proxy ? . $modal ? . msgError ? . ( "请上传 PNG、JPG、JPEG、WEBP 或 GIF 格式的甘特图" ) ;
event . target . value = "" ;
return ;
}
uploadingGantt . value = true ;
try {
const formData = new FormData ( ) ;
formData . append ( "file" , file ) ;
const response = await uploadStudentTrainingFile ( formData ) ;
ganttImage . value = {
url : response ? . url || response ? . fileName || "" ,
name : response ? . originalFilename || file . name ,
} ;
if ( ! ganttImage . value . url ) throw new Error ( "上传结果缺少文件地址" ) ;
proxy ? . $modal ? . msgSuccess ( "甘特图上传成功" ) ;
} catch ( error ) {
ganttImage . value = { url : "" , name : "" } ;
proxy ? . $modal ? . msgError ? . ( error ? . message || "甘特图上传失败" ) ;
} finally {
uploadingGantt . value = false ;
event . target . value = "" ;
}
}
async function saveAndGoToStepFour ( ) {
if ( uploadingGantt . value || ! stepTwoPassed . value ) return ;
checking . value = true ;
try {
await checkProductDevelopmentProcessStepThree ( buildStepThreeOutcome ( ) ) ;
await saveStudentTrainingAnswer ( TASK _KEY , { step3Answer : JSON . stringify ( buildStepThreeOutcome ( ) ) , currentStep : 4 , submitted : false , saveAction : "SAVE" } ) ;
clearDraft ( ) ;
stepThreePassed . value = true ;
currentStep . value = 4 ;
proxy ? . $modal ? . msgSuccess ( "已保存,已进入第 4 步" ) ;
} catch ( error ) {
proxy ? . $modal ? . msgError ? . ( error ? . message || "请先上传甘特图" ) ;
} finally {
checking . value = false ;
}
}
function resetStepThree ( ) {
ganttImage . value = { url : "" , name : "" } ;
proxy ? . $modal ? . msgSuccess ( "已清空第 3 步上传内容" ) ;
}
async function submitTraining ( ) {
if ( checking . value || ! stepThreePassed . value ) return ;
checking . value = true ;
try {
await checkProductDevelopmentProcessStepFour ( buildStepFourOutcome ( ) ) ;
await saveStudentTrainingAnswer ( TASK _KEY , { step4Answer : JSON . stringify ( buildStepFourOutcome ( ) ) , currentStep : 4 , submitted : true , saveAction : "SUBMIT" } ) ;
clearDraft ( ) ;
proxy ? . $modal ? . msgSuccess ( "任务提交成功" ) ;
} catch ( error ) {
proxy ? . $modal ? . msgError ? . ( error ? . message || "请完成并行协同复盘与两个AI应用场景" ) ;
} finally {
checking . value = false ;
}
}
function resetStepFour ( ) {
stepFourForm . value = { parallelWorkAnalysis : "" , aiScenarioOne : "" , aiScenarioTwo : "" } ;
proxy ? . $modal ? . msgSuccess ( "已清空第 4 步填写内容" ) ;
}
function resolveGanttImageUrl ( url ) {
const source = String ( url || "" ) . trim ( ) ;
if ( ! source || source . startsWith ( "data:" ) || source . startsWith ( "blob:" ) ) return source ;
if ( ! isExternal ( source ) ) return source . startsWith ( "/" ) ? source : ` / ${ source } ` ;
try {
const parsed = new URL ( source ) ;
if ( [ "localhost" , "127.0.0.1" , "::1" ] . includes ( parsed . hostname ) ) {
return ` ${ parsed . pathname } ${ parsed . search } ${ parsed . hash } ` ;
}
} catch {
return source ;
}
return source ;
}
function previewGanttImage ( ) {
window . open ( resolveGanttImageUrl ( ganttImage . value . url ) , "_blank" , "noopener,noreferrer" ) ;
}
function downloadPracticeGuide ( ) {
downloadBuiltInFile ( GANTT _PRACTICE _GUIDE _URL , GANTT _PRACTICE _GUIDE _NAME ) ;
}
function downloadGanttTemplate ( ) {
downloadBuiltInFile ( GANTT _TEMPLATE _URL , GANTT _TEMPLATE _NAME ) ;
}
function downloadBuiltInFile ( url , filename ) {
const link = document . createElement ( "a" ) ;
link . href = url ;
link . download = filename ;
document . body . appendChild ( link ) ;
link . click ( ) ;
document . body . removeChild ( link ) ;
}
function exportScheduleList ( ) {
const rows = processRows . value . map ( ( row ) => ( {
阶段 : row . stage ,
任务编号 : row . taskNo ,
具体工作任务 : row . task ,
是否关键任务 : row . classification ,
开始时间 : row . startDate ,
完成时间 : row . endDate ,
项目依赖关系 : row . dependency ,
} ) ) ;
const worksheet = XLSX . utils . json _to _sheet ( rows ) ;
worksheet [ "!cols" ] = [
{ wch : 12 } , { wch : 10 } , { wch : 28 } , { wch : 14 } , { wch : 14 } , { wch : 14 } , { wch : 22 } ,
] ;
const workbook = XLSX . utils . book _new ( ) ;
XLSX . utils . book _append _sheet ( workbook , worksheet , "WBS排期表" ) ;
XLSX . writeFile ( workbook , "产品开发WBS任务拆解与排期表.xlsx" ) ;
proxy ? . $modal ? . msgSuccess ( "列表导出成功" ) ;
}
function exportOutcome ( ) {
const blob = new Blob ( [ JSON . stringify ( buildDraft ( ) , null , 2 ) ] , { type : "application/json;charset=utf-8" } ) ;
const url = URL . createObjectURL ( blob ) ;
const link = document . createElement ( "a" ) ;
link . href = url ;
link . download = "产品开发主要流程-实训成果.json" ;
document . body . appendChild ( link ) ;
link . click ( ) ;
document . body . removeChild ( link ) ;
URL . revokeObjectURL ( url ) ;
proxy ? . $modal ? . msgSuccess ( "导出成功" ) ;
}
< / script >
< style lang = "scss" scoped >
/ *
* 页面级令牌 ( 墨黑极简主题 ) : 纯白画布 + 墨黑主操作 + 中性灰层次 。
* 覆盖全局主色 / 文字 / 边框令牌 , 使所有 var ( -- student - * ) 规则自动重着色 。
* /
. product - process {
/* 主色:教育蓝 */
-- student - accent : # 2563 eb ;
-- student - accent - deep : # 1 d4ed8 ;
-- student - accent - light : # 3 b82f6 ;
-- student - accent - soft : # eff6ff ;
/* 语义色 */
-- student - danger : # ef4444 ;
-- student - success : # 2 ba471 ;
/* 三层次标识(仅小色条) */
-- student - layer - 1 : # 1 d4ed8 ;
-- student - layer - 2 : # 3 b82f6 ;
-- student - layer - 3 : # 93 c5fd ;
/* 结构色:中性灰白系 */
-- student - panel - border : # e5e7eb ;
-- student - card - bg : # ffffff ;
-- student - panel - bg : # fafbfc ;
-- student - text : # 1 f2937 ;
-- student - text - secondary : # 6 b7280 ;
/* Element Plus 主色变量 */
-- el - color - primary : # 2563 eb ;
-- el - color - primary - light - 3 : # 60 a5fa ;
-- el - color - primary - light - 5 : # 93 c5fd ;
-- el - color - primary - light - 7 : # bfdbfe ;
-- el - color - primary - light - 8 : # dbeafe ;
-- el - color - primary - light - 9 : # eff6ff ;
-- el - color - primary - dark - 2 : # 1 d4ed8 ;
-- el - input - hover - border - color : # 3 b82f6 ;
-- el - input - focus - border - color : # 2563 eb ;
-- el - input - focus - ring - color : rgba ( 59 , 130 , 246 , 0.15 ) ;
-- el - textarea - hover - border - color : # 3 b82f6 ;
-- el - textarea - focus - border - color : # 2563 eb ;
/* 紧凑字号梯度 */
-- student - fs - display : 17 px ;
-- student - fs - h1 : 15 px ;
-- student - fs - h2 : 14 px ;
-- student - fs - h3 : 13 px ;
-- student - fs - body : 13 px ;
-- student - fs - body - sm : 12 px ;
-- student - fs - caption : 11 px ;
-- student - fs - button : 13 px ;
-- student - fs - button - lg : 13 px ;
-- student - fs - data : 14 px ;
min - width : 0 ;
color : var ( -- student - text ) ;
}
/* 主列布局:容器栅格与内边距由全局 .student-training-shell 统一提供 */
. process - core {
display : grid ;
gap : 10 px ;
align - content : start ;
}
/* 任务头:白卡 + 左侧教育蓝竖条 + 徽章 + 标题 + 简介 */
. task - header {
position : relative ;
margin : 0 ;
padding : 18 px 20 px 18 px 24 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 12 px ;
background : var ( -- student - card - bg ) ;
overflow : hidden ;
& : : before {
content : "" ;
position : absolute ;
top : 0 ;
bottom : 0 ;
left : 0 ;
width : 4 px ;
background : var ( -- student - accent ) ;
}
}
. task - head - main {
display : grid ;
justify - items : start ;
gap : 8 px ;
min - width : 0 ;
}
. task - title {
margin : 0 ;
color : var ( -- student - text ) ;
font - size : 17 px ;
line - height : 1.35 ;
font - weight : 700 ;
}
. task - brief {
margin : 0 ;
color : var ( -- student - text - secondary ) ;
font - size : 12 px ;
line - height : 1.6 ;
}
. task - brief - text {
display : - webkit - box ;
overflow : hidden ;
- webkit - box - orient : vertical ;
- webkit - line - clamp : 1 ;
}
. task - brief - toggle {
padding : 0 ;
border : 0 ;
color : var ( -- student - accent - deep ) ;
background : transparent ;
font : inherit ;
font - weight : 700 ;
cursor : pointer ;
& : hover {
text - decoration : underline ;
}
}
. task - brief - more {
display : grid ;
gap : 6 px ;
margin - top : 8 px ;
b {
color : var ( -- student - text ) ;
}
ul {
display : grid ;
gap : 4 px ;
margin : 0 ;
padding - left : 18 px ;
}
p {
margin : 0 ;
}
}
/* 工作台:白底卡片 + 12px 圆角 */
. process - workbench {
margin : 0 ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 12 px ;
background : var ( -- student - card - bg ) ;
& : : before {
display : none ! important ;
content : none ! important ;
}
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 操作工具栏横条 : 内联布局 , 去掉悬浮和边框
* === === === === === === === === === === === === === === === === === === === === * /
. task - action - strip {
margin : 0 ;
padding : 10 px 16 px ;
: deep ( . training - task - toolbar ) {
display : flex ;
flex - wrap : wrap ;
align - items : center ;
justify - content : flex - end ;
gap : 8 px ;
}
}
/* 步骤导航(极简文字下划线)的换肤见非 scoped 块 */
. step - name {
display : block ;
overflow : hidden ;
min - width : 0 ;
color : inherit ;
text - overflow : ellipsis ;
}
/* 步骤卡片: 12px 圆角白卡 */
. process - step - card {
margin : 0 ;
padding : 18 px 20 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 12 px ;
background : var ( -- student - card - bg ) ;
}
. step - title - row {
display : flex ;
align - items : flex - start ;
justify - content : space - between ;
gap : 12 px ;
margin - bottom : 10 px ;
}
. step - title - row h3 {
margin : 2 px 0 0 ;
color : var ( -- student - text ) ;
font - size : 15 px ;
font - weight : 700 ;
line - height : 1.35 ;
}
. step - title - actions {
display : flex ;
align - items : center ;
flex - wrap : wrap ;
justify - content : flex - end ;
gap : 8 px ;
}
/* 标题行内联导出:次按钮的紧凑小号(继承 .outline-action 描边与圆角语言) */
. schedule - export - action {
min - height : 36 px ;
height : 36 px ;
padding : 0 14 px ;
font - size : 12 px ;
font - weight : 600 ;
border - radius : 8 px ;
}
/* Step 编号小标签:教育蓝引导文字 */
. section - eyebrow {
margin : 0 ;
color : var ( -- student - accent ) ;
font - size : var ( -- student - fs - caption ) ;
font - weight : 800 ;
line - height : 1.3 ;
letter - spacing : 0.12 em ;
}
/* 步骤小徽章:教育蓝浅底 */
. step - chip {
flex : 0 0 auto ;
padding : 4 px 10 px ;
border : 1 px solid # bfdbfe ;
border - radius : 999 px ;
color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
font - size : var ( -- student - fs - caption ) ;
font - weight : 600 ;
white - space : nowrap ;
}
/* 步骤说明 */
. process - intro {
margin : 0 0 12 px ;
color : # 4 b5563 ;
font - size : 13 px ;
line - height : 1.6 ;
}
/* 表格:纯白 + 中性边框 */
. training - table - wrap {
overflow - x : auto ;
border : 1 px solid # e5e7eb ;
border - radius : 12 px ;
background : # ffffff ;
}
. process - table {
width : 100 % ;
min - width : 920 px ;
border - collapse : collapse ;
table - layout : fixed ;
}
. process - table th {
padding : 9 px 10 px ;
border - bottom : 1 px solid # e5e7eb ;
color : # 374151 ;
background : # ffffff ;
font - size : 12 px ;
font - weight : 700 ;
text - align : center ;
}
. process - table td {
padding : 9 px 10 px ;
border - top : 1 px solid # f0f1f3 ;
color : # 374151 ;
background : # ffffff ;
vertical - align : middle ;
}
. stage - cell ,
. task - no - cell {
color : # 1 f2937 ;
font - size : 12 px ;
font - weight : 700 ;
text - align : center ;
}
. task - cell {
color : # 374151 ;
font - size : 12 px ;
line - height : 1.55 ;
text - align : center ;
}
/* 下拉与日期输入:中性描边,聚焦墨黑 */
. select - cell select ,
. schedule - table input {
box - sizing : border - box ;
width : 100 % ;
min - height : 34 px ;
padding : 5 px 9 px ;
border : 1 px solid # d1d5db ;
border - radius : 8 px ;
outline : none ;
color : # 1 f2937 ;
background : # ffffff ;
font - family : inherit ;
font - size : 12 px ;
}
. select - cell select : focus ,
. schedule - table input : focus {
border - color : # 2563 eb ;
box - shadow : 0 0 0 3 px rgba ( 37 , 99 , 235 , 0.15 ) ;
}
/* 判题反馈:仅文字着色,不铺行底色 */
. classification - feedback {
margin : 5 px 0 0 ;
font - size : 11 px ;
line - height : 1.4 ;
}
tr . is - correct . classification - feedback {
color : var ( -- student - success ) ;
}
tr . is - incorrect . classification - feedback {
color : var ( -- student - danger ) ;
}
/* 判题汇总横幅 */
. classification - summary {
margin : 12 px 0 0 ;
padding : 8 px 12 px ;
border - radius : 8 px ;
color : var ( -- student - danger ) ;
background : rgba ( 217 , 85 , 76 , 0.08 ) ;
font - size : 12 px ;
font - weight : 600 ;
}
. classification - summary . is - success {
color : var ( -- student - success ) ;
background : rgba ( 74 , 157 , 110 , 0.08 ) ;
}
/* 步骤操作按钮:居中紧凑排列(与标准页 .step-navigation 同规格) */
. task - actions {
display : flex ;
flex - wrap : wrap ;
justify - content : center ;
gap : 14 px ;
margin - top : 20 px ;
}
/ *
* 次按钮 ( 清空填写 / 本步 ) : 白底 + 墨黑半透明描边 + 墨黑字 ;
* 与标准样例一致 : 36 px 高 、 8 px 圆角 、 700 字重 、 hover 轻微浮起 。
* /
. btn - nav {
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 8 px ;
min - height : 36 px ;
height : 36 px ;
padding : 0 14 px ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ;
border - radius : 8 px ;
color : var ( -- student - accent ) ;
background : # ffffff ;
font - family : inherit ;
font - size : 13 px ;
font - weight : 700 ;
line - height : 1 ;
text - shadow : none ;
cursor : pointer ;
transition : transform 0.16 s ease , border - color 0.16 s ease , color 0.16 s ease , background 0.16 s ease , box - shadow 0.16 s ease ;
}
. btn - nav : hover : not ( : disabled ) {
transform : translateY ( - 1 px ) ;
border - color : var ( -- student - accent ) ;
color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
box - shadow : 0 3 px 10 px rgba ( 37 , 99 , 235 , 0.12 ) ;
}
. btn - nav : active : not ( : disabled ) {
transform : translateY ( 0 ) ;
}
/* 主按钮(保存进入下一步 / 提交任务):墨黑实底白字 + 中性柔影 */
. btn - nav . btn - submit {
border - color : var ( -- student - accent ) ;
color : # ffffff ;
background : var ( -- student - accent ) ;
box - shadow : 0 3 px 10 px rgba ( 37 , 99 , 235 , 0.28 ) ;
min - height : 36 px ;
height : 36 px ;
padding : 0 14 px ;
border - radius : 8 px ;
}
. btn - nav . btn - submit : hover : not ( : disabled ) {
border - color : var ( -- student - accent - deep ) ;
color : # ffffff ;
background : var ( -- student - accent - deep ) ;
box - shadow : 0 5 px 14 px rgba ( 29 , 78 , 216 , 0.32 ) ;
}
. btn - nav : disabled {
cursor : not - allowed ;
opacity : 0.5 ;
transform : none ;
box - shadow : none ;
}
/* 空步骤占位 */
. step - placeholder {
display : grid ;
place - items : center ;
min - height : 220 px ;
padding : 24 px ;
border : 1 px dashed # d1d5db ;
border - radius : 12 px ;
color : # 9 ca3af ;
background : # fafbfc ;
text - align : center ;
}
. step - placeholder p {
margin : 0 0 6 px ;
color : # 4 b5563 ;
font - size : 14 px ;
font - weight : 600 ;
}
/* 步骤门控提示:中性灰底,安静提示 */
. step - warning {
margin : 0 0 12 px ;
padding : 8 px 12 px ;
border : 1 px solid # e5e7eb ;
border - left : 3 px solid # 9 ca3af ;
border - radius : 8 px ;
color : # 6 b7280 ;
background : # eff6ff ;
font - size : 12 px ;
}
/* 排期表列宽 */
. schedule - table { min - width : 1020 px ; }
. schedule - table . stage - cell { width : 7 % ; }
. schedule - table . task - no - cell { width : 7 % ; }
. schedule - table . task - cell { width : 18 % ; }
. classification - cell {
width : 12 % ;
color : # 1 f2937 ;
font - size : 12 px ;
font - weight : 700 ;
text - align : center ;
}
. schedule - table th : nth - child ( 5 ) ,
. schedule - table td : nth - child ( 5 ) ,
. schedule - table th : nth - child ( 6 ) ,
. schedule - table td : nth - child ( 6 ) { width : 18 % ; }
. schedule - table . dependency - input { min - width : 0 ; }
. schedule - table . dependency - input : : placeholder {
color : # 9 ca3af ;
opacity : 1 ;
}
. schedule - table th : nth - child ( 7 ) ,
. schedule - table td : nth - child ( 7 ) { width : 20 % ; }
. schedule - table input : disabled {
cursor : not - allowed ;
opacity : 0.6 ;
}
/* 甘特图上传区:白底 + 中性虚线,无光晕 */
. gantt - upload - panel {
display : grid ;
justify - items : center ;
gap : 16 px ;
min - height : 260 px ;
padding : 24 px 16 px ;
border : 1 px dashed # d1d5db ;
border - radius : 12 px ;
background : # ffffff ;
}
/* 上传触发器:教育蓝实底主操作 */
. gantt - upload - trigger {
display : grid ;
place - items : center ;
align - content : center ;
gap : 8 px ;
width : 240 px ;
height : 110 px ;
border - radius : 10 px ;
color : # ffffff ;
background : # 2563 eb ;
cursor : pointer ;
transition : background 0.18 s ease ;
}
. gantt - upload - trigger : hover : not ( . disabled ) {
background : # 1 d4ed8 ;
}
. gantt - upload - trigger input {
position : absolute ;
width : 1 px ;
height : 1 px ;
opacity : 0 ;
}
. gantt - upload - trigger . el - icon {
font - size : 22 px ;
color : # ffffff ;
}
. gantt - upload - trigger span {
color : # ffffff ;
font - size : 12 px ;
}
. gantt - upload - trigger . disabled {
cursor : not - allowed ;
background : # 9 ca3af ;
}
. gantt - preview {
width : min ( 100 % , 720 px ) ;
margin : 0 ;
text - align : center ;
}
. gantt - preview img {
display : block ;
width : 100 % ;
max - height : 380 px ;
object - fit : contain ;
border : 1 px solid # e5e7eb ;
border - radius : 8 px ;
background : # fff ;
cursor : zoom - in ;
}
. gantt - preview figcaption {
margin - top : 6 px ;
color : # 9 ca3af ;
font - size : 11 px ;
}
/* 练习指引:白底中性左色条 */
. gantt - guide {
width : 100 % ;
box - sizing : border - box ;
margin - top : 16 px ;
padding : 14 px 18 px ;
border : 1 px solid # e5e7eb ;
border - left : 3 px solid # 2563 eb ;
border - radius : 8 px ;
color : # 4 b5563 ;
background : # ffffff ;
}
. gantt - guide h4 {
margin : 0 0 10 px ;
color : # 1 f2937 ;
font - size : 13 px ;
font - weight : 700 ;
}
. gantt - guide ol {
margin : 0 ;
padding - left : 18 px ;
}
. gantt - guide li {
margin : 6 px 0 ;
font - size : 12 px ;
line - height : 1.6 ;
}
. gantt - guide button {
display : inline - flex ;
align - items : center ;
margin - left : 8 px ;
padding : 2 px 10 px ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ;
border - radius : 6 px ;
color : var ( -- student - accent ) ;
background : # ffffff ;
font - family : inherit ;
font - size : 11 px ;
font - weight : 700 ;
line - height : 1.5 ;
cursor : pointer ;
transition : border - color 0.16 s ease , background 0.16 s ease ;
}
. gantt - guide button : hover {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
}
/* 复盘区 */
. process - reflection {
display : grid ;
gap : 18 px ;
}
. reflection - field {
display : grid ;
gap : 8 px ;
color : # 4 b5563 ;
font - size : 13 px ;
line - height : 1.55 ;
}
. reflection - field span {
color : # 1 f2937 ;
font - weight : 600 ;
}
. reflection - field textarea {
width : 100 % ;
box - sizing : border - box ;
min - height : 150 px ;
resize : vertical ;
padding : 10 px 12 px ;
border : 1 px solid # d1d5db ;
border - radius : 8 px ;
color : # 1 f2937 ;
background : # ffffff ;
font - family : inherit ;
font - size : 13 px ;
line - height : 1.6 ;
}
. reflection - field textarea : focus {
outline : 0 ;
border - color : # 2563 eb ;
box - shadow : 0 0 0 3 px rgba ( 37 , 99 , 235 , 0.15 ) ;
}
. reflection - field textarea : disabled {
cursor : not - allowed ;
opacity : 0.6 ;
background : # eff6ff ;
}
. reflection - question {
color : # 1 f2937 ;
font - size : 13 px ;
font - weight : 600 ;
line - height : 1.6 ;
}
/* 两个场景并排 */
. scenario - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 18 px ;
}
. scenario - grid . reflection - field textarea {
min - height : 150 px ;
}
. process - reflection . disabled {
opacity : 0.7 ;
}
@ media ( max - width : 1100 px ) {
. product - process {
grid - template - columns : 1 fr ;
}
}
@ media ( max - width : 900 px ) {
. product - process {
padding : 12 px ;
}
. process - core {
padding : 14 px ;
border - radius : 16 px ;
}
. step - title - row {
flex - direction : column ;
align - items : stretch ;
}
. step - indicator {
width : 100 % ;
overflow - x : auto ;
}
. step - tab {
flex : 0 0 auto ;
}
. step - chip {
display : none ;
}
. scenario - grid {
grid - template - columns : 1 fr ;
}
/* 按钮区窄屏全宽的强制覆盖见非 scoped 块 */
}
< / style >
< style lang = "scss" >
/* ===== AI 侧栏样式已收敛进 TrainingAiSidebar 组件(墨黑极简),此处不再覆盖 ===== */
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 步骤导航 : 胶囊分段 + 连接 ( 教育蓝主题 )
* 与 new - product - survey . vue 完全一致 , 覆盖全局 ! important 规则
* === === === === === === === === === === === === === === === === === === === === * /
# app . studentLayout section . app - main : has ( . product - process ) . step - indicator {
display : flex ! important ;
flex - wrap : nowrap ! important ;
align - items : stretch ! important ;
justify - content : center ! important ;
gap : 0 ! important ;
width : 100 % ! important ;
margin : 0 0 20 px ! important ;
padding : 0 ! important ;
border : none ! important ;
border - radius : 0 ! important ;
background : transparent ! important ;
box - shadow : none ! important ;
. step - tab {
flex : 1 1 0 ! important ;
max - width : 240 px ! important ;
display : flex ! important ;
flex - direction : row ! important ;
align - items : center ! important ;
justify - content : center ! important ;
gap : 8 px ! important ;
min - width : 0 ! important ;
height : auto ! important ;
min - height : 40 px ! important ;
margin : 0 ! important ;
padding : 8 px 14 px ! important ;
border : 1 px solid # e5e7eb ! important ;
border - right : none ! important ;
border - radius : 0 ! important ;
background : # ffffff ! important ;
box - shadow : none ! important ;
color : # 6 b7280 ! important ;
font - size : 13 px ! important ;
font - weight : 600 ! important ;
line - height : 1.3 ! important ;
white - space : normal ! important ;
text - align : center ! important ;
cursor : pointer ;
transition : all 0.16 s ease ! important ;
& : first - child {
border - radius : 8 px 0 0 8 px ! important ;
}
& : last - child {
border - radius : 0 8 px 8 px 0 ! important ;
border - right : 1 px solid # e5e7eb ! important ;
}
/* 不显示顶部引导线 */
. step - bar { display : none ! important ; }
/* 编号小徽章 */
. step - no {
position : static ! important ;
flex : 0 0 auto ! important ;
display : inline - flex ! important ;
align - items : center ! important ;
justify - content : center ! important ;
min - width : 22 px ! important ;
height : 22 px ! important ;
padding : 0 5 px ! important ;
border - radius : 6 px ! important ;
background : # f3f4f6 ! important ;
color : # 6 b7280 ! important ;
font - family : var ( -- student - font - data ) , ui - monospace , "SF Mono" , Consolas , monospace ! important ;
font - size : 12 px ! important ;
font - weight : 700 ! important ;
line - height : 1 ! important ;
transition : all 0.16 s ease ! important ;
& : : after { display : none ! important ; }
}
. step - name { color : inherit ! important ; }
/* 未到达项 hover: 教育蓝边浅底 */
& : hover : not ( . active ) : not ( . complete ) {
border - color : # 2563 eb ! important ;
background : # eff6ff ! important ;
color : # 2563 eb ! important ;
z - index : 1 ! important ;
. step - no { background : # 2563 eb ! important ; color : # ffffff ! important ; }
}
/* 已完成:绿色编号徽章 */
& . complete : not ( . active ) {
border - color : # e5e7eb ! important ;
color : # 374151 ! important ;
. step - no { background : rgba ( 43 , 164 , 113 , 0.12 ) ! important ; color : # 16915 f ! important ; }
& : hover {
border - color : # 2 ba471 ! important ;
background : # f0fdf6 ! important ;
z - index : 1 ! important ;
. step - no { background : # 2 ba471 ! important ; color : # ffffff ! important ; }
}
}
/* 当前步骤:教育蓝实底白字 */
& . active {
border - color : # 2563 eb ! important ;
background : # 2563 eb ! important ;
color : # ffffff ! important ;
font - weight : 700 ! important ;
z - index : 2 ! important ;
. step - no { background : # ffffff ! important ; color : # 2563 eb ! important ; }
& : hover {
background : # 1 d4ed8 ! important ;
border - color : # 1 d4ed8 ! important ;
}
}
}
}
@ media ( max - width : 900 px ) {
# app . studentLayout section . app - main : has ( . product - process ) . step - indicator {
justify - content : flex - start ! important ;
overflow - x : auto ! important ;
}
# app . studentLayout section . app - main : has ( . product - process ) . step - indicator . step - tab {
flex : 0 0 140 px ! important ;
max - width : none ! important ;
}
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 按钮风格强制同步 : 教育蓝
* 全局 student - training . scss 用 ! important 把 . task - actions 按钮锁成浅蓝大胶囊 ,
* scoped 规则无法取胜 。 这里用 id 锚点 ( 特异性含 # app ) + ! important 稳定覆盖 。
* === === === === === === === === === === === === === === === === === === === === * /
/* Step 02 标题行"导出列表": 36px 紧凑小号 */
# app . studentLayout section . app - main : has ( . product - process ) . step - title - actions button . outline - action {
display : inline - flex ! important ;
align - items : center ! important ;
justify - content : center ! important ;
gap : 5 px ! important ;
width : auto ! important ;
height : 34 px ! important ;
min - height : 34 px ! important ;
padding : 0 14 px ! important ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ! important ;
border - radius : 8 px ! important ;
color : # 2563 eb ! important ;
background : # ffffff ! important ;
box - shadow : none ! important ;
text - shadow : none ! important ;
font - size : 12 px ! important ;
font - weight : 600 ! important ;
line - height : 1 ! important ;
white - space : nowrap ! important ;
}
# app . studentLayout section . app - main : has ( . product - process ) . step - title - actions button . outline - action . el - icon {
color : inherit ! important ;
font - size : 12 px ! important ;
}
# app . studentLayout section . app - main : has ( . product - process ) . step - title - actions button . outline - action : hover {
border - color : # 2563 eb ! important ;
color : # 2563 eb ! important ;
background : # eff6ff ! important ;
transform : none ! important ;
}
/* --- 底部步骤按钮: 36px 标准按钮 --- */
# app . studentLayout section . app - main : has ( . product - process ) . task - actions {
gap : 14 px ! important ;
margin - top : 20 px ! important ;
}
# app . studentLayout section . app - main : has ( . product - process ) . task - actions > button {
display : inline - flex ! important ;
align - items : center ! important ;
justify - content : center ! important ;
gap : 8 px ! important ;
min - width : 0 ! important ;
width : auto ! important ;
height : 34 px ! important ;
min - height : 34 px ! important ;
padding : 0 14 px ! important ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ! important ;
border - radius : 8 px ! important ;
color : # 2563 eb ! important ;
background : # ffffff ! important ;
box - shadow : none ! important ;
text - shadow : none ! important ;
font - size : 13 px ! important ;
font - weight : 700 ! important ;
line - height : 1 ! important ;
transition : transform 0.16 s ease , border - color 0.16 s ease , background 0.16 s ease , box - shadow 0.16 s ease ! important ;
}
# app . studentLayout section . app - main : has ( . product - process ) . task - actions > button . el - icon {
display : inline - flex ! important ;
}
# app . studentLayout section . app - main : has ( . product - process ) . task - actions > button : hover : not ( : disabled ) {
transform : translateY ( - 1 px ) ! important ;
border - color : # 2563 eb ! important ;
color : # 2563 eb ! important ;
background : # eff6ff ! important ;
box - shadow : 0 3 px 10 px rgba ( 37 , 99 , 235 , 0.12 ) ! important ;
}
/* 主按钮:教育蓝实底白字 + 蓝色柔影 */
# app . studentLayout section . app - main : has ( . product - process ) . task - actions > button . btn - submit {
border - color : # 2563 eb ! important ;
color : # ffffff ! important ;
background : # 2563 eb ! important ;
box - shadow : 0 3 px 10 px rgba ( 37 , 99 , 235 , 0.28 ) ! important ;
}
# app . studentLayout section . app - main : has ( . product - process ) . task - actions > button . btn - submit : hover : not ( : disabled ) {
border - color : # 1 d4ed8 ! important ;
color : # ffffff ! important ;
background : # 1 d4ed8 ! important ;
box - shadow : 0 5 px 14 px rgba ( 29 , 78 , 216 , 0.32 ) ! important ;
}
# app . studentLayout section . app - main : has ( . product - process ) . task - actions > button : disabled {
cursor : not - allowed ! important ;
opacity : 0.5 ! important ;
transform : none ! important ;
box - shadow : none ! important ;
}
/* 窄屏:按钮纵向全宽(全局容器 min-width/布局均为 !important, 须在此覆盖) */
@ media ( max - width : 900 px ) {
# app . studentLayout section . app - main : has ( . product - process ) . step - title - actions button . outline - action ,
# app . studentLayout section . app - main : has ( . product - process ) . task - actions > button {
width : 100 % ! important ;
}
# app . studentLayout section . app - main : has ( . product - process ) . task - actions {
flex - direction : column ! important ;
align - items : stretch ! important ;
}
/* task-action-strip 响应式 */
# app . studentLayout section . app - main : has ( . product - process ) . task - action - strip {
position : static ! important ;
}
# app . studentLayout section . app - main : has ( . product - process ) . task - action - strip : deep ( . training - task - toolbar ) {
justify - content : stretch ! important ;
}
# app . studentLayout section . app - main : has ( . product - process ) . task - action - strip : deep ( . training - task - toolbar > * ) {
flex : 1 1 auto ! important ;
}
}
< / style >