< template >
< div class = "industry-pest-page student-training-shell" >
< main class = "industry-core" >
< section class = "task-header" >
< div class = "task-head-main" >
< h1 class = "task-title" > { { taskTitle } } < / h1 >
< p 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 >
< / p >
< / div >
< / section >
< section class = "task-action-strip" >
< TrainingTaskToolbar :task-key ="TASK_KEY" :on-submit ="submitTraining" :on-reset ="resetTraining"
: on - export = "exportOutcome" / >
< / section >
< section class = "industry-workbench" >
< nav 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 }]"
@ click = "switchStep(index + 1)" >
< span class = "step-marker" > { { 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" / >
< / template >
< / nav >
< section class = "industry-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" > PEST 分类练习 < / span >
< / div >
< p class = "step-desc" > 请根据案例文档中的案例信息进行 PEST 分析 , 进行上述各项内容归类 , 把文档中相应内容填写到以下四类中 。 < / p >
< TrainingStepOneChoiceQuestions :questions ="taskConfig?.stepOneQuestions"
v - model = "stepOneChoiceAnswers" / >
< div class = "pest-table-wrap" >
< table class = "pest-table" >
< thead >
< tr >
< th > 序号 < / th >
< th > PEST类型 < / th >
< th > 信息资讯 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="(row, index) in pestRows" :key="row.key" >
< td > { { index + 1 } } < / td >
< td class = "type-cell" > { { row . label } } < / td >
< td > < el -input v -model .trim = " form [ row.key ] " type = "textarea"
: autosize = "{ minRows: 2 }" : placeholder = "row.placeholder" / > < / td >
< / tr >
< / tbody >
< / table >
< / 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 >
< div class = "research-guide" >
< p > 请自行收集资料 , 补充 PEST 完善 , 每个维度至少补充 1 条 。 信息收集可参考以下方法 : < / p >
< ol >
< li > 自行通过互联网平台搜索 、 电商平台 ( 京东 / 天猫搜索 “ 智能健身镜 ” ) 、 行业报告网站 ( 艾瑞咨询 、 前瞻产业研究院等 ) 、 新闻资讯 ( 百度 / 头条搜索健身行业关键词 ) 。 < / li >
< li > 使用 AI 大模型智能搜索功能进行信息收集 , 参考提示词见实训任务案例文档 。 < / li >
< / ol >
< p > 请把补充的 PEST 信息资讯填入下表 : < / p >
< / div >
< p v-if ="!stepOnePassed" class="step-warning" > 请 先 完 成 并 保 存 第 1 步 的 PEST 信 息 归 类 , 再 填 写 本 步 骤 。 < / p >
< div class = "pest-table-wrap" >
< table class = "pest-table" >
< thead >
< tr >
< th > 序号 < / th >
< th > PEST类型 < / th >
< th > 补充信息资讯 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="(row, index) in pestRows" :key="row.key" >
< td > { { index + 1 } } < / td >
< td class = "type-cell" > { { row . label } } < / td >
< td > < el -input v -model .trim = " supplementForm [ row.key ] " type = "textarea"
: autosize = "{ minRows: 2 }" : disabled = "!stepOnePassed"
: placeholder = "row.placeholder" / > < / td >
< / tr >
< / tbody >
< / table >
< / 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 = "step-desc" > 基于上述 PEST 信息归类与补充的信息 , 从每个 PEST 维度分别提炼该产品面临的核心机遇与潜在挑战 , 并给出应对思路 。 每个单元格至少填写 1
条 , 可按换行分条 。 < / p >
< p v-if ="!stepTwoPassed" class="step-warning" > 请 先 完 成 并 保 存 第 2 步 的 PEST 补 充 资 讯 , 再 填 写 本 步 骤 。 < / p >
< div class = "analysis-table-wrap" >
< table class = "analysis-table" >
< thead >
< tr >
< th > PEST维度 < / th >
< th > 核心机遇 ( 至少1条 ) < / th >
< th > 潜在挑战 ( 至少1条 ) < / th >
< th > 应对思路建议 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="row in pestAnalysisRows" :key="row.key" >
< td class = "type-cell" > { { row . label } } < / td >
< td > < el -input v -model .trim = " analysisForm [ row.key ] .opportunities " type = "textarea"
: autosize = "{ minRows: 2 }" : disabled = "!stepTwoPassed"
placeholder = "例如:政策、需求或技术带来的增长机会" / > < / td >
< td > < el -input v -model .trim = " analysisForm [ row.key ] .challenges " type = "textarea"
: autosize = "{ minRows: 2 }" : disabled = "!stepTwoPassed"
placeholder = "例如:合规、成本、竞争或使用门槛" / > < / td >
< td > < el -input v -model .trim = " analysisForm [ row.key ] .strategies " type = "textarea"
: autosize = "{ minRows: 2 }" : disabled = "!stepTwoPassed"
placeholder = "例如:针对挑战给出的产品、运营或合规方案" / > < / td >
< / tr >
< / tbody >
< / table >
< / 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 class = "step-desc" > 基于前述 PEST 分析 , 完成行业环境评估 、 市场进入判断与风险总结 。 < / p >
< p v-if ="!stepThreePassed" class="step-warning" > 请 先 完 成 并 保 存 第 3 步 的 机 会 与 挑 战 分 析 , 再 填 写 本 步 骤 。 < / p >
< div class = "conclusion-form" : class = "{ 'is-disabled': !stepThreePassed }" >
< section class = "conclusion-section" >
< h4 > ( 1 ) 总体 PEST 环境评估 < / h4 >
< p > 概括当前宏观环境对该产品的总体友好程度 , 从以下五个等级中选择一个 : < / p >
< div class = "option-row" > < el -radio -group v -model = " conclusionForm.overallAssessment "
: disabled = "!stepThreePassed" class = "option-row-group" > < el -radio
v - for = "item in assessmentOptions" : key = "item" : label = "item"
class = "option-card" > { { item } } < / e l - r a d i o > < / e l - r a d i o - g r o u p > < / d i v >
< / section >
< section class = "conclusion-section" >
< h4 > ( 2 ) 市场进入结论 < / h4 >
< p > 从以下三个结论中选择一个 , 并说明理由 : < / p >
< div class = "option-row decision-options" > < el -radio -group
v - model = "conclusionForm.marketDecision" : disabled = "!stepThreePassed"
class = "option-row-group" > < el -radio v -for = " item in decisionOptions " :key ="item"
: label = "item" class = "option-card" > { { item } } < / e l - r a d i o > < / e l - r a d i o - g r o u p >
< / div > < el -input v -model .trim = " conclusionForm.decisionReason " type = "textarea"
: autosize = "{ minRows: 2 }" : disabled = "!stepThreePassed"
placeholder = "请说明市场进入结论的理由" / >
< / section >
< section class = "conclusion-section" >
< h4 > ( 3 ) 市场进入需要关注的风险点 < / h4 >
< p > 请指出 2 个以上的风险点 。 < / p >
< div class = "risk-grid" > < label > 核心风险点 1 : < el -input v -model .trim = " conclusionForm.riskOne "
type = "textarea" : autosize = "{ minRows: 2 }" : disabled = "!stepThreePassed"
placeholder = "填写核心风险点" / > < / label > < label > 核心风险点 2 : < el -input
v - model . trim = "conclusionForm.riskTwo" type = "textarea"
: autosize = "{ minRows: 2 }" : disabled = "!stepThreePassed"
placeholder = "填写核心风险点" / > < / label > < label > 其他风险点 : < el -input
v - model . trim = "conclusionForm.otherRisks" type = "textarea"
: autosize = "{ minRows: 2 }" : disabled = "!stepThreePassed"
placeholder = "可补充其他风险点" / > < / label > < / div >
< / section >
< section class = "conclusion-section" >
< h4 > ( 4 ) 总结 PEST 环境评估结论 < / h4 > < el -input v -model .trim = " conclusionForm.summary "
type = "textarea" : autosize = "{ minRows: 3 }" : disabled = "!stepThreePassed"
placeholder = "总结该产品的 PEST 环境评估结论" / >
< / section >
< / div >
< / template >
< div v -else class = "step-placeholder" >
< p > 第 { { currentStep } } 步内容待配置 < / p > < span > 前序步骤已保存 , 可从上方步骤栏返回查看 。 < / span >
< / div >
< div class = "step-navigation" >
< div class = "step-nav-left" >
< button v -if = " currentStep > 1 " type=" button " class=" btn - nav btn - ghost " :disabled=" saving "
@ click = "previousStep" >
< el -icon >
< ArrowLeft / >
< / e l - i c o n >
上一步
< / button >
< button type = "button" class = "btn-nav btn-save" :disabled ="saving"
@ click = "saveCurrentProgress" >
< el -icon >
< CircleCheck / >
< / e l - i c o n >
保存当前进度
< / button >
< / div >
< div class = "step-nav-right" >
< span class = "step-progress" > Step { { String ( currentStep ) . padStart ( 2 , "0" ) } } / { {
String ( trainingSteps . length ) . padStart ( 2 , "0" ) } } < / span >
< button v -if = " currentStep < trainingSteps.length " type = "button" class = "btn-nav btn-primary"
: disabled = "saving" @ click = "nextStep" >
下一步
< el -icon >
< ArrowRight / >
< / e l - i c o n >
< / button >
< / div >
< / div >
< / section >
< / section >
< / main >
< TrainingAiSidebar study -tip = " 先 从 案 例 中 提 取 宏 观 环 境 信 息 , 再 按 政 治 、 经 济 、 社 会 、 技 术 四 个 维 度 归 类 和 补 充 。 " reference -title = " PEST 分 类 提 示 "
reference - text = "· 政治:政策、法规、认证、税收、数据合规<br />· 经济:市场规模、成本、商业模式、供应链<br />· 社会:人群需求、消费趋势、生活方式与场景<br />· 技术:产品技术、平台能力、设备互联与创新"
: progress - items = "progressItems" / >
< / div >
< / template >
< script setup >
import { ArrowLeft , ArrowRight , CircleCheck , DataAnalysis } from "@element-plus/icons-vue"
import { checkIndustryDemandAnalysisStepOne , checkIndustryDemandAnalysisStepTwo , checkIndustryDemandAnalysisStepThree , checkIndustryDemandAnalysisStepFour , deleteStudentTrainingAnswer , getStudentTrainingAnswer , saveStudentTrainingAnswer } 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 TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"
import TrainingStepOneChoiceQuestions from "@/views/components/TrainingStepOneChoiceQuestions.vue"
const TASK _KEY = "industry-demand-analysis"
const { proxy } = getCurrentInstance ( )
const userStore = useUserStore ( )
const DEFAULT _STEPS = [ "PEST宏观环境分析" , "行业机会识别" , "需求判断" , "分析结论" ]
const pestRows = [
{ key : "political" , label : "政治环境" , placeholder : "填写政策、法规、认证、税收或数据合规等信息" } ,
{ key : "economic" , label : "经济环境" , placeholder : "填写市场规模、成本、商业模式或供应链等信息" } ,
{ key : "social" , label : "社会环境" , placeholder : "填写用户需求、消费趋势、生活方式或场景变化等信息" } ,
{ key : "technology" , label : "技术环境" , placeholder : "填写产品技术、平台能力、设备互联或创新等信息" } ,
]
const pestAnalysisRows = [
{ key : "political" , label : "政治环境( P) " } ,
{ key : "economic" , label : "经济环境( E) " } ,
{ key : "social" , label : "社会环境( S) " } ,
{ key : "technology" , label : "技术环境( T) " } ,
]
const assessmentOptions = [ "高度友好" , "较为友好" , "中性" , "存在风险" , "风险较高" ]
const decisionOptions = [ "建议进入" , "谨慎进入" , "建议暂缓" ]
const defaultTask = {
title : "行业需求分析" ,
background : "通过 PEST 模型识别影响行业需求的宏观环境因素,为后续产品机会判断提供依据。" ,
goals : [ "掌握 PEST 四维度的分类方法" , "提炼案例中的宏观环境信息" , "形成行业需求分析的基础材料" ] ,
requirement : "请阅读案例资料,将相关信息归类到政治、经济、社会和技术环境中,并补充自行收集的行业资讯。" ,
}
const taskConfig = ref ( null )
const briefOpen = ref ( false )
const currentStep = ref ( 1 )
const saving = ref ( false )
const draftReady = ref ( false )
const stepOnePassed = ref ( false )
const stepTwoPassed = ref ( false )
const stepThreePassed = ref ( false )
const form = ref ( createForm ( ) )
const stepOneChoiceAnswers = ref ( { } )
const supplementForm = ref ( createForm ( ) )
const analysisForm = ref ( createAnalysisForm ( ) )
const conclusionForm = ref ( createConclusionForm ( ) )
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 progressItems = computed ( ( ) => pestRows . map ( ( row ) => ( { text : ` ${ conclusionForm . value . summary ? "已总结" : analysisForm . value [ row . key ] . opportunities ? "已分析" : supplementForm . value [ row . key ] ? "已补充" : form . value [ row . key ] ? "已归类" : "待填写" } : ${ row . label } ` , status : conclusionForm . value . summary || analysisForm . value [ row . key ] . opportunities || supplementForm . value [ row . key ] || form . value [ row . key ] ? "done" : "" } ) ) )
onMounted ( async ( ) => { await loadTaskConfig ( ) ; loadDraft ( ) ; await loadSavedAnswer ( ) ; draftReady . value = true } )
watch ( ( ) => JSON . stringify ( buildDraft ( ) ) , persistDraft )
function createForm ( ) { return { political : "" , economic : "" , social : "" , technology : "" } }
function createAnalysisForm ( ) { return { political : createAnalysisRow ( ) , economic : createAnalysisRow ( ) , social : createAnalysisRow ( ) , technology : createAnalysisRow ( ) } }
function createAnalysisRow ( ) { return { opportunities : "" , challenges : "" , strategies : "" } }
function createConclusionForm ( ) { return { overallAssessment : "" , marketDecision : "" , decisionReason : "" , riskOne : "" , riskTwo : "" , otherRisks : "" , summary : "" } }
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 , form ) ; stepOnePassed . value = true ; clearDraft ( ) }
if ( answer ? . step2Answer ) { restoreOutcome ( answer . step2Answer , supplementForm , "supplements" ) ; stepTwoPassed . value = true }
if ( answer ? . step3Answer ) { restoreAnalysisOutcome ( answer . step3Answer ) ; stepThreePassed . value = true }
if ( answer ? . step4Answer ) restoreConclusionOutcome ( answer . step4Answer )
if ( Number ( answer ? . currentStep ) > 0 ) currentStep . value = Number ( answer . currentStep )
} catch ( error ) { /* Keep the local draft when the answer endpoint is unavailable. */ }
}
function restoreOutcome ( value , target , field = "pest" ) { try { const parsed = typeof value === "string" ? JSON . parse ( value ) : value ; target . value = { ... createForm ( ) , ... ( parsed ? . [ field ] || parsed || { } ) } ; if ( field === "pest" && parsed ? . choiceAnswers && typeof parsed . choiceAnswers === "object" ) stepOneChoiceAnswers . value = parsed . choiceAnswers } catch ( error ) { target . value = createForm ( ) } }
function restoreAnalysisOutcome ( value ) { try { const parsed = typeof value === "string" ? JSON . parse ( value ) : value ; const source = parsed ? . analysis || parsed || { } ; analysisForm . value = Object . fromEntries ( pestAnalysisRows . map ( ( row ) => [ row . key , { ... createAnalysisRow ( ) , ... ( source [ row . key ] || { } ) } ] ) ) } catch ( error ) { analysisForm . value = createAnalysisForm ( ) } }
function restoreConclusionOutcome ( value ) { try { const parsed = typeof value === "string" ? JSON . parse ( value ) : value ; conclusionForm . value = { ... createConclusionForm ( ) , ... ( parsed ? . conclusion || parsed || { } ) } } catch ( error ) { conclusionForm . value = createConclusionForm ( ) } }
function buildStepOneOutcome ( ) { return { title : taskTitle . value , pest : { ... form . value } , choiceAnswers : { ... stepOneChoiceAnswers . value } } }
function buildStepTwoOutcome ( ) { return { title : taskTitle . value , supplements : { ... supplementForm . value } } }
function buildStepThreeOutcome ( ) { return { title : taskTitle . value , analysis : JSON . parse ( JSON . stringify ( analysisForm . value ) ) } }
function buildStepFourOutcome ( ) { return { title : taskTitle . value , conclusion : { ... conclusionForm . value } } }
function buildDraft ( ) { return { step1 : buildStepOneOutcome ( ) , step2 : buildStepTwoOutcome ( ) , step3 : buildStepThreeOutcome ( ) , step4 : buildStepFourOutcome ( ) , currentStep : currentStep . value } }
function getLocalDraftKey ( ) { const info = userStore . userInfo || { } ; return ` ${ TASK _KEY } :draft: ${ info . userId || info . user _id || info . username || info . userName || "anonymous" } ` }
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 , form ) ; restoreOutcome ( draft ? . step2 , supplementForm , "supplements" ) ; restoreAnalysisOutcome ( draft ? . step3 ) ; restoreConclusionOutcome ( 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 switchStep ( step ) { if ( step === 2 && ! stepOnePassed . value ) { proxy ? . $modal ? . msgWarning ( "请先完成并保存第 1 步" ) ; return } if ( step === 3 && ! stepTwoPassed . value ) { proxy ? . $modal ? . msgWarning ( "请先完成并保存第 2 步" ) ; return } if ( step === 4 && ! stepThreePassed . value ) { proxy ? . $modal ? . msgWarning ( "请先完成并保存第 3 步" ) ; return } currentStep . value = step }
async function saveAndGoToStepTwo ( ) { if ( saving . value ) return ; saving . value = true ; try { await checkIndustryDemandAnalysisStepOne ( form . value ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step1Answer : JSON . stringify ( buildStepOneOutcome ( ) ) , currentStep : 2 , submitted : false , saveAction : "SAVE" } ) ; stepOnePassed . value = true ; clearDraft ( ) ; currentStep . value = 2 ; proxy ? . $modal ? . msgSuccess ( "已保存,已进入第 2 步" ) } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请完整填写 PEST 四类信息" ) } finally { saving . value = false } }
async function saveAndGoToStepThree ( ) { if ( saving . value || ! stepOnePassed . value ) return ; saving . value = true ; try { await checkIndustryDemandAnalysisStepTwo ( supplementForm . value ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step2Answer : JSON . stringify ( buildStepTwoOutcome ( ) ) , currentStep : 3 , submitted : false , saveAction : "SAVE" } ) ; stepTwoPassed . value = true ; clearDraft ( ) ; currentStep . value = 3 ; proxy ? . $modal ? . msgSuccess ( "已保存,已进入第 3 步" ) } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请为每个 PEST 维度至少补充 1 条信息" ) } finally { saving . value = false } }
async function saveAndGoToStepFour ( ) { if ( saving . value || ! stepTwoPassed . value ) return ; saving . value = true ; try { await checkIndustryDemandAnalysisStepThree ( { rows : pestAnalysisRows . map ( ( row ) => ( { dimension : row . key , ... analysisForm . value [ row . key ] } ) ) } ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step3Answer : JSON . stringify ( buildStepThreeOutcome ( ) ) , currentStep : 4 , submitted : false , saveAction : "SAVE" } ) ; stepThreePassed . value = true ; clearDraft ( ) ; currentStep . value = 4 ; proxy ? . $modal ? . msgSuccess ( "已保存,已进入第 4 步" ) } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请完成每个 PEST 维度的机遇、挑战和应对思路" ) } finally { saving . value = false } }
async function submitTraining ( ) { if ( saving . value || ! stepThreePassed . value ) return ; saving . value = true ; try { await checkIndustryDemandAnalysisStepFour ( conclusionForm . value ) ; 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 || "请完成 PEST 环境评估结论" ) } finally { saving . value = false } }
async function saveCurrentStepFour ( ) { if ( saving . value || ! stepThreePassed . value ) return ; saving . value = true ; try { await checkIndustryDemandAnalysisStepFour ( conclusionForm . value ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step4Answer : JSON . stringify ( buildStepFourOutcome ( ) ) , currentStep : 4 , submitted : false , saveAction : "SAVE" } ) ; clearDraft ( ) ; proxy ? . $modal ? . msgSuccess ( "第 4 步已保存" ) } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请完成 PEST 环境评估结论" ) } finally { saving . value = false } }
async function saveCurrentProgress ( showMessage = true ) {
if ( saving . value ) return false
saving . value = true
try {
const payloads = {
1 : async ( ) => { await checkIndustryDemandAnalysisStepOne ( form . value ) ; return { step1Answer : JSON . stringify ( buildStepOneOutcome ( ) ) , currentStep : 1 } } ,
2 : async ( ) => { if ( ! stepOnePassed . value ) throw new Error ( "请先完成第 1 步" ) ; await checkIndustryDemandAnalysisStepTwo ( supplementForm . value ) ; return { step2Answer : JSON . stringify ( buildStepTwoOutcome ( ) ) , currentStep : 2 } } ,
3 : async ( ) => { if ( ! stepTwoPassed . value ) throw new Error ( "请先完成第 2 步" ) ; await checkIndustryDemandAnalysisStepThree ( { rows : pestAnalysisRows . map ( ( row ) => ( { dimension : row . key , ... analysisForm . value [ row . key ] } ) ) } ) ; return { step3Answer : JSON . stringify ( buildStepThreeOutcome ( ) ) , currentStep : 3 } } ,
4 : async ( ) => { if ( ! stepThreePassed . value ) throw new Error ( "请先完成第 3 步" ) ; await checkIndustryDemandAnalysisStepFour ( conclusionForm . value ) ; return { step4Answer : JSON . stringify ( buildStepFourOutcome ( ) ) , currentStep : 4 } } ,
}
const answer = await payloads [ currentStep . value ] ? . ( )
if ( ! answer ) return false
await saveStudentTrainingAnswer ( TASK _KEY , { ... answer , submitted : false , saveAction : "SAVE" } )
if ( currentStep . value === 1 ) stepOnePassed . value = true
if ( currentStep . value === 2 ) stepTwoPassed . value = true
if ( currentStep . value === 3 ) stepThreePassed . value = true
clearDraft ( )
if ( showMessage ) proxy ? . $modal ? . msgSuccess ( "当前步骤已保存" )
return true
} catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请完整填写当前步骤内容" ) ; return false } finally { saving . value = false }
}
function previousStep ( ) { if ( currentStep . value > 1 ) currentStep . value -= 1 }
async function nextStep ( ) { if ( currentStep . value < trainingSteps . value . length && await saveCurrentProgress ( false ) ) currentStep . value += 1 }
async function resetTraining ( ) { form . value = createForm ( ) ; stepOneChoiceAnswers . value = { } ; supplementForm . value = createForm ( ) ; analysisForm . value = createAnalysisForm ( ) ; conclusionForm . value = createConclusionForm ( ) ; stepOnePassed . value = false ; stepTwoPassed . value = false ; stepThreePassed . value = false ; currentStep . value = 1 ; clearDraft ( ) ; try { saving . value = true ; await deleteStudentTrainingAnswer ( TASK _KEY ) } catch ( error ) { /* Local clearing still succeeds. */ } finally { saving . value = false } proxy ? . $modal ? . msgSuccess ( "已重新开始本次实训" ) }
function resetStepOne ( ) { form . value = createForm ( ) ; stepOneChoiceAnswers . value = { } ; stepOnePassed . value = false ; proxy ? . $modal ? . msgSuccess ( "已清空第 1 步填写内容" ) }
function resetStepTwo ( ) { supplementForm . value = createForm ( ) ; proxy ? . $modal ? . msgSuccess ( "已清空第 2 步填写内容" ) }
function resetStepThree ( ) { analysisForm . value = createAnalysisForm ( ) ; proxy ? . $modal ? . msgSuccess ( "已清空第 3 步填写内容" ) }
function resetStepFour ( ) { conclusionForm . value = createConclusionForm ( ) ; proxy ? . $modal ? . msgSuccess ( "已清空第 4 步填写内容" ) }
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 >
/* ===== 页面级令牌(教育蓝极简主题) ===== */
. industry - pest - page {
-- 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 ;
-- 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-training-shell, 页面不再重复声明 */
min - width : 0 ;
color : var ( -- student - text ) ;
background : # ffffff ;
}
/* 对齐 foundation 的 .training-core: 只设 gap, 无边框/背景/阴影 */
. industry - core {
display : grid ;
gap : 10 px ;
align - content : start ;
min - width : 0 ;
border : 0 ;
background : transparent ;
box - shadow : none ;
}
/* 任务头:白卡 + 左侧教育蓝竖条 + 12px 圆角 */
. task - header {
position : relative ;
display : flex ;
align - items : flex - start ;
justify - content : space - between ;
gap : 12 px ;
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 - badge ,
. section - eyebrow {
color : var ( -- student - text - secondary ) ;
font - weight : 700 ;
font - size : var ( -- student - fs - caption ) ;
letter - spacing : 0.12 em ;
text - transform : uppercase ;
}
. task - title {
margin : 0 ;
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - display ) ;
font - weight : 700 ;
line - height : 1.35 ;
}
/* task-header 内部主列(对齐 foundation 的 .task-head-main) */
. task - head - main {
display : grid ;
justify - items : start ;
gap : 8 px ;
min - width : 0 ;
flex : 1 1 auto ;
}
. task - brief {
margin : 0 ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body - sm ) ;
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 ;
padding - top : 8 px ;
border - top : 1 px solid var ( -- student - panel - border ) ;
b {
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - body - sm ) ;
}
ul {
display : grid ;
gap : 4 px ;
margin : 0 ;
padding - left : 18 px ;
}
p {
margin : 0 ;
}
}
. 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 ;
}
}
/* step-card 层:透明容器,去掉旧的 border/bg( 对齐 foundation 的 .task-card) */
. industry - step - card {
padding : 0 ;
border : 0 ;
border - radius : 0 ;
background : transparent ;
display : grid ;
gap : 10 px ;
}
. step - title - row {
display : flex ;
justify - content : space - between ;
align - items : flex - start ;
gap : 12 px ;
}
. step - title - row h3 {
margin : 4 px 0 0 ;
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - h1 ) ;
font - weight : 700 ;
line - height : 1.35 ;
}
. step - chip {
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 6 px ;
padding : 3 px 10 px ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - caption ) ;
font - weight : 600 ;
white - space : nowrap ;
}
. step - desc ,
. research - guide {
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.6 ;
}
. step - desc {
margin : 14 px 0 ;
}
. research - guide {
margin : 14 px 0 12 px ;
padding : 12 px 14 px ;
border - left : 3 px solid var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
border - radius : 0 8 px 8 px 0 ;
}
. research - guide p {
margin : 0 0 6 px ;
}
. research - guide ol {
margin : 0 0 6 px ;
padding - left : 20 px ;
}
. research - guide li + li {
margin - top : 4 px ;
}
. step - warning {
margin : 10 px 0 ;
color : # b45309 ;
font - size : var ( -- student - fs - body - sm ) ;
}
/* ===== 表格 ===== */
. pest - table - wrap ,
. analysis - table - wrap {
overflow - x : auto ;
}
. pest - table ,
. analysis - table {
width : 100 % ;
min - width : 720 px ;
border - collapse : collapse ;
}
. pest - table th ,
. pest - table td ,
. analysis - table th ,
. analysis - table td {
border : 1 px solid var ( -- student - panel - border ) ;
}
. pest - table th ,
. analysis - table th {
padding : 10 px 12 px ;
color : var ( -- student - text ) ;
background : # fafbfc ;
font - size : var ( -- student - fs - body - sm ) ;
font - weight : 800 ;
text - align : left ;
}
. pest - table th : first - child ,
. pest - table td : first - child ,
. analysis - table th : first - child ,
. analysis - table td : first - child {
width : 8 % ;
text - align : center ;
}
. pest - table th : nth - child ( 2 ) ,
. pest - table . type - cell {
width : 18 % ;
text - align : center ;
font - weight : 700 ;
}
. analysis - table th : nth - child ( 2 ) ,
. analysis - table . type - cell {
width : 14 % ;
text - align : center ;
font - weight : 700 ;
}
. pest - table textarea ,
. analysis - table textarea ,
. conclusion - section textarea {
box - sizing : border - box ;
display : block ;
width : 100 % ;
min - height : 120 px ;
resize : vertical ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 6 px ;
padding : 10 px 12 px ;
color : var ( -- student - text ) ;
background : # ffffff ;
font : inherit ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.55 ;
}
. pest - table textarea ,
. analysis - table textarea {
border : 0 ;
border - radius : 0 ;
background : transparent ;
padding : 8 px 10 px ;
}
. analysis - table textarea {
min - height : 140 px ;
}
. pest - table textarea : focus ,
. analysis - table textarea : focus ,
. conclusion - section textarea : focus {
outline : 2 px solid rgba ( 37 , 99 , 235 , 0.2 ) ;
outline - offset : - 2 px ;
}
. pest - table textarea : disabled ,
. analysis - table textarea : disabled ,
. conclusion - section textarea : disabled {
cursor : not - allowed ;
color : var ( -- student - text - secondary ) ;
background : var ( -- student - accent - soft ) ;
}
/* ===== 结论表单 ===== */
. conclusion - form {
display : grid ;
gap : 14 px ;
}
. conclusion - section {
padding : 14 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 10 px ;
background : # ffffff ;
}
. conclusion - section h4 {
margin : 0 ;
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - h2 ) ;
font - weight : 700 ;
}
. conclusion - section p {
margin : 6 px 0 10 px ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.55 ;
}
. option - row {
display : flex ;
flex - wrap : wrap ;
gap : 8 px 14 px ;
margin - bottom : 12 px ;
}
. option - card {
display : inline - flex ;
align - items : center ;
gap : 6 px ;
min - width : 100 px ;
padding : 8 px 12 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 8 px ;
color : var ( -- student - text ) ;
cursor : pointer ;
transition : border - color 0.16 s ease , background 0.16 s ease , color 0.16 s ease ;
font - size : var ( -- student - fs - body ) ;
}
. option - card : hover {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
}
. option - card input {
accent - color : var ( -- student - accent ) ;
}
. risk - grid {
display : grid ;
grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ;
gap : 12 px ;
}
. risk - grid label {
display : grid ;
gap : 6 px ;
color : var ( -- student - text ) ;
font - weight : 600 ;
font - size : var ( -- student - fs - body - sm ) ;
}
. risk - grid textarea {
min - height : 120 px ;
font - weight : 400 ;
font - size : var ( -- student - fs - body ) ;
}
/* ===== 底部步骤导航按钮( scoped 基础) ===== */
. step - navigation {
position : relative ;
display : flex ;
flex - wrap : wrap ;
align - items : center ;
justify - content : space - between ;
gap : 10 px ;
margin - top : 16 px ;
padding : 12 px 16 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 10 px ;
background : # ffffff ;
}
. step - nav - left ,
. step - nav - right {
display : flex ;
align - items : center ;
gap : 8 px ;
flex - wrap : nowrap ;
}
. step - nav - right {
margin - left : auto ;
}
. btn - primary {
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 6 px ;
min - height : 36 px ;
height : 36 px ;
padding : 0 14 px ;
border : 1 px solid var ( -- student - accent ) ;
border - radius : 8 px ;
color : # ffffff ;
background : var ( -- student - accent ) ;
font - family : inherit ;
font - size : var ( -- student - fs - button ) ;
font - weight : 600 ;
box - shadow : 0 1 px 2 px rgba ( 37 , 99 , 235 , 0.2 ) , 0 2 px 8 px rgba ( 37 , 99 , 235 , 0.15 ) ;
cursor : pointer ;
transition : all 0.16 s ease ;
white - space : nowrap ;
. el - icon {
font - size : 18 px ;
}
& : hover : not ( : disabled ) {
border - color : var ( -- student - accent - deep ) ;
background : var ( -- student - accent - deep ) ;
transform : translateY ( - 1 px ) ;
box - shadow : 0 2 px 6 px rgba ( 29 , 78 , 216 , 0.3 ) , 0 4 px 12 px rgba ( 29 , 78 , 216 , 0.22 ) ;
}
& : active : not ( : disabled ) {
transform : translateY ( 0 ) ;
}
}
. btn - ghost {
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 6 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 : var ( -- student - fs - button ) ;
font - weight : 600 ;
cursor : pointer ;
transition : all 0.16 s ease ;
white - space : nowrap ;
. el - icon {
font - size : 18 px ;
}
& : hover : not ( : disabled ) {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
transform : translateY ( - 1 px ) ;
box - shadow : 0 2 px 6 px rgba ( 37 , 99 , 235 , 0.12 ) ;
}
}
. btn - save {
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 6 px ;
min - height : 36 px ;
height : 36 px ;
padding : 0 14 px ;
border : 1 px solid # e5e7eb ;
border - radius : 8 px ;
color : # 4 b5563 ;
background : # f9fafb ;
font - family : inherit ;
font - size : var ( -- student - fs - button ) ;
font - weight : 600 ;
cursor : pointer ;
transition : all 0.16 s ease ;
white - space : nowrap ;
. el - icon {
font - size : 18 px ;
}
& : hover : not ( : disabled ) {
border - color : # d1d5db ;
background : # f3f4f6 ;
}
}
. btn - primary : disabled ,
. btn - ghost : disabled ,
. btn - save : disabled ,
. btn - nav : disabled {
cursor : not - allowed ;
opacity : 0.5 ;
transform : none ;
box - shadow : none ;
}
. step - progress {
color : var ( -- student - text - secondary ) ;
font - family : var ( -- student - font - data ) ;
font - size : var ( -- student - fs - caption ) ;
font - weight : 700 ;
letter - spacing : 0.05 em ;
white - space : nowrap ;
}
. step - placeholder {
padding : 60 px 20 px ;
text - align : center ;
color : var ( -- student - text - secondary ) ;
}
. step - placeholder p {
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - h2 ) ;
font - weight : 600 ;
}
/* ===== 响应式 ===== */
@ media ( max - width : 840 px ) {
. risk - grid {
grid - template - columns : 1 fr ;
}
}
@ media ( max - width : 720 px ) {
. task - header {
flex - direction : column ;
align - items : flex - start ;
}
. step - chip {
display : none ;
}
. step - navigation {
flex - direction : column ;
align - items : stretch ;
}
. step - nav - left ,
. step - nav - right {
width : 100 % ;
}
. btn - nav {
width : 100 % ;
}
}
< / style >
< style lang = "scss" >
/* ===== 步骤导航:分段连接胶囊(教育蓝主题) ===== */
# app . studentLayout section . app - main : has ( . industry - pest - page ) . 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 - connector {
display : none ! important ;
}
. step - tab {
min - width : 0 ! important ;
display : flex ! important ;
flex - direction : row ! important ;
align - items : center ! important ;
justify - content : center ! important ;
gap : 8 px ! 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 : var ( -- student - fs - body ) ! important ;
font - weight : 600 ! important ;
line - height : 1.3 ! important ;
letter - spacing : 0 ! 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 - marker {
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 : none ! 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 : var ( -- student - fs - body - sm ) ! important ;
font - weight : 700 ! important ;
line - height : 1 ! important ;
transition : all 0.16 s ease ! important ;
}
. step - copy {
display : flex ! important ;
flex - direction : column ! important ;
gap : 0 ! important ;
min - width : 0 ! important ;
}
. step - name {
overflow : hidden ! important ;
text - overflow : ellipsis ! important ;
white - space : normal ! important ;
line - height : 1.3 ! important ;
color : inherit ! important ;
}
. step - action {
display : none ! important ;
}
& : hover : not ( . active ) : not ( . complete ) {
border - color : # 2563 eb ! important ;
background : # eff6ff ! important ;
color : # 2563 eb ! important ;
z - index : 1 ! important ;
. step - marker {
background : # 2563 eb ! important ;
color : # ffffff ! important ;
}
}
& . complete : not ( . active ) {
border - color : # e5e7eb ! important ;
color : # 374151 ! important ;
. step - marker {
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 - marker {
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 - marker {
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 ( . industry - pest - page ) . step - indicator {
justify - content : flex - start ! important ;
overflow - x : auto ! important ;
}
# app . studentLayout section . app - main : has ( . industry - pest - page ) . step - indicator . step - tab {
flex : 0 0 140 px ! important ;
max - width : none ! important ;
}
}
/* ===== 顶部任务工具栏换肤 ===== */
. industry - pest - page . task - toolbar - button . el - button {
display : inline - flex ! important ;
align - items : center ! important ;
justify - content : center ! important ;
gap : 6 px ! important ;
min - height : 34 px ! important ;
height : 34 px ! important ;
padding : 0 12 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 ;
font - size : var ( -- student - fs - body ) ! important ;
font - weight : 600 ! important ;
transition : border - color 0.16 s ease , background 0.16 s ease , color 0.16 s ease , box - shadow 0.16 s ease ! important ;
& : hover : not ( . is - disabled ) {
border - color : # 2563 eb ! important ;
color : # 2563 eb ! important ;
background : # eff6ff ! important ;
transform : translateY ( - 1 px ) ! important ;
box - shadow : 0 2 px 6 px rgba ( 37 , 99 , 235 , 0.12 ) ! important ;
}
}
. industry - pest - page . task - toolbar - button -- submit {
border - color : # 2563 eb ! important ;
background : # 2563 eb ! important ;
color : # ffffff ! important ;
box - shadow : 0 1 px 2 px rgba ( 37 , 99 , 235 , 0.35 ) , 0 3 px 10 px rgba ( 37 , 99 , 235 , 0.22 ) ! important ;
& : hover : not ( . is - disabled ) {
background : # 1 d4ed8 ! important ;
box - shadow : 0 2 px 4 px rgba ( 37 , 99 , 235 , 0.4 ) , 0 5 px 14 px rgba ( 29 , 78 , 216 , 0.28 ) ! important ;
}
}
< / style >