< template >
< div class = "competitive-analysis-page student-training-shell" >
< main class = "analysis-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 ="saveAndCompleteTraining" :on-reset ="resetStepFour"
: on - export = "exportOutcome" / >
< / section >
< section class = "analysis-workbench" >
< nav class = "step-indicator" aria -label = " 竞 争 环 境 分 析 步 骤 " > < template v -for = " ( step , index ) in trainingSteps "
: key = "`${index}-${step}`" > < 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 > { { step } } < / strong > < small > { { currentStep
=== index + 1 ? '当前步骤' : '点击切换' } } → < / small > < / span > < / button > < i
v - if = "index < trainingSteps.length - 1" class = "step-connector" / > < / template >
< / nav >
< section class = "step-card" >
< template v-if ="currentStep === 1" >
< div class = "step-title-row" >
< div >
< p > Step 01 < / p >
< h2 > { { trainingSteps [ 0 ] } } < / h2 >
< / div > < span > SWOT 分类练习 < / span >
< / div >
< p class = "step-desc" > 请根据案例文档中信息进行 SWOT 分析 , 进行各项内容归类 , 把文档中相应内容填写到以下四类中 。 < / p >
< div class = "swot-table-wrap" >
< table class = "swot-table" >
< thead >
< tr >
< th > 序号 < / th >
< th > SWOT维度 < / th >
< th > 关键信息要点 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="(row, index) in swotRows" :key="row.key" >
< td > { { index + 1 } } < / td >
< td class = "dimension-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 > Step 02 < / p >
< h2 > { { trainingSteps [ 1 ] } } < / h2 >
< / div > < span > 资料补充 < / span >
< / div >
< div class = "research-guide" >
< p > 请自行收集资料 , 补充完善 SWOT 信息 , 每个维度至少补充 1 条 。 信息收集可参考以下方法 : < / p >
< ol >
< li > 自行通过互联网平台收集 ; 可在电商平台 ( 京东 / 天猫 ) 搜索 “ 案例产品相关竞品 ” , 浏览竞品用户评价 , 从中发现竞品的优劣势和用户未满足的需求 。 < / li >
< li > 使用 AI 大模型智能搜索功能进行信息收集 , 参考提示词见实训任务案例文档 。 < / li >
< / ol >
< p > 请把补充的 SWOT 信息资讯填入下表 : < / p >
< / div >
< p v-if ="!stepOnePassed" class="step-warning" > 请 先 完 成 并 保 存 第 1 步 的 SWOT 信 息 归 类 , 再 填 写 本 步 骤 。 < / p >
< div class = "swot-table-wrap" >
< table class = "swot-table" >
< thead >
< tr >
< th > 序号 < / th >
< th > SWOT维度 < / th >
< th > 补充信息要点 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="(row, index) in swotRows" :key="row.key" >
< td > { { index + 1 } } < / td >
< td class = "dimension-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 > Step 03 < / p >
< h2 > { { trainingSteps [ 2 ] } } < / h2 >
< / div > < span > SWOT 策略矩阵 < / span >
< / div >
< p class = "step-desc" > 结合 SWOT 因素矩阵 , 将内部因素与外部因素交叉组合 , 分别提出四种策略 。 每类策略至少提出 1 条 。 < / p >
< p v-if ="!stepTwoPassed" class="step-warning" > 请 先 完 成 并 保 存 第 2 步 的 SWOT 补 充 信 息 , 再 填 写 本 步 骤 。 < / p >
< div class = "strategy-table-wrap" >
< table class = "strategy-table" >
< thead >
< tr >
< th > 策略类型 < / th >
< th > 组合逻辑 ( 自动生成 ) < / th >
< th > 具体策略建议 < / th >
< th > 预期效果 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="row in strategyDefinitions" :key="row.key" >
< td class = "strategy-type" > { { row . label } } < / td >
< td class = "logic-cell" >
< p v-for ="item in getCombinedLogic(row)" :key="item.label" > < strong > { {
item . label } } < / strong > { { item . value || '暂无已保存信息' } } < / p >
< / td >
< td > < el -input v -model .trim = " strategyForm [ row.key ] .recommendation "
type = "textarea" : autosize = "{ minRows: 2 }" : disabled = "!stepTwoPassed"
placeholder = "填写具体策略建议" / > < / td >
< td > < el -input v -model .trim = " strategyForm [ row.key ] .expectedEffect "
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 > Step 04 < / p >
< h2 > { { trainingSteps [ 3 ] } } < / h2 >
< / div > < span > 最终决策 < / span >
< / div >
< p class = "step-desc" > 结合以上分析 , 完成以下决策问题 : 总体竞争态势评估 、 策略优先级排序 、 风险识别与竞争环境总结 。 < / p >
< p v-if ="!stepThreePassed" class="step-warning" > 请 先 完 成 并 保 存 第 3 步 的 四 类 SWOT 策 略 , 再 完 成 最 终 决 策 。 < / p >
< section class = "decision-section" >
< div class = "section-heading" > < span > 01 < / span >
< div >
< h3 > 案例公司进入该市场的总体竞争态势评估 < / h3 >
< / div >
< / div >
< el -radio -group v -model = " finalForm.overallAssessment " class = "assessment-options"
: disabled = "!stepThreePassed" >
< el -radio v -for = " option in overallAssessmentOptions " :key ="option"
: label = "option" / >
< / e l - r a d i o - g r o u p >
< / section >
< section class = "decision-section" >
< div class = "section-heading" > < span > 02 < / span >
< div >
< h3 > 优先策略排序 < / h3 >
< p > 策略内容由第 3 步填写的 “ 具体策略建议 ” 自动带入 ; 请完成优先级排序并说明理由 。 < / p >
< / div >
< / div >
< div class = "priority-table-wrap" >
< table class = "priority-table" >
< thead >
< tr >
< th > 优先级排序 < / th >
< th > 策略 < / th >
< th > 理由 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="row in strategyDefinitions" :key="row.key" >
< td > < el -select v -model = " finalForm.priorities [ row.key ] .priority "
: disabled = "!stepThreePassed" : aria - label = "`${row.label} 的优先级排序`"
placeholder = "请选择" style = "width:100%" > < el -option :value ="null"
label = "请选择" / > < el -option v -for = " rank in 4 " :key ="rank"
: value = "rank" : label = "`${rank}`" / > < / e l - s e l e c t > < / t d >
< td class = "priority-strategy" > < strong > { { row . label } } < / strong >
< p > { { strategyForm [ row . key ] . recommendation || '请先在第 3 步填写该策略建议' } } < / p >
< / td >
< td > < el -input v -model .trim = " finalForm.priorities [ row.key ] .reason "
type = "textarea" : autosize = "{ minRows: 2 }"
: disabled = "!stepThreePassed" placeholder = "说明该策略的排序理由" / > < / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
< section class = "decision-section" >
< div class = "section-heading" > < span > 03 < / span >
< div >
< h3 > 需关注的风险点 < / h3 >
< p > 请至少指出 2 个风险点 。 < / p >
< / div >
< / div >
< div class = "risk-grid" > < label > < span > 核心风险点 1 < / span > < el -input
v - model . trim = "finalForm.coreRiskOne" type = "textarea" : autosize = "{ minRows: 2 }"
: disabled = "!stepThreePassed" placeholder = "填写核心风险点" / > < / label > < label > < span > 核心风险点
2 < / span > < el -input v -model .trim = " finalForm.coreRiskTwo " type = "textarea"
: autosize = "{ minRows: 2 }" : disabled = "!stepThreePassed"
placeholder = "填写核心风险点" / > < / label > < label > < span > 其他风险点 < / span > < el -input
v - model . trim = "finalForm.otherRisk" type = "textarea" : autosize = "{ minRows: 2 }"
: disabled = "!stepThreePassed" placeholder = "可补充其他风险点" / > < / label > < / div >
< / section >
< section class = "decision-section" >
< div class = "section-heading" > < span > 04 < / span >
< div >
< h3 > 总结 SWOT 竞争环境评估结论 < / h3 >
< / div >
< / div > < el -input v -model .trim = " finalForm.conclusion "
class = "conclusion-textarea" type = "textarea" : autosize = "{ minRows: 3 }"
: disabled = "!stepThreePassed" placeholder = "综合说明优先策略、风险应对方向及最终结论" / >
< / section >
< / template >
< div v -else class = "step-placeholder" >
< p > 第 { { currentStep } } 步内容待配置 < / p > < span > 第 1 步 SWOT 分类已保存 , 可从上方步骤栏返回查看 。 < / span >
< / div >
< / section >
< div class = "step-navigation" >
< div class = "step-nav-left" >
< button v-if ="currentStep > 1" type="button" class="btn-nav btn-ghost" @click="prevStep" >
< 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" >
{ { saving ? "处理中…" : "下一步" } }
< el -icon >
< ArrowRight / >
< / e l - i c o n >
< / button >
< / div >
< / div >
< / section >
< / main >
< TrainingAiSidebar study -tip = " 先 辨 别 内 部 条 件 与 外 部 环 境 , 再 归 类 为 优 势 、 劣 势 、 机 会 和 威 胁 。 " reference -title = " SWOT 分 类 提 示 "
reference - text = "· 优势:企业或产品已有的资源、能力、渠道与差异化<br />· 劣势:内部能力、成本、体验、资源或覆盖不足<br />· 机会:外部市场增长、政策变化、消费趋势与技术窗口<br />· 威胁:竞争加剧、替代品、成本风险、政策限制与需求变化"
: progress - items = "progressItems" / >
< / div >
< / template >
< script setup >
import { ArrowLeft , ArrowRight , CircleCheck , RefreshLeft , TrendCharts } from "@element-plus/icons-vue"
import { checkCompetitiveEnvironmentAnalysisStepOne , checkCompetitiveEnvironmentAnalysisStepTwo , checkCompetitiveEnvironmentAnalysisStepThree , checkCompetitiveEnvironmentAnalysisStepFour , 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"
const TASK _KEY = "competitive-environment-analysis"
const { proxy } = getCurrentInstance ( )
const userStore = useUserStore ( )
const DEFAULT _STEPS = [ "SWOT竞争环境分析" , "竞品调研与比较" , "竞争策略设计" , "竞争环境结论" ]
const swotRows = [
{ key : "strengths" , label : "优势( S) " , placeholder : "填写案例中的资源、能力、渠道、产品或服务优势" } ,
{ key : "weaknesses" , label : "劣势( W) " , placeholder : "填写案例中的内部短板、成本、体验或资源不足" } ,
{ key : "opportunities" , label : "机会( O) " , placeholder : "填写案例中的市场、政策、消费趋势或技术机会" } ,
{ key : "threats" , label : "威胁( T) " , placeholder : "填写案例中的竞争、替代、成本、政策或需求风险" } ,
]
const strategyDefinitions = [
{ key : "so" , label : "SO策略( 优势× 机会) " , first : "strengths" , firstLabel : "优势" , second : "opportunities" , secondLabel : "机会" } ,
{ key : "wo" , label : "WO策略( 劣势× 机会) " , first : "weaknesses" , firstLabel : "劣势" , second : "opportunities" , secondLabel : "机会" } ,
{ key : "st" , label : "ST策略( 优势× 威胁) " , first : "strengths" , firstLabel : "优势" , second : "threats" , secondLabel : "威胁" } ,
{ key : "wt" , label : "WT策略( 劣势× 威胁) " , first : "weaknesses" , firstLabel : "劣势" , second : "threats" , secondLabel : "威胁" } ,
]
const overallAssessmentOptions = [ "优势明显,建议快速进入" , "机会大于挑战,建议积极布局" , "挑战与机会并存,需谨慎推进" , "劣势突出,建议暂缓进入" ]
const defaultTask = { title : "竞争环境分析" , background : "通过 SWOT 模型归类案例资料中的竞争环境信息,为后续竞品比较与竞争策略判断提供依据。" , goals : [ "掌握 SWOT 四维度的分类方法" , "提炼案例中的竞争环境关键信息" , "形成竞争分析的基础材料" ] , requirement : "请阅读案例资料,将相关信息归类为优势、劣势、机会和威胁。" }
const taskConfig = ref ( null ) ; const briefOpen = ref ( true ) ; 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 supplementForm = ref ( createForm ( ) ) ; const strategyForm = ref ( createStrategyForm ( ) ) ; const finalForm = ref ( createFinalForm ( ) )
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 ( ( ) => swotRows . map ( ( row ) => ( { text : ` ${ strategyForm . value . so . recommendation ? "已制定策略" : supplementForm . value [ row . key ] ? "已补充" : form . value [ row . key ] ? "已归类" : "待填写" } : ${ row . label } ` , status : strategyForm . value . so . recommendation || 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 { strengths : "" , weaknesses : "" , opportunities : "" , threats : "" } }
function createStrategyForm ( ) { return { so : createStrategyRow ( ) , wo : createStrategyRow ( ) , st : createStrategyRow ( ) , wt : createStrategyRow ( ) } }
function createStrategyRow ( ) { return { recommendation : "" , expectedEffect : "" } }
function createFinalForm ( ) { return { priorities : Object . fromEntries ( strategyDefinitions . map ( ( row ) => [ row . key , { priority : null , reason : "" } ] ) ) , overallAssessment : "" , coreRiskOne : "" , coreRiskTwo : "" , otherRisk : "" , conclusion : "" } }
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 ) { restoreStrategyOutcome ( answer . step3Answer ) ; stepThreePassed . value = true } if ( answer ? . step4Answer ) restoreFinalOutcome ( answer . step4Answer ) ; if ( Number ( answer ? . currentStep ) > 0 ) currentStep . value = Number ( answer . currentStep ) } catch ( error ) { /* Keep local draft when unavailable. */ } }
function restoreOutcome ( value , target , field = "swot" ) { try { const parsed = typeof value === "string" ? JSON . parse ( value ) : value ; target . value = { ... createForm ( ) , ... ( parsed ? . [ field ] || parsed || { } ) } } catch ( error ) { target . value = createForm ( ) } }
function restoreStrategyOutcome ( value ) { try { const parsed = typeof value === "string" ? JSON . parse ( value ) : value ; const source = parsed ? . strategies || parsed || { } ; strategyForm . value = Object . fromEntries ( strategyDefinitions . map ( ( row ) => [ row . key , { ... createStrategyRow ( ) , ... ( source [ row . key ] || { } ) } ] ) ) } catch ( error ) { strategyForm . value = createStrategyForm ( ) } }
function restoreFinalOutcome ( value ) { try { const parsed = typeof value === "string" ? JSON . parse ( value ) : value ; const source = parsed ? . decision || parsed || { } ; const priorities = source . priorities || { } ; finalForm . value = { ... createFinalForm ( ) , ... source , priorities : Object . fromEntries ( strategyDefinitions . map ( ( row ) => [ row . key , { priority : null , reason : "" , ... ( priorities [ row . key ] || { } ) } ] ) ) } } catch ( error ) { finalForm . value = createFinalForm ( ) } }
function getCombinedLogic ( row ) { return [ { label : ` ${ row . firstLabel } ( 第1步) · ` , value : form . value [ row . first ] } , { label : ` ${ row . firstLabel } ( 第2步补充) · ` , value : supplementForm . value [ row . first ] } , { label : ` ${ row . secondLabel } ( 第1步) · ` , value : form . value [ row . second ] } , { label : ` ${ row . secondLabel } ( 第2步补充) · ` , value : supplementForm . value [ row . second ] } ] }
function buildStepOneOutcome ( ) { return { title : taskTitle . value , swot : { ... form . value } } } function buildStepTwoOutcome ( ) { return { title : taskTitle . value , supplements : { ... supplementForm . value } } } function buildStepThreeOutcome ( ) { return { title : taskTitle . value , strategies : JSON . parse ( JSON . stringify ( strategyForm . value ) ) } } function buildStepFourOutcome ( ) { return { title : taskTitle . value , decision : JSON . parse ( JSON . stringify ( finalForm . 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 ) { /* Optional browser draft. */ } } 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" ) ; restoreStrategyOutcome ( draft ? . step3 ) ; restoreFinalOutcome ( 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 }
function prevStep ( ) { if ( currentStep . value > 1 ) currentStep . value -= 1 }
async function saveCurrentProgress ( ) { if ( saving . value ) return ; saving . value = true ; try { const step = currentStep . value ; const payload = { currentStep : step , submitted : false , saveAction : "SAVE" } ; if ( step === 1 ) payload . step1Answer = JSON . stringify ( buildStepOneOutcome ( ) ) ; else if ( step === 2 ) payload . step2Answer = JSON . stringify ( buildStepTwoOutcome ( ) ) ; else if ( step === 3 ) payload . step3Answer = JSON . stringify ( buildStepThreeOutcome ( ) ) ; else if ( step === 4 ) payload . step4Answer = JSON . stringify ( buildStepFourOutcome ( ) ) ; await saveStudentTrainingAnswer ( TASK _KEY , payload ) ; proxy ? . $modal ? . msgSuccess ( "已保存" ) } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "保存失败,请稍后重试" ) } finally { saving . value = false } }
async function nextStep ( ) { if ( currentStep . value === 1 ) return saveAndGoToStepTwo ( ) ; if ( currentStep . value === 2 ) return saveAndGoToStepThree ( ) ; if ( currentStep . value === 3 ) return saveAndGoToStepFour ( ) }
async function saveAndGoToStepTwo ( ) { if ( saving . value ) return ; saving . value = true ; try { await checkCompetitiveEnvironmentAnalysisStepOne ( 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 || "请完整填写 SWOT 四类信息" ) } finally { saving . value = false } }
async function saveAndGoToStepThree ( ) { if ( saving . value || ! stepOnePassed . value ) return ; saving . value = true ; try { await checkCompetitiveEnvironmentAnalysisStepTwo ( 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 || "请为每个 SWOT 维度至少补充 1 条信息" ) } finally { saving . value = false } }
async function saveAndGoToStepFour ( ) { if ( saving . value || ! stepTwoPassed . value ) return ; saving . value = true ; try { await checkCompetitiveEnvironmentAnalysisStepThree ( { strategies : strategyDefinitions . map ( ( row ) => ( { strategyType : row . key , ... strategyForm . 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 || "请完成四类 SWOT 策略建议和预期效果" ) } finally { saving . value = false } }
async function saveAndCompleteTraining ( ) { if ( saving . value || ! stepThreePassed . value ) return ; if ( ! finalForm . value . overallAssessment ) { proxy ? . $modal ? . msgError ? . ( "请先完成“案例公司进入该市场的总体竞争态势评估”选择题" ) ; return } saving . value = true ; try { await checkCompetitiveEnvironmentAnalysisStepFour ( { priorities : strategyDefinitions . map ( ( row ) => ( { strategyType : row . key , ... finalForm . value . priorities [ row . key ] } ) ) , coreRiskOne : finalForm . value . coreRiskOne , coreRiskTwo : finalForm . value . coreRiskTwo , otherRisk : finalForm . value . otherRisk , conclusion : finalForm . value . conclusion } ) ; 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 || "请完成总体竞争态势评估选择题、完整填写策略优先级、至少 2 个核心风险点和总结 SWOT 竞争环境评估结论" ) } finally { saving . value = false } }
async function resetTraining ( ) { form . value = createForm ( ) ; supplementForm . value = createForm ( ) ; strategyForm . value = createStrategyForm ( ) ; finalForm . value = createFinalForm ( ) ; stepOnePassed . value = false ; stepTwoPassed . value = false ; stepThreePassed . value = false ; currentStep . value = 1 ; clearDraft ( ) ; try { saving . value = true ; await saveStudentTrainingAnswer ( TASK _KEY , { step1Answer : JSON . stringify ( buildStepOneOutcome ( ) ) , currentStep : 1 , submitted : false , saveAction : "RESET" } ) } catch ( error ) { /* Local clearing still succeeds. */ } finally { saving . value = false } proxy ? . $modal ? . msgSuccess ( "已清空填写内容" ) }
function resetStepTwo ( ) { supplementForm . value = createForm ( ) ; proxy ? . $modal ? . msgSuccess ( "已清空第 2 步填写内容" ) }
function resetStepThree ( ) { strategyForm . value = createStrategyForm ( ) ; proxy ? . $modal ? . msgSuccess ( "已清空第 3 步填写内容" ) }
function resetStepFour ( ) { finalForm . value = createFinalForm ( ) ; 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 >
/* ===== 页面级令牌(教育蓝极简主题) ===== */
. competitive - analysis - 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, 页面不再重复声明 */
position : relative ;
z - index : 2 ;
display : grid ;
grid - template - columns : minmax ( 720 px , 1 fr ) 340 px ;
gap : 24 px ;
width : 100 % ;
max - width : 100 % ;
min - height : calc ( 100 vh - 106 px ) ;
margin : 0 ;
padding : 24 px 24 px 32 px ;
color : var ( -- student - text ) ;
background : # ffffff ;
}
/* 对齐 foundation 的 .training-core: 只设 gap, 无边框/背景/阴影 */
. analysis - 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 ,
. step - title - row p {
color : var ( -- student - text - secondary ) ;
font - weight : 700 ;
font - size : var ( -- student - fs - caption ) ;
letter - spacing : 0.12 em ;
text - transform : uppercase ;
}
. task - header h1 {
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 : block ;
}
. 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 ;
}
}
. analysis - toolbar {
display : flex ;
align - items : center ;
gap : 10 px ;
flex : 0 0 auto ;
justify - content : flex - end ;
}
. 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 ;
}
}
/* workbench 层:透明容器 */
. analysis - workbench {
margin : 0 ;
padding : 0 ;
border : 0 ;
border - radius : 0 ;
background : transparent ;
display : grid ;
gap : 10 px ;
}
/* step-card 层:透明容器 */
. 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 p {
margin : 0 ;
}
. step - title - row h2 {
margin : 4 px 0 0 ;
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - h1 ) ;
font - weight : 700 ;
line - height : 1.35 ;
}
. step - title - row > span {
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 {
margin : 14 px 0 ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.6 ;
}
. 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 ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.6 ;
}
. 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 ) ;
}
/* ===== 表格 ===== */
. swot - table - wrap ,
. strategy - table - wrap ,
. priority - table - wrap {
overflow - x : auto ;
}
. swot - table ,
. strategy - table ,
. priority - table {
width : 100 % ;
border - collapse : collapse ;
}
. swot - table {
min - width : 720 px ;
}
. strategy - table {
min - width : 980 px ;
}
. priority - table {
min - width : 820 px ;
}
. swot - table th ,
. swot - table td ,
. strategy - table th ,
. strategy - table td ,
. priority - table th ,
. priority - table td {
border : 1 px solid var ( -- student - panel - border ) ;
}
. swot - table th ,
. strategy - table th ,
. priority - 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 ;
}
. swot - table th : first - child ,
. swot - table td : first - child {
width : 8 % ;
text - align : center ;
}
. swot - table th : nth - child ( 2 ) ,
. dimension - cell {
width : 18 % ;
text - align : center ;
font - weight : 700 ;
}
. strategy - table th : first - child ,
. strategy - table . strategy - type {
width : 18 % ;
text - align : center ;
font - weight : 700 ;
}
. strategy - table th : nth - child ( 2 ) {
width : 28 % ;
}
. strategy - table . logic - cell {
padding : 10 px 12 px ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body - sm ) ;
line - height : 1.55 ;
}
. strategy - table . logic - cell p {
margin : 0 0 6 px ;
}
. strategy - table . logic - cell p : last - child {
margin - bottom : 0 ;
}
. strategy - table . logic - cell strong {
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - body - sm ) ;
}
. priority - table th : first - child ,
. priority - table td : first - child {
width : 16 % ;
text - align : center ;
}
. priority - table th : nth - child ( 2 ) {
width : 42 % ;
}
. swot - table textarea ,
/* ===== 总体竞争态势评估选择题 ===== */
. assessment - options {
display : flex ;
flex - wrap : wrap ;
gap : 8 px 28 px ;
}
. assessment - options . el - radio {
height : 28 px ;
margin - right : 0 ;
}
. strategy - table textarea ,
. priority - table textarea ,
. risk - grid textarea ,
. conclusion - textarea {
box - sizing : border - box ;
display : block ;
width : 100 % ;
min - height : 112 px ;
resize : vertical ;
border : 0 ;
padding : 8 px 10 px ;
color : var ( -- student - text ) ;
background : transparent ;
font : inherit ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.55 ;
}
. swot - table textarea : focus ,
. strategy - table textarea : focus ,
. priority - table textarea : focus ,
. risk - grid textarea : focus ,
. conclusion - textarea : focus {
outline : 2 px solid rgba ( 37 , 99 , 235 , 0.15 ) ;
outline - offset : - 2 px ;
}
. swot - table textarea : disabled ,
. strategy - table textarea : disabled ,
. priority - table textarea : disabled ,
. risk - grid textarea : disabled ,
. conclusion - textarea : disabled {
cursor : not - allowed ;
color : var ( -- student - text - secondary ) ;
background : var ( -- student - accent - soft ) ;
}
/* ===== 决策区 ===== */
. decision - section {
margin - top : 20 px ;
padding : 14 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 10 px ;
background : # ffffff ;
}
. priority - table select {
min - width : 90 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 6 px ;
padding : 6 px 8 px ;
color : var ( -- student - text ) ;
background : # ffffff ;
font : inherit ;
font - size : var ( -- student - fs - body - sm ) ;
}
. priority - strategy {
padding : 10 px 12 px ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body - sm ) ;
line - height : 1.55 ;
}
. priority - strategy strong {
color : var ( -- student - text ) ;
font - weight : 600 ;
}
. priority - strategy p {
margin : 4 px 0 0 ;
white - space : pre - line ;
}
. priority - table textarea {
min - height : 96 px ;
}
. 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 ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 6 px ;
background : # ffffff ;
}
. conclusion - textarea {
min - height : 160 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 6 px ;
background : # ffffff ;
}
/* ===== 底部导航:工具栏式 ===== */
. 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 - nav {
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 6 px ;
min - height : 36 px ;
height : 36 px ;
padding : 0 14 px ;
border - radius : 8 px ;
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 : var ( -- student - fs - h2 ) ;
}
}
. btn - primary {
border : 1 px solid var ( -- student - accent ) ;
color : # ffffff ;
background : var ( -- student - accent ) ;
box - shadow : 0 1 px 2 px rgba ( 37 , 99 , 235 , 0.2 ) , 0 2 px 8 px rgba ( 37 , 99 , 235 , 0.15 ) ;
& : 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 {
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ;
color : var ( -- student - accent ) ;
background : # ffffff ;
& : 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 {
border : 1 px solid # e5e7eb ;
color : # 4 b5563 ;
background : # f9fafb ;
& : 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 - size : var ( -- student - fs - caption ) ;
font - weight : 700 ;
letter - spacing : 0.05 em ;
white - space : nowrap ;
}
. btn {
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 - text ) ;
background : # ffffff ;
font - size : var ( -- student - fs - button ) ;
font - weight : 700 ;
cursor : pointer ;
text - shadow : none ;
transition : border - color 0.16 s ease , background 0.16 s ease , color 0.16 s ease , transform 0.16 s ease , box - shadow 0.16 s ease ;
}
. btn : hover {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
color : var ( -- student - accent ) ;
transform : translateY ( - 1 px ) ;
box - shadow : 0 3 px 10 px rgba ( 37 , 99 , 235 , 0.12 ) ;
}
. btn . btn - primary {
border - color : var ( -- student - accent ) ;
color : # ffffff ;
background : var ( -- student - accent ) ;
font - weight : 700 ;
box - shadow : 0 3 px 10 px rgba ( 37 , 99 , 235 , 0.28 ) ;
}
. btn . btn - primary : hover {
background : var ( -- student - accent - deep ) ;
border - color : var ( -- student - accent - deep ) ;
transform : translateY ( - 1 px ) ;
box - shadow : 0 5 px 14 px rgba ( 29 , 78 , 216 , 0.32 ) ;
}
. btn : disabled {
cursor : not - allowed ;
opacity : 0.5 ;
transform : none ;
box - shadow : none ;
}
. outline - action {
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 - size : var ( -- student - fs - body ) ;
font - weight : 700 ;
cursor : pointer ;
text - shadow : none ;
transition : border - color 0.16 s ease , background 0.16 s ease , color 0.16 s ease ;
}
. outline - action : hover {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
color : var ( -- student - accent ) ;
}
. 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 : 720 px ) {
. task - header {
flex - direction : column ;
align - items : flex - start ;
}
. workbench - head {
justify - content : flex - start ;
flex - wrap : wrap ;
}
. step - title - row > span {
display : none ;
}
. step - navigation {
flex - direction : column ;
align - items : stretch ;
}
. step - nav - left ,
. step - nav - right {
width : 100 % ;
}
. btn - nav {
width : 100 % ;
}
. risk - grid {
grid - template - columns : 1 fr ;
}
}
/* ===== 步骤内小节标题(蓝底序号,统一规范) ===== */
. section - heading {
display : flex ;
align - items : flex - start ;
gap : 12 px ;
margin - bottom : 10 px ;
> span {
display : grid ;
flex : 0 0 28 px ;
width : 28 px ;
height : 28 px ;
place - items : center ;
border - radius : 6 px ;
color : # ffffff ;
background : var ( -- student - accent , # 2563 eb ) ;
font - size : var ( -- student - fs - body , 14 px ) ;
font - weight : 700 ;
}
h3 {
margin : 0 ;
color : var ( -- student - text , # 1 f2937 ) ;
font - size : var ( -- student - fs - h1 , 18 px ) ;
font - weight : 700 ;
}
p {
margin : 4 px 0 0 ;
color : var ( -- student - text - secondary , # 6 b7280 ) ;
font - size : var ( -- student - fs - body , 14 px ) ;
line - height : 1.6 ;
}
}
< / style >
< style lang = "scss" >
/* ===== 步骤导航:分段连接胶囊(教育蓝主题) ===== */
# app . studentLayout section . app - main : has ( . competitive - analysis - 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 : nowrap ! 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 - copy strong {
overflow : hidden ! important ;
text - overflow : ellipsis ! important ;
white - space : nowrap ! important ;
line - height : 1.3 ! important ;
color : inherit ! important ;
font - weight : 600 ! important ;
font - size : var ( -- student - fs - body ) ! important ;
}
. step - copy small {
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 ( . competitive - analysis - page ) . step - indicator {
justify - content : flex - start ! important ;
overflow - x : auto ! important ;
}
# app . studentLayout section . app - main : has ( . competitive - analysis - page ) . step - indicator . step - tab {
flex : 0 0 140 px ! important ;
max - width : none ! important ;
}
}
/* ===== 顶部任务工具栏换肤 ===== */
. competitive - analysis - 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 ;
}
}
. competitive - analysis - 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 ;
}
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 顶部工具栏 : TrainingMaterialButton 资料按钮 → 教育蓝
* === === === === === === === === === === === === === === === === === === === === * /
. competitive - analysis - page . analysis - toolbar . material - button . el - button {
height : 34 px ! important ;
min - height : 34 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 {
border - color : # 2563 eb ! important ;
color : # 2563 eb ! important ;
background : # eff6ff ! important ;
box - shadow : 0 2 px 6 px rgba ( 37 , 99 , 235 , 0.12 ) ! important ;
}
& . is - plain {
border - color : # e5e7eb ! important ;
color : # 9 ca3af ! important ;
& : hover {
border - color : # e5e7eb ! important ;
color : # 9 ca3af ! important ;
background : # ffffff ! important ;
box - shadow : none ! important ;
}
}
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 顶部导出按钮 ( 原生 < button .outline -action > )
* === === === === === === === === === === === === === === === === === === === === * /
# app . studentLayout section . app - main : has ( . competitive - analysis - page ) . analysis - toolbar > button . outline - action {
display : inline - flex ! important ;
align - items : center ! important ;
justify - content : center ! important ;
gap : 6 px ! important ;
height : 34 px ! important ;
min - height : 34 px ! important ;
padding : 0 14 px ! important ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ! important ;
border - radius : 8 px ! important ;
color : # 2563 eb ! important ;
background : # ffffff ! important ;
font - size : var ( -- student - fs - body ) ! important ;
font - weight : 600 ! important ;
line - height : 1 ! 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 ;
}
# app . studentLayout section . app - main : has ( . competitive - analysis - page ) . analysis - toolbar > button . outline - action : hover {
border - color : # 2563 eb ! important ;
color : # 2563 eb ! important ;
background : # eff6ff ! important ;
box - shadow : 0 2 px 6 px rgba ( 37 , 99 , 235 , 0.12 ) ! important ;
transform : none ! important ;
}
< / style >