< template >
< div class = "product-process student-training-shell" >
< main class = "process-core" >
< section class = "task-header" >
< div class = "task-badge" > < el -icon > < Operation / > < / e l - i c o n > 核 心 实 训 · 产 品 开 发 基 础 < / d i v >
< h1 class = "task-title" > { { taskTitle } } < / h1 >
< / section >
< TrainingTaskBrief :background ="taskBackground" :goals ="taskGoals" :requirements ="taskRequirement" :show-material ="false" / >
< section class = "process-workbench" >
< 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 / > < / e l - i c o n > 导 出 实 训 成 果 < / b u t t o n >
< / div >
< / div >
< nav v-if ="trainingSteps.length" class="step-indicator" aria-label="产品开发主要流程步骤" >
< template v-for ="(step, index) in trainingSteps" :key="`${step}-${index}`" >
< button
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"
@ click = "currentStep = index + 1"
>
< span class = "step-marker" aria -hidden = " true " > { { String ( index + 1 ) . padStart ( 2 , '0' ) } } < / span >
< span class = "step-copy" > < strong class = "step-name" :title ="step" > { { step } } < / strong > < span class = "step-action" > { { currentStep === index + 1 ? '当前步骤' : '点击切换' } } → < / span > < / span >
< / button >
< span v -if = " index < trainingSteps.length - 1 " class = "step-connector" aria -hidden = " true " / >
< / template >
< / 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 >
< span class = "step-chip" > 项目排期练习 < / span >
< / 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 = "如: 1.1;无" 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 ="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 步 < / button >
< 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 = "请结合甘特图说明可同步推进的电商运营工作、介入节点及预期效率提升" / >
< / 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初稿" / > < / label >
< label class = "reflection-field" > < span > 场景2 : < / span > < textarea v -model .trim = " stepFourForm.aiScenarioTwo " : disabled = "!stepThreePassed || checking" placeholder = "例如:使用 AI 生成电商详情页文案、主图脚本或运营素材" / > < / label >
< / div >
< / section >
< div class = "task-actions" >
< button type = "button" class = "btn-nav btn-submit" : disabled = "checking || !stepThreePassed" @click ="submitTraining" > 提 交 任 务 < / button >
< button type = "button" class = "btn-nav" :disabled ="checking" @click ="resetStepFour" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 本 步 < / b u t t o n >
< / div >
< / 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"
/ >
< / div >
< / template >
< script setup >
import { Download , 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 useUserStore from "@/store/modules/user" ;
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue" ;
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue" ;
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue" ;
const { proxy } = getCurrentInstance ( ) ;
const userStore = useUserStore ( ) ;
const TASK _KEY = "product-development-process" ;
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 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 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 ;
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 previewGanttImage ( ) {
window . open ( ganttImage . value . url , "_blank" , "noopener,noreferrer" ) ;
}
function downloadPracticeGuide ( ) {
const content = [
"产品开发甘特图实训操作说明" ,
"1. 使用第 2 步填写的任务编号、开始时间、完成时间和项目依赖关系建立任务表。" ,
"2. 在 Excel 或 WPS 中按日期轴创建堆积条形图;开始时间作为不可见基线,任务工期作为可见条形。" ,
"3. 为关键任务使用醒目颜色,并标注任务名称、开始及完成时间。" ,
"4. 导出或截图为 PNG/JPG 后上传至本步骤。" ,
] . join ( "\n" ) ;
downloadBlob ( new Blob ( [ content ] , { type : "text/plain;charset=utf-8" } ) , "产品开发甘特图-实训操作说明.txt" ) ;
}
function downloadGanttTemplate ( ) {
const rows = processRows . value . map ( ( row ) => ( {
阶段 : row . stage ,
任务编号 : row . taskNo ,
具体工作任务 : row . task ,
是否关键任务 : row . classification ,
开始时间 : row . startDate ,
完成时间 : row . endDate ,
项目依赖关系 : row . dependency ,
} ) ) ;
const sheet = XLSX . utils . json _to _sheet ( rows ) ;
sheet [ "!cols" ] = [ { wch : 16 } , { wch : 12 } , { wch : 32 } , { wch : 16 } , { wch : 14 } , { wch : 14 } , { wch : 24 } ] ;
const workbook = XLSX . utils . book _new ( ) ;
XLSX . utils . book _append _sheet ( workbook , sheet , "甘特图任务表" ) ;
XLSX . writeFile ( workbook , "产品开发甘特图绘制模板.xlsx" ) ;
}
function downloadBlob ( blob , filename ) {
const url = URL . createObjectURL ( blob ) ;
const link = document . createElement ( "a" ) ;
link . href = url ;
link . download = filename ;
document . body . appendChild ( link ) ;
link . click ( ) ;
document . body . removeChild ( link ) ;
URL . revokeObjectURL ( url ) ;
}
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 >
. product - process {
position : relative ;
z - index : 2 ;
display : grid ;
grid - template - columns : minmax ( 720 px , 1 fr ) 340 px ;
gap : 24 px ;
width : 100 % ;
min - height : calc ( 100 vh - 106 px ) ;
padding : 24 px 24 px 32 px ;
color : # e8f7ff ;
background : transparent ;
}
. process - core {
min - width : 0 ;
padding : 20 px ;
border : 1 px solid rgba ( 0 , 180 , 255 , 0.25 ) ;
border - radius : 24 px ;
background : rgba ( 8 , 18 , 28 , 0.6 ) ;
box - shadow : 0 22 px 48 px rgba ( 0 , 0 , 0 , 0.22 ) , inset 0 1 px 0 rgba ( 169 , 229 , 255 , 0.08 ) ;
backdrop - filter : blur ( 8 px ) ;
}
. task - header { margin - bottom : 16 px ; }
. task - badge { display : inline - flex ; align - items : center ; gap : 7 px ; margin - bottom : 12 px ; padding : 4 px 14 px ; border : 1 px solid rgba ( 0 , 170 , 255 , 0.5 ) ; border - radius : 30 px ; color : # d6f6ff ; background : rgba ( 0 , 170 , 255 , 0.2 ) ; font - size : 12 px ; font - weight : 700 ; }
. task - title { margin : 0 ; color : transparent ; background : linear - gradient ( 135 deg , # fff , # 8 bcbff ) ; - webkit - background - clip : text ; background - clip : text ; font - size : 30 px ; font - weight : 900 ; line - height : 1.3 ; }
. process - workbench { min - width : 0 ; margin : 0 ; padding : 14 px 16 px ; border : 1 px solid rgba ( 82 , 174 , 226 , 0.42 ) ; border - radius : 20 px ; background : rgba ( 0 , 25 , 42 , 0.64 ) ; }
. process - workbench : : before { display : none ! important ; content : none ! important ; }
. workbench - head { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 18 px ; margin - bottom : 22 px ; padding - bottom : 18 px ; border - bottom : 1 px solid rgba ( 0 , 180 , 255 , 0.2 ) ; }
. workbench - actions { display : flex ; flex : 0 0 auto ; flex - wrap : wrap ; justify - content : flex - end ; gap : 10 px ; }
. outline - action , . btn - nav { display : inline - flex ; align - items : center ; justify - content : center ; gap : 8 px ; font - family : inherit ; cursor : pointer ; }
. outline - action , . workbench - actions : deep ( . material - download ) { min - height : 40 px ; padding : 0 18 px ; border : 1 px solid rgba ( 99 , 217 , 255 , 0.55 ) ; border - radius : 999 px ; color : # e9fbff ; background : rgba ( 17 , 126 , 178 , 0.36 ) ; box - shadow : none ; font - size : 14 px ; font - weight : 800 ; line - height : 1 ; }
. outline - action : hover , . workbench - actions : deep ( . material - download : hover ) { border - color : rgba ( 116 , 235 , 255 , 0.85 ) ; color : # fff ; background : rgba ( 18 , 146 , 204 , 0.5 ) ; transform : translateY ( - 1 px ) ; }
. workbench - head -- actions - only { justify - content : flex - end ; margin - bottom : 10 px ; padding - bottom : 0 ; border - bottom : 0 ; }
. step - indicator { position : relative ; display : flex ; align - items : center ; gap : 0 ; width : calc ( 100 % + 24 px ) ; margin : 0 - 12 px 32 px ; padding : 18 px 22 px ; overflow - x : auto ; border : 1 px solid rgba ( 0 , 180 , 255 , 0.3 ) ; border - radius : 18 px ; background : linear - gradient ( 90 deg , rgba ( 6 , 42 , 61 , 0.58 ) , rgba ( 1 , 18 , 31 , 0.42 ) ) , rgba ( 0 , 0 , 0 , 0.3 ) ; box - shadow : inset 0 1 px 0 rgba ( 145 , 233 , 255 , 0.08 ) ; scrollbar - width : thin ; }
. step - tab { display : flex ; flex : 1 0 145 px ; align - items : center ; min - width : 0 ; min - height : 66 px ; padding : 8 px 10 px ; border : 1 px solid rgba ( 68 , 141 , 177 , 0.35 ) ; border - radius : 8 px ; color : # 9 ab3d0 ; background : rgba ( 3 , 27 , 43 , 0.52 ) ; box - shadow : inset 0 1 px 0 rgba ( 135 , 221 , 255 , 0.04 ) ; font - family : inherit ; cursor : pointer ; text - align : left ; transition : transform .2 s ease , color .2 s ease , border - color .2 s ease , background .2 s ease , box - shadow .2 s ease ; }
. step - tab : hover { border - color : rgba ( 89 , 223 , 255 , 0.82 ) ; color : # fff ; background : rgba ( 8 , 92 , 131 , 0.52 ) ; box - shadow : 0 7 px 18 px rgba ( 0 , 0 , 0 , .24 ) , inset 0 1 px 0 rgba ( 152 , 239 , 255 , .16 ) ; transform : translateY ( - 2 px ) ; } . step - tab : hover . step - action { color : # 9 bf4ff ; } . step - tab : focus - visible { outline : 2 px solid # 72 e8ff ; outline - offset : 4 px ; }
. step - tab . active { border - color : rgba ( 89 , 226 , 255 , .85 ) ; color : # fff ; background : linear - gradient ( 115 deg , rgba ( 4 , 109 , 155 , .78 ) , rgba ( 5 , 62 , 102 , .64 ) ) ; box - shadow : 0 0 18 px rgba ( 24 , 183 , 235 , .22 ) , inset 0 1 px 0 rgba ( 176 , 246 , 255 , .2 ) ; } . step - tab . active . step - marker { border - color : # 72 e8ff ; color : # fff ; background : linear - gradient ( 135 deg , # 08 b7e8 , # 1375 d0 ) ; box - shadow : 0 0 0 5 px rgba ( 25 , 179 , 237 , .14 ) , 0 0 18 px rgba ( 46 , 208 , 255 , .58 ) ; } . step - tab . complete { color : # dffbff ; } . step - tab . complete . step - marker { border - color : rgba ( 84 , 230 , 221 , .85 ) ; color : # 062 b37 ; background : # 6 ef0e1 ; box - shadow : 0 0 12 px rgba ( 79 , 231 , 218 , .35 ) ; }
. step - marker { display : inline - flex ; flex : 0 0 38 px ; align - items : center ; justify - content : center ; width : 38 px ; height : 38 px ; border : 1 px solid rgba ( 101 , 176 , 208 , .6 ) ; border - radius : 50 % ; color : # a6c4d9 ; background : rgba ( 3 , 25 , 40 , .8 ) ; font - size : 12 px ; font - weight : 900 ; letter - spacing : .04 em ; } . step - copy { display : grid ; min - width : 0 ; gap : 4 px ; margin - left : 10 px ; } . step - name { display : - webkit - box ; overflow : hidden ; color : inherit ; font - size : 14 px ; font - weight : 800 ; line - height : 1.25 ; text - overflow : ellipsis ; - webkit - box - orient : vertical ; - webkit - line - clamp : 2 ; } . step - action { color : # 66 b6d4 ; font - size : 10 px ; font - weight : 800 ; letter - spacing : .06 em ; line - height : 1 ; } . step - connector { position : relative ; flex : 0 1 40 px ; min - width : 18 px ; height : 2 px ; margin : 0 8 px ; background : rgba ( 98 , 169 , 201 , .34 ) ; } . step - connector : : after { position : absolute ; top : 50 % ; right : - 1 px ; width : 6 px ; height : 6 px ; border - top : 1 px solid # 73 b1ce ; border - right : 1 px solid # 73 b1ce ; content : '' ; transform : translateY ( - 50 % ) rotate ( 45 deg ) ; } . step - tab . complete + . step - connector { background : linear - gradient ( 90 deg , # 6 ef0e1 , # 1 cb8e9 ) ; box - shadow : 0 0 9 px rgba ( 53 , 218 , 235 , .46 ) ; } . step - tab . complete + . step - connector : : after { border - color : # 63 e6f1 ; }
. process - step - card { margin : 0 ; padding : 16 px ; border : 1 px solid rgba ( 82 , 174 , 226 , 0.42 ) ; border - radius : 20 px ; background : rgba ( 0 , 25 , 42 , 0.64 ) ; } . step - title - row { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 18 px ; margin - bottom : 12 px ; } . step - title - row h3 { margin : 4 px 0 0 ; color : # fff ; font - size : 22 px ; font - weight : 900 ; line - height : 1.3 ; } . section - eyebrow { margin : 0 ; color : # 71 dfff ; font - size : 12 px ; font - weight : 800 ; line - height : 1.4 ; } . step - chip { padding : 6 px 12 px ; border : 1 px solid rgba ( 94 , 219 , 255 , .54 ) ; border - radius : 999 px ; color : # a6ebff ; background : rgba ( 9 , 104 , 149 , .2 ) ; font - size : 13 px ; font - weight : 800 ; }
. process - intro { margin : 0 0 18 px ; color : # d7ecff ; font - size : 15 px ; line - height : 1.8 ; } . training - table - wrap { overflow - x : auto ; border : 1 px solid rgba ( 0 , 180 , 255 , .22 ) ; border - radius : 22 px ; background : rgba ( 1 , 14 , 26 , .72 ) ; } . process - table { width : 100 % ; min - width : 920 px ; border - collapse : collapse ; table - layout : fixed ; } . process - table th { padding : 12 px ; border - right : 1 px solid rgba ( 77 , 167 , 220 , .2 ) ; color : # eaffff ; background : rgba ( 8 , 55 , 82 , .98 ) ; font - size : 14 px ; font - weight : 900 ; text - align : center ; } . process - table td { padding : 12 px ; border - top : 1 px solid rgba ( 77 , 167 , 220 , .2 ) ; border - right : 1 px solid rgba ( 77 , 167 , 220 , .2 ) ; color : # d7f1ff ; background : rgba ( 1 , 14 , 26 , .48 ) ; vertical - align : middle ; } . process - table th : last - child , . process - table td : last - child { border - right : 0 ; } . stage - cell { width : 17 % ; color : # eafaff ; font - size : 14 px ; font - weight : 900 ; text - align : center ; } . task - no - cell { width : 12 % ; color : # eafaff ; font - size : 14 px ; font - weight : 800 ; text - align : center ; } . task - cell { color : # cfe8f8 ; font - size : 14 px ; line - height : 1.7 ; text - align : center ; } . select - cell { width : 23 % ; } . select - cell select , . schedule - table input { box - sizing : border - box ; width : 100 % ; min - height : 38 px ; padding : 7 px 10 px ; border : 1 px solid rgba ( 45 , 95 , 126 , .95 ) ; border - radius : 10 px ; outline : none ; color : # eef5ff ; background : # 0 f1a24 ; font : inherit ; } . select - cell select : focus , . schedule - table input : focus { border - color : rgba ( 92 , 224 , 255 , .7 ) ; box - shadow : 0 0 0 3 px rgba ( 0 , 174 , 255 , .13 ) ; } . classification - feedback { margin : 6 px 0 0 ; color : # ff9b8f ; font - size : 12 px ; line - height : 1.5 ; } . is - correct { background : rgba ( 55 , 196 , 147 , .08 ) ; } . is - incorrect { background : rgba ( 255 , 91 , 82 , .1 ) ; } . classification - summary { margin : 16 px 0 0 ; color : # ffbc86 ; } . classification - summary . is - success { color : # 6 ee8b1 ; }
. task - actions { display : flex ; justify - content : center ; gap : 52 px ; margin - top : 36 px ; } . btn - nav { min - width : 172 px ; min - height : 64 px ; padding : 0 28 px ; border : 1 px solid # 2 c83aa ; border - radius : 6 px ; color : # dff7ff ; background : rgba ( 13 , 64 , 88 , .62 ) ; box - shadow : none ; font - size : 24 px ; font - weight : 900 ; line - height : 1 ; } . btn - submit { border - color : # 2 c83aa ; background : rgba ( 13 , 64 , 88 , .62 ) ; } . task - actions . btn - nav . el - icon { display : none ; } . btn - nav : hover : not ( : disabled ) { border - color : # 46 b5e7 ; color : # fff ; background : rgba ( 18 , 85 , 116 , .78 ) ; } . btn - nav : disabled { cursor : not - allowed ; opacity : .6 ; } . step - placeholder { display : grid ; place - items : center ; min - height : 280 px ; padding : 28 px ; border : 1 px dashed rgba ( 101 , 207 , 239 , .42 ) ; border - radius : 18 px ; color : # b8d9ec ; background : rgba ( 1 , 14 , 26 , .42 ) ; text - align : center ; } . step - placeholder p { margin : 0 0 8 px ; color : # eaffff ; font - size : 20 px ; font - weight : 800 ; }
. step - warning { margin : 0 0 16 px ; padding : 10 px 14 px ; border : 1 px solid rgba ( 255 , 173 , 93 , .62 ) ; border - radius : 10 px ; color : # ffd19f ; background : rgba ( 187 , 102 , 27 , .16 ) ; } . schedule - table { min - width : 1180 px ; } . schedule - table . stage - cell { width : 12 % ; } . schedule - table . task - no - cell { width : 8 % ; } . schedule - table . task - cell { width : 21 % ; } . classification - cell { width : 13 % ; color : # eafaff ; font - weight : 800 ; text - align : center ; } . schedule - table . dependency - input { min - width : 180 px ; } . schedule - table input : disabled { cursor : not - allowed ; opacity : .6 ; }
. gantt - upload - panel { display : grid ; justify - items : center ; gap : 22 px ; min - height : 320 px ; padding : 32 px 16 px ; border : 1 px dashed rgba ( 71 , 204 , 255 , .55 ) ; border - radius : 18 px ; background : radial - gradient ( circle at center , rgba ( 15 , 119 , 163 , .16 ) , transparent 55 % ) ; } . gantt - upload - trigger { display : grid ; place - items : center ; align - content : center ; gap : 9 px ; width : 280 px ; height : 138 px ; border - radius : 12 px ; color : # fff ; background : linear - gradient ( 135 deg , # 0 ba6e0 , # 0877 ad ) ; cursor : pointer ; box - shadow : 0 14 px 30 px rgba ( 5 , 126 , 180 , .24 ) ; } . gantt - upload - trigger input { position : absolute ; width : 1 px ; height : 1 px ; opacity : 0 ; } . gantt - upload - trigger . el - icon { font - size : 26 px ; } . gantt - upload - trigger span { color : # cbefff ; font - size : 12 px ; } . gantt - upload - trigger . disabled { cursor : not - allowed ; opacity : .55 ; } . gantt - preview { width : min ( 100 % , 760 px ) ; margin : 0 ; text - align : center ; } . gantt - preview img { display : block ; width : 100 % ; max - height : 410 px ; object - fit : contain ; border : 1 px solid rgba ( 84 , 211 , 255 , .5 ) ; border - radius : 12 px ; background : # fff ; cursor : zoom - in ; } . gantt - preview figcaption { margin - top : 8 px ; color : # a8dce9 ; font - size : 13 px ; } . gantt - guide { margin - top : 26 px ; padding : 18 px 22 px ; border - left : 3 px solid # 23 c4ff ; border - radius : 0 14 px 14 px 0 ; color : # d4edf6 ; background : rgba ( 12 , 75 , 104 , .22 ) ; } . gantt - guide h4 { margin : 0 0 14 px ; color : # fff ; } . gantt - guide ol { margin : 0 ; padding - left : 20 px ; } . gantt - guide li { margin : 11 px 0 ; line - height : 1.7 ; } . gantt - guide button { margin - left : 12 px ; border : 0 ; border - radius : 6 px ; padding : 5 px 10 px ; color : # fff ; background : # 109 ed4 ; cursor : pointer ; }
. process - reflection { display : grid ; gap : 28 px ; } . reflection - field { display : grid ; gap : 12 px ; color : # d8edf4 ; font - size : 16 px ; line - height : 1.7 ; } . reflection - field span { color : # f1fbff ; font - weight : 600 ; } . reflection - field textarea { min - height : 190 px ; resize : vertical ; padding : 14 px ; border : 1 px solid rgba ( 45 , 95 , 126 , .95 ) ; border - radius : 14 px ; color : # eef5ff ; background : # 0 f1a24 ; font : inherit ; line - height : 1.7 ; } . reflection - field textarea : focus { outline : 0 ; border - color : rgba ( 92 , 224 , 255 , .7 ) ; box - shadow : 0 0 0 3 px rgba ( 0 , 174 , 255 , .13 ) ; } . reflection - field textarea : disabled { cursor : not - allowed ; opacity : .55 ; } . reflection - question { color : # f1fbff ; font - size : 16 px ; font - weight : 600 ; line - height : 1.8 ; } . scenario - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 28 px ; } . scenario - grid . reflection - field textarea { min - height : 180 px ; } . process - reflection . disabled { opacity : .72 ; }
@ media ( max - width : 1100 px ) { . product - process { grid - template - columns : 1 fr ; } }
@ media ( max - width : 900 px ) { . product - process { padding : 14 px 14 px 18 px ; } . process - core { padding : 18 px ; border - radius : 24 px ; } . workbench - head , . workbench - actions , . task - actions , . step - indicator , . step - title - row { flex - direction : column ; align - items : stretch ; } . workbench - actions : deep ( . material - download ) , . outline - action { width : 100 % ; } . step - indicator { width : 100 % ; margin : 0 0 32 px ; padding : 16 px ; overflow - x : visible ; border - radius : 20 px ; } . step - tab { flex : 0 0 auto ; width : 100 % ; min - width : 0 ; } . step - connector { flex : 0 0 20 px ; align - self : flex - start ; width : 2 px ; min - width : 0 ; height : 20 px ; margin : 4 px 0 4 px 18 px ; } . step - connector : : after { top : auto ; right : auto ; bottom : - 1 px ; left : 50 % ; transform : translateX ( - 50 % ) rotate ( 135 deg ) ; } . step - chip { display : none ; } . scenario - grid { grid - template - columns : 1 fr ; gap : 18 px ; } . task - actions { gap : 14 px ; } . btn - nav { width : 100 % ; } }
< / style >