< template >
< div class = "competitive-analysis-page student-training-shell" >
< main class = "analysis-core" >
< section class = "task-header" > < div class = "task-badge" > < el -icon > < TrendCharts / > < / e l - i c o n > 核 心 实 训 · 市 场 洞 察 与 需 求 分 析 < / d i v > < h 1 > { { t a s k T i t l e } } < / h 1 > < / s e c t i o n >
< TrainingTaskBrief :background ="taskBackground" :goals ="taskGoals" :requirements ="taskRequirement" :show-material ="false" / >
< section class = "analysis-workbench" >
< div class = "workbench-head" > < TrainingMaterialButton :material-name ="taskConfig?.materialName" :material-url ="taskConfig?.materialUrl" / > < button type = "button" class = "outline-action" @click ="exportOutcome" > < el -icon > < Download / > < / e l - i c o n > 导 出 实 训 成 果 < / b u t t o n > < / d i v >
< 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 > < textarea v -model .trim = " form [ row.key ] " :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 > < textarea v -model .trim = " supplementForm [ row.key ] " :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 > < textarea v -model .trim = " strategyForm [ row.key ] .recommendation " :disabled ="!stepTwoPassed" placeholder = "填写具体策略建议" / > < / td > < td > < textarea v -model .trim = " strategyForm [ row.key ] .expectedEffect " :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><select v-model.number="finalForm.priorities[row.key].priority" :disabled="!stepThreePassed" :aria-label="`${row.label} 的优先级排序`"><option :value="null">请选择</option><option v-for="rank in 4" :key="rank" :value ="rank" > {{ rank }} < / option > < / select > < / td > < td class = "priority-strategy" > < strong > { { row . label } } < / strong > < p > { { strategyForm [ row . key ] . recommendation || '请先在第 3 步填写该策略建议' } } < / p > < / td > < td > < textarea v -model .trim = " finalForm.priorities [ row.key ] .reason " :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 > < textarea v -model .trim = " finalForm.coreRiskOne " :disabled ="!stepThreePassed" placeholder = "填写核心风险点" / > < / label > < label > < span > 核心风险点 2 < / span > < textarea v -model .trim = " finalForm.coreRiskTwo " :disabled ="!stepThreePassed" placeholder = "填写核心风险点" / > < / label > < label > < span > 其他风险点 < / span > < textarea v -model .trim = " finalForm.otherRisk " :disabled ="!stepThreePassed" placeholder = "可补充其他风险点" / > < / label > < / div > < / section >
< section class = "decision-section" > < h3 > ( 3 ) 总结 SWOT 竞争环境评估结论 < / h3 > < textarea v -model .trim = " finalForm.conclusion " class = "conclusion-textarea" :disabled ="!stepThreePassed" placeholder = "综合说明优先策略、风险应对方向及最终结论" / > < / section >
< div class = "task-actions" > < button type = "button" class = "btn btn-primary" : disabled = "saving || !stepThreePassed" @click ="saveAndCompleteTraining" > 保 存 并 完 成 实 训 < / button > < button type = "button" class = "btn" : disabled = "saving || !stepThreePassed" @click ="resetStepFour" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / 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 { Download , 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 TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue" ;
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.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 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 { display : grid ; grid - template - columns : minmax ( 720 px , 1 fr ) 340 px ; gap : 24 px ; min - height : calc ( 100 vh - 106 px ) ; padding : 24 px ; color : # e8f7ff ; background : # 06111 d ; } . analysis - core { min - width : 0 ; border : 1 px solid rgba ( 0 , 180 , 255 , .25 ) ; background : rgba ( 8 , 18 , 28 , .6 ) ; box - shadow : 0 22 px 48 px rgba ( 0 , 0 , 0 , .22 ) ; } . task - header { padding : 22 px 28 px 4 px ; } . task - badge , . step - title - row p { color : # 65 dcff ; font - weight : 700 ; } . task - header h1 { margin : 10 px 0 0 ; color : # fff ; font - size : 28 px ; } . analysis - workbench { margin : 24 px ; padding : 22 px ; border : 1 px solid rgba ( 33 , 194 , 255 , .35 ) ; border - radius : 20 px ; background : rgba ( 1 , 24 , 39 , .72 ) ; } . workbench - head { display : flex ; justify - content : flex - end ; gap : 12 px ; } . outline - action , . btn { display : inline - flex ; align - items : center ; justify - content : center ; gap : 6 px ; border : 1 px solid # 2 cc9ff ; border - radius : 999 px ; padding : 10 px 18 px ; color : # dff8ff ; background : rgba ( 8 , 87 , 121 , .55 ) ; cursor : pointer ; } . step - indicator { display : flex ; align - items : center ; gap : 12 px ; margin : 28 px 0 ; padding : 12 px ; border : 1 px solid rgba ( 30 , 190 , 255 , .28 ) ; border - radius : 22 px ; } . step - tab { display : flex ; align - items : center ; flex : 1 ; gap : 10 px ; min - width : 0 ; padding : 10 px ; border : 0 ; border - radius : 16 px ; color : # 96 c6d8 ; background : transparent ; text - align : left ; cursor : pointer ; } . step - tab . active { color : # fff ; background : linear - gradient ( 110 deg , # 148 fc6 , # 0879 aa ) ; } . step - marker { display : grid ; place - items : center ; flex : 0 0 42 px ; width : 42 px ; height : 42 px ; border : 1 px solid # 4 ed7ff ; border - radius : 50 % ; font - weight : 800 ; } . step - copy { display : grid ; gap : 4 px ; min - width : 0 ; } . step - copy strong { overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; } . step - copy small { color : # 5 ec9ed ; } . step - connector { flex : 0 0 25 px ; height : 2 px ; background : rgba ( 83 , 204 , 255 , .4 ) ; } . step - card { padding : 24 px ; border : 1 px solid rgba ( 31 , 185 , 249 , .32 ) ; border - radius : 18 px ; background : rgba ( 1 , 21 , 34 , .65 ) ; } . step - title - row { display : flex ; justify - content : space - between ; align - items : flex - start ; } . step - title - row p { margin : 0 ; } . step - title - row h2 { margin : 5 px 0 0 ; color : # fff ; font - size : 24 px ; } . step - title - row > span { border : 1 px solid # 38 caff ; border - radius : 999 px ; padding : 7 px 12 px ; color : # 9 aeaff ; } . step - desc { margin : 24 px 0 ; color : # c5e4ef ; font - size : 16 px ; line - height : 1.8 ; } . research - guide { margin : 22 px 0 18 px ; padding : 16 px 18 px ; border - left : 3 px solid # 35 cfff ; color : # c5e4ef ; background : rgba ( 13 , 95 , 129 , .16 ) ; line - height : 1.8 ; } . research - guide p { margin : 0 0 8 px ; } . research - guide ol { margin : 0 0 8 px ; padding - left : 24 px ; } . research - guide li + li { margin - top : 6 px ; } . step - warning { margin : 12 px 0 ; color : # ffd58e ; } . swot - table - wrap , . strategy - table - wrap , . priority - table - wrap { overflow - x : auto ; } . swot - table { width : 100 % ; min - width : 720 px ; border - collapse : collapse ; } . swot - table th , . swot - table td , . strategy - table th , . strategy - table td , . priority - table th , . priority - table td { border : 1 px solid rgba ( 139 , 213 , 241 , .45 ) ; } . swot - table th , . strategy - table th , . priority - table th { padding : 14 px ; color : # fff ; background : # 079 ed9 ; font - size : 16 px ; } . 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 ; } . swot - table textarea , . strategy - table textarea , . priority - table textarea , . risk - grid textarea , . conclusion - textarea { box - sizing : border - box ; display : block ; width : 100 % ; min - height : 136 px ; resize : vertical ; border : 0 ; padding : 16 px ; color : # e9faff ; background : transparent ; font : inherit ; line - height : 1.7 ; } . swot - table textarea : focus , . strategy - table textarea : focus , . priority - table textarea : focus , . risk - grid textarea : focus , . conclusion - textarea : focus { outline : 2 px solid rgba ( 57 , 207 , 255 , .4 ) ; 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 : # 8 aa8b5 ; background : rgba ( 255 , 255 , 255 , .03 ) ; } . strategy - table { width : 100 % ; min - width : 980 px ; border - collapse :
< / style >