@ -119,6 +119,28 @@
< 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 >
@ -136,7 +158,7 @@
< script setup >
import { Download , Operation , RefreshLeft , Upload } from "@element-plus/icons-vue" ;
import * as XLSX from "xlsx" ;
import { checkProductDevelopmentProcessStepOne , checkProductDevelopmentProcessStepTwo , checkProductDevelopmentProcessStepThree , getStudentTrainingAnswer, saveStudentTrainingAnswer , uploadStudentTrainingFile } from "@/api/studentTrainingAnswer" ;
import { checkProductDevelopmentProcessStepOne , checkProductDevelopmentProcessStepTwo , checkProductDevelopmentProcessStepThree , checkProductDevelopmentProcessStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer , uploadStudentTrainingFile } from "@/api/studentTrainingAnswer" ;
import { getTrainingTaskByKey } from "@/api/trainingTask" ;
import useUserStore from "@/store/modules/user" ;
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue" ;
@ -170,8 +192,10 @@ 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 ) ) ;
@ -224,7 +248,11 @@ async function loadSavedAnswer() {
}
if ( answer ? . step2Answer ) restoreStepTwoOutcome ( answer . step2Answer ) ;
if ( answer ? . step2Answer ) stepTwoPassed . value = true ;
if ( answer ? . step3Answer ) restoreStepThreeOutcome ( answer . step3Answer ) ;
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 ) {
@ -267,6 +295,19 @@ function restoreStepThreeOutcome(value) {
}
}
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" } ` ;
@ -287,7 +328,11 @@ function buildStepThreeOutcome() {
return { title : taskTitle . value , ganttImageUrl : ganttImage . value . url , ganttImageName : ganttImage . value . name } ;
}
function buildDraft ( ) { return { step1 : buildOutcome ( ) , step2 : buildStepTwoOutcome ( ) , step3 : buildStepThreeOutcome ( ) , currentStep : currentStep . value } ; }
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 ;
@ -302,6 +347,7 @@ function loadDraft() {
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 ( ) ; }
@ -349,7 +395,9 @@ async function resetTraining() {
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 ;
@ -419,6 +467,7 @@ async function saveAndGoToStepFour() {
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 ) {
@ -433,6 +482,26 @@ function resetStepThree() {
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" ) ;
}
@ -501,6 +570,7 @@ function exportOutcome() {
. process - step - card { padding : 24 px ; border : 1 px solid rgba ( 31 , 185 , 249 , .32 ) ; border - radius : 18 px ; background : rgba ( 1 , 21 , 34 , .65 ) ; } . step - title - row { display : flex ; justify - content : space - between ; align - items : flex - start ; } . step - title - row h3 { margin : 5 px 0 0 ; color : # fff ; font - size : 24 px ; } . step - chip { border : 1 px solid # 38 caff ; border - radius : 999 px ; padding : 7 px 12 px ; color : # 9 aeaff ; } . process - intro { margin : 24 px 0 ; line - height : 1.8 ; color : # c5e4ef ; } . training - table - wrap { overflow - x : auto ; } . process - table { width : 100 % ; min - width : 740 px ; border - collapse : collapse ; color : # dff5ff ; } . process - table th { padding : 14 px ; color : # fff ; background : # 079 ed9 ; font - size : 16 px ; } . process - table td , . process - table th { border : 1 px solid rgba ( 139 , 213 , 241 , .45 ) ; text - align : center ; } . process - table td { padding : 9 px 12 px ; } . stage - cell { width : 16 % ; font - weight : 700 ; } . task - no - cell { width : 15 % ; } . task - cell { width : 37 % ; } . select - cell { width : 32 % ; } . select - cell select { width : 170 px ; max - width : 100 % ; padding : 6 px 10 px ; border : 1 px solid # 6 ea9bd ; color : # 132 c36 ; background : # fff ; font - size : 15 px ; } . classification - feedback { margin : 6 px 0 0 ; color : # ff9b8f ; font - size : 12 px ; } . 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 { justify - content : center ; margin - top : 26 px ; } . btn - submit { border - color : # 36 d9ff ; background : linear - gradient ( 110 deg , # 0 eaae4 , # 0572 ac ) ; font - weight : 700 ; } . btn - nav : disabled { opacity : .6 ; cursor : not - allowed ; } . step - placeholder { padding : 90 px 20 px ; text - align : center ; color : # a8cfdd ; } . step - placeholder p { color : # fff ; font - size : 20 px ; }
. step - warning { margin : 0 0 16 px ; padding : 10 px 14 px ; border : 1 px solid rgba ( 255 , 173 , 93 , .62 ) ; border - radius : 8 px ; color : # ffd19f ; background : rgba ( 187 , 102 , 27 , .16 ) ; } . schedule - table { min - width : 1180 px ; } . schedule - table . stage - cell { width : 13 % ; } . schedule - table . task - no - cell { width : 9 % ; } . schedule - table . task - cell { width : 20 % ; } . classification - cell { width : 13 % ; font - weight : 700 ; } . schedule - table input { box - sizing : border - box ; width : 150 px ; max - width : 100 % ; padding : 7 px 8 px ; border : 1 px solid # 6 ea9bd ; color : # 132 c36 ; background : # fff ; } . schedule - table . dependency - input { width : 190 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 : 14 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 : 10 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 : 10 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 ; background : rgba ( 12 , 75 , 104 , .22 ) ; color : # d4edf6 ; } . 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 ; border : 1 px solid rgba ( 126 , 212 , 243 , .6 ) ; border - radius : 8 px ; padding : 14 px ; color : # eafaff ; background : rgba ( 3 , 35 , 53 , .78 ) ; font : inherit ; line - height : 1.7 ; } . reflection - field textarea : focus { outline : 2 px solid rgba ( 47 , 204 , 255 , .45 ) ; border - color : # 45 d2ff ; } . 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 : 1280 px ) { . product - process { grid - template - columns : 1 fr ; } . step - indicator { overflow - x : auto ; } . step - tab { min - width : 190 px ; } }
@ media ( max - width : 720 px ) { . product - process { padding : 12 px ; } . process - workbench { margin : 12 px ; padding : 14 px ; } . task - header { padding : 18 px ; } . workbench - actions { justify - content : flex - start ; flex - wrap : wrap ; } . step - title - row { gap : 12 px ; } . step - chip { display : none ; } }
@ media ( max - width : 720 px ) { . product - process { padding : 12 px ; } . process - workbench { margin : 12 px ; padding : 14 px ; } . task - header { padding : 18 px ; } . workbench - actions { justify - content : flex - start ; flex - wrap : wrap ; } . step - title - row { gap : 12 px ; } . step - chip { display : none ; } . scenario - grid { grid - template - columns : 1 fr ; gap : 18 px ; } }
< / style >