< 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 >
< div class = "task-actions" > < button type = "button" class = "btn btn-primary" :disabled ="saving" @click ="saveAndGoToStepTwo" > 保 存 并 进 入 第 2 步 < / button > < button type = "button" class = "btn" :disabled ="saving" @click ="resetTraining" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / 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 >
< div class = "task-actions" > < button type = "button" class = "btn btn-primary" : disabled = "saving || !stepOnePassed" @click ="saveAndGoToStepThree" > 保 存 并 进 入 第 3 步 < / button > < button type = "button" class = "btn" : disabled = "saving || !stepOnePassed" @click ="resetStepTwo" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / 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 >
< div class = "task-actions" > < button type = "button" class = "btn btn-primary" : disabled = "saving || !stepTwoPassed" @click ="saveAndGoToStepFour" > 保 存 并 进 入 第 4 步 < / button > < button type = "button" class = "btn" : disabled = "saving || !stepTwoPassed" @click ="resetStepThree" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / 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" > 结合前述 SWOT 分析 , 完成策略优先级排序 、 风险识别与竞争环境总结 。 < / p >
< p v-if ="!stepThreePassed" class="step-warning" > 请 先 完 成 并 保 存 第 3 步 的 四 类 SWOT 策 略 , 再 完 成 最 终 决 策 。 < / p >
< section class = "decision-section" > < h3 > ( 1 ) 优先策略排序 < / h3 > < p class = "section-tip" > 策略内容由第 3 步填写的 “ 具体策略建议 ” 自动带入 ; 请完成优先级排序并说明理由 。 < / p > < 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}`" /></el-select></td><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" > < h3 > ( 2 ) 需关注的风险点 < / h3 > < p > 请至少指出 2 个风险点 。 < / p > < 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" > < h3 > ( 3 ) 总结 SWOT 竞争环境评估结论 < / h3 > < 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 >
< / section >
< / main >
< TrainingAiSidebar study -tip = " 先 辨 别 内 部 条 件 与 外 部 环 境 , 再 归 类 为 优 势 、 劣 势 、 机 会 和 威 胁 。 " reference -title = " SWOT 分 类 提 示 " reference -text = " · 优 势 : 企 业 或 产 品 已 有 的 资 源 、 能 力 、 渠 道 与 差 异 化 < br / > · 劣势 : 内部能力 、 成本 、 体验 、 资源或覆盖不足 < br / > · 机会 : 外部市场增长 、 政策变化 、 消费趋势与技术窗口 < br / > · 威胁 : 竞争加剧 、 替代品 、 成本风险 、 政策限制与需求变化 " :progress-items=" progressItems " / >
< / div >
< / template >
< script setup >
import { 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 defaultTask = { title : "竞争环境分析" , background : "通过 SWOT 模型归类案例资料中的竞争环境信息,为后续竞品比较与竞争策略判断提供依据。" , goals : [ "掌握 SWOT 四维度的分类方法" , "提炼案例中的竞争环境关键信息" , "形成竞争分析的基础材料" ] , 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 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 : "" } ] ) ) , 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 ; }
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 ; 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 - fs - display : 17 px ;
-- student - fs - h1 : 15 px ;
-- student - fs - h2 : 14 px ;
-- student - fs - h3 : 13 px ;
-- student - fs - body : 13 px ;
-- student - fs - body - sm : 12 px ;
-- student - fs - caption : 11 px ;
-- student - fs - button : 13 px ;
-- student - fs - button - lg : 13 px ;
-- student - fs - data : 14 px ;
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 : 11 px ;
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 : 12 px ;
line - height : 1.6 ;
}
. task - brief - text {
display : - webkit - box ;
overflow : hidden ;
- webkit - box - orient : vertical ;
- webkit - line - clamp : 1 ;
}
. task - brief - toggle {
padding : 0 ;
border : 0 ;
color : var ( -- student - accent - deep ) ;
background : transparent ;
font : inherit ;
font - weight : 700 ;
cursor : pointer ;
& : hover { text - decoration : underline ; }
}
. task - brief - more {
display : grid ;
gap : 6 px ;
margin - top : 8 px ;
padding - top : 8 px ;
border - top : 1 px solid var ( -- student - panel - border ) ;
b { color : var ( -- student - text ) ; font - size : 12 px ; }
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 : 11 px ;
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 ,
. 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 ;
}
. decision - section h3 {
margin : 0 ;
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - h2 ) ;
font - weight : 700 ;
}
. decision - section > p {
margin : 6 px 0 10 px ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.55 ;
}
. section - tip {
color : var ( -- student - accent ) ! important ;
font - size : var ( -- student - fs - body - sm ) ! important ;
}
. 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 ;
}
/* ===== 底部按钮 ===== */
. task - actions {
display : flex ;
justify - content : center ;
gap : 10 px ;
margin - top : 18 px ;
}
. 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 : 13 px ;
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 ; }
. risk - grid { grid - template - columns : 1 fr ; }
}
< / 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 {
flex : 1 1 0 ! important ;
max - width : 240 px ! important ;
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 : 13 px ! 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 : 12 px ! 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 : normal ! important ;
line - height : 1.3 ! important ;
color : inherit ! important ;
font - weight : 600 ! important ;
font - size : 13 px ! 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 : 13 px ! 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 : 13 px ! 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 : 13 px ! 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 ;
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 步骤操作按钮 : task - actions > button . btn
* === === === === === === === === === === === === === === === === === === === === * /
# app . studentLayout section . app - main : has ( . competitive - analysis - page ) . task - actions {
gap : 14 px ! important ;
margin - top : 20 px ! important ;
}
# app . studentLayout section . app - main : has ( . competitive - analysis - page ) . task - actions > button {
display : inline - flex ! important ;
align - items : center ! important ;
justify - content : center ! important ;
gap : 8 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 : 13 px ! important ;
font - weight : 700 ! important ;
line - height : 1 ! important ;
transition : transform 0.16 s ease , border - color 0.16 s ease , background 0.16 s ease , box - shadow 0.16 s ease ! important ;
}
# app . studentLayout section . app - main : has ( . competitive - analysis - page ) . task - actions > button : hover : not ( : disabled ) {
transform : translateY ( - 1 px ) ! important ;
border - color : # 2563 eb ! important ;
color : # 2563 eb ! important ;
background : # eff6ff ! important ;
box - shadow : 0 3 px 10 px rgba ( 37 , 99 , 235 , 0.12 ) ! important ;
}
# app . studentLayout section . app - main : has ( . competitive - analysis - page ) . task - actions > button . btn - primary {
border - color : # 2563 eb ! important ;
color : # ffffff ! important ;
background : # 2563 eb ! important ;
box - shadow : 0 3 px 10 px rgba ( 37 , 99 , 235 , 0.28 ) ! important ;
}
# app . studentLayout section . app - main : has ( . competitive - analysis - page ) . task - actions > button . btn - primary : hover : not ( : disabled ) {
border - color : # 1 d4ed8 ! important ;
color : # ffffff ! important ;
background : # 1 d4ed8 ! important ;
box - shadow : 0 5 px 14 px rgba ( 29 , 78 , 216 , 0.32 ) ! important ;
}
< / style >