< template >
< div class = "feasibility-page student-training-shell" >
< main class = "feasibility-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 ="saveStepThree" :on-export ="exportOutcome" / >
< / section >
< section class = "workbench" >
< nav class = "step-indicator" aria -label = " 商 业 可 行 性 论 证 步 骤 " >
< button v -for = " ( step , index ) in trainingSteps " :key ="step" type = "button"
: class = "['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]"
: aria - label = "`第 ${index + 1} 步:${step}`"
: aria - current = "currentStep === index + 1 ? 'step' : undefined" : title = "step"
@ click = "switchStep(index + 1)" >
< span class = "step-bar" aria -hidden = " true " > < / span >
< span class = "step-no" aria -hidden = " true " > { { String ( index + 1 ) . padStart ( 2 , '0' ) } } < / span >
< span class = "step-name" > { { step } } < / span >
< / button >
< / nav >
< section v-if ="currentStep === 1" class="step-card" >
< div class = "step-heading" >
< div >
< p class = "section-eyebrow" > Step 01 < / p >
< h2 > 商业可行性论证 < / h2 >
< / div > < em > 结构化论证 < / em >
< / div >
< p class = "intro" > 商业可行性论证需要从多个维度系统展开 。 以下提供了可行性分析的各个子模块 , 根据案例信息 , 完善填写以下信息 。 < / p >
< section class = "form-section" >
< div class = "section-title" > < span > 01 < / span >
< div >
< h3 > 项目概述 < / h3 >
< p > 结合前序 PEST 、 SWOT 分析 , 说明项目为何值得推进 。 < / p >
< / div >
< / div >
< div class = "form-table overview-table" >
< label v-for ="field in overviewFields" :key="field.key" > < span > { { field . label
} } < / span > < el -input v -model = " form.projectOverview [ field.key ] " type = "textarea"
: autosize = "{ minRows: 2, maxRows: 6 }" : placeholder = "field.placeholder" / > < / label >
< / div >
< / section >
< section class = "form-section" >
< div class = "section-title" > < span > 02 < / span >
< div >
< h3 > 市场规模与竞争格局 < / h3 >
< p > 填写可验证的市场数据 , 比例项目以百分数填写 , 例如 12.5 。 < / p >
< / div >
< / div >
< div class = "form-table market-table" >
< label v-for ="field in marketFields" :key="field.key" > < span > { { field . label } } < / span >
< div class = "input-with-suffix" > < el -input -number v -if = " field.type = = = ' number ' "
v - model = "form.marketAnalysis[field.key]" : min = "0" : step = "0.01"
controls - position = "right" : placeholder = "field.placeholder" / > < el -input v -else
v - model = "form.marketAnalysis[field.key]" : placeholder = "field.placeholder" / > < b
v - if = "field.suffix" > { { field . suffix } } < / b > < / div >
< / label >
< / div >
< / section >
< section class = "form-section" >
< div class = "section-title" > < span > 03 < / span >
< div >
< h3 > 目标用户群体分析 < / h3 >
< p > 从不同消费能力和使用场景中识别最有价值的用户 。 < / p >
< / div >
< / div >
< div class = "table-wrap" >
< table >
< thead >
< tr >
< th > 用户群体 < / th >
< th > 典型场景 < / th >
< th > 核心需求 < / th >
< th > 消费能力 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="(user, index) in form.targetUsers" :key ="index" >
< td > < el -input v -model = " user.userGroup " type = "textarea"
: autosize = "{ minRows: 2, maxRows: 4 }"
: placeholder = "targetUserExamples[index].userGroup" / > < / td >
< td > < el -input v -model = " user.scenario " type = "textarea"
: autosize = "{ minRows: 2, maxRows: 4 }"
: placeholder = "targetUserExamples[index].scenario" / > < / td >
< td > < el -input v -model = " user.coreNeed " type = "textarea"
: autosize = "{ minRows: 2, maxRows: 4 }"
: placeholder = "targetUserExamples[index].coreNeed" / > < / td >
< td > < el -input v -model = " user.spendingPower "
: placeholder = "targetUserExamples[index].spendingPower" / > < / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
< section class = "form-section" >
< div class = "section-title" > < span > 04 < / span >
< div >
< h3 > 商业模式梳理 < / h3 >
< p > 分别说明基础销售和持续增值服务如何形成收入 。 < / p >
< / div >
< / div >
< div class = "table-wrap" >
< table >
< thead >
< tr >
< th > 模块 < / th >
< th > 案例公司的策略 < / th >
< th > 说明 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="model in form.businessModels" :key="model.module" >
< td class = "fixed-cell" > { { model . module } } < / td >
< td > < el -input v -model = " model.strategy " type = "textarea"
: autosize = "{ minRows: 2, maxRows: 4 }" placeholder = "填写渠道、产品组合或服务策略" / >
< / td >
< td > < el -input v -model = " model.explanation " type = "textarea"
: autosize = "{ minRows: 2, maxRows: 4 }" placeholder = "说明策略与用户、成本的关系" / >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
< section class = "form-section" >
< div class = "section-title" > < span > 05 < / span >
< div >
< h3 > 成本结构分析 < / h3 >
< p > 填写各项单位成本 , 单位总成本由系统自动汇总 。 < / p >
< / div >
< / div >
< div class = "table-wrap" >
< table >
< thead >
< tr >
< th > 成本项目 < / th >
< th > 金额 ( 元 / 台 ) < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="cost in form.costs" :key="cost.id" >
< td class = "fixed-cell" > { { cost . label } } < / td >
< td > < el -input -number v -model = " cost.amount " :min ="0" :step ="0.01"
controls - position = "right" placeholder = "填写金额" / > < / td >
< / tr >
< tr class = "total-row" >
< td > 单位总成本 < / td >
< td > < span class = "computed-value" > { { currency ( totalCost ) } } < / span > < / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
< section class = "form-section" >
< div class = "section-title" > < span > 06 < / span >
< div >
< h3 > 定价与毛利分析 < / h3 >
< p > 单位毛利 = 单位目标售价 − 单位总成本 ; 毛利率 = 单位毛利 ÷ 单位总成本 。 < / p >
< / div >
< / div >
< div class = "table-wrap" >
< table >
< thead >
< tr >
< th > 指标 < / th >
< th > 数值 < / th >
< / tr >
< / thead >
< tbody >
< tr >
< td class = "fixed-cell" > 目标售价 ( 元 / 台 ) < / td >
< td > < el -input -number v -model = " form.targetPrice " :min ="0" :step ="0.01"
controls - position = "right" placeholder = "填写目标售价" / > < / td >
< / tr >
< tr class = "total-row" >
< td > 单位毛利 ( 元 / 台 ) < / td >
< td > < span class = "computed-value" > { { currency ( unitGrossProfit ) } } < / span > < / td >
< / tr >
< tr class = "total-row" >
< td > 毛利率 ( % ) < / td >
< td > < span class = "computed-value" > { { percent ( grossMargin ) } } < / span > < / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
< section class = "form-section" >
< div class = "section-title" > < span > 07 < / span >
< div >
< h3 > 风险识别与评估 < / h3 >
< p > 识别项目主要风险 , 并明确影响程度及可执行的应对措施 。 < / p >
< / div >
< / div >
< div class = "table-wrap" >
< table >
< thead >
< tr >
< th > 风险类型 < / th >
< th > 具体风险描述 < / th >
< th > 影响程度 < / th >
< th > 应对措施 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="risk in form.risks" :key="risk.type" >
< td class = "fixed-cell" > { { risk . type } } < / td >
< td > < el -input v -model = " risk.description " type = "textarea"
: autosize = "{ minRows: 2, maxRows: 4 }" placeholder = "填写具体风险" / > < / td >
< td > < el -select v -model = " risk.impact " placeholder = "请选择" size = "default"
class = "risk-impact-select" > < el -option label = "高" value = "高" / > < el -option
label = "中" value = "中" / > < el -option label = "低" value = "低" / > < / e l - s e l e c t >
< / td >
< td > < el -input v -model = " risk.response " type = "textarea"
: autosize = "{ minRows: 2, maxRows: 4 }" placeholder = "填写应对措施" / > < / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
< div class = "step-actions" > < button class = "action-button primary" type = "button" :disabled ="saving"
@ click = "validateAndContinue" > { { saving ? '校验并保存中…' : '校验并进入第 2 步' } } < / button > < button
class = "action-button" type = "button" : disabled = "saving" @ click = "resetForm" > < el -icon >
< RefreshLeft / >
< / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / section >
< section v -else -if = " currentStep = = = 2 " class = "step-card" >
< div class = "step-heading" >
< div >
< p class = "section-eyebrow" > Step 02 < / p >
< h2 > 项目可行性初步决策 < / h2 >
< / div > < em > 立项判断 < / em >
< / div >
< p class = "intro" > 综合以上所有分析 , 初步判断项目的可行性 , 并明确如果立项后最需要优先解决的关键问题 。 < / p >
< section class = "decision-section" >
< h3 > < span > 01 < / span > 该项目商业可行性总体评估为 : < / h3 >
< el -radio -group v-model ="stepTwo.decision" class="decision-grid" @change="handleDecisionChange" >
< el -radio v -for = " option in feasibilityOptions " :key ="option.value" :label ="option.value"
class = "decision-option" >
< strong > { { option . label } } < / strong > < small > { { option . hint } } < / small >
< / e l - r a d i o >
< / e l - r a d i o - g r o u p >
< / section >
< section class = "decision-section priority-section"
: class = "{ muted: stepTwo.decision === 'not-feasible' }" >
< div class = "priority-heading" >
< h3 > < span > 02 < / span > 如果立项 , 你认为最需要优先解决的 3 个关键问题是 : < / h3 > < b > { { selectedPriorityCount } } / 3 < / b >
< / div >
< p v-if ="stepTwo.decision === 'not-feasible'" class="hint" > 已 选 择 " 不 建 议 立 项 " , 本 项 无 需 填 写 。 < / p >
< p v -else class = "hint" > 请选择且仅选择 3 项 , 作为后续立项推进的优先事项 。 < / p >
< el -checkbox -group v-model ="stepTwo.priorities" class="priority-grid" >
< el -checkbox v -for = " item in priorityOptions " :key ="item.value" :label ="item.value"
: disabled = "stepTwo.decision === 'not-feasible' || (!stepTwo.priorities.includes(item.value) && selectedPriorityCount >= 3)"
class = "priority-option" >
< span > < strong > { { item . label } } < / strong > < small > { { item . detail } } < / small > < / span >
< / e l - c h e c k b o x >
< / e l - c h e c k b o x - g r o u p >
< / section >
< div class = "step-actions" > < button class = "action-button primary" type = "button" :disabled ="saving"
@ click = "saveStepTwo" > { { saving ? '校验并保存中…' : '保存并进入第 3 步' } } < el -icon > < ArrowRight / > < / e l - i c o n > < / b u t t o n > < b u t t o n
class = "action-button" type = "button" : disabled = "saving" @ click = "resetStepTwo" > < el -icon >
< RefreshLeft / >
< / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / section >
< section v -else -if = " currentStep = = = 3 " class = "step-card report-step" >
< div class = "step-heading" >
< div >
< p class = "section-eyebrow" > Step 03 < / p >
< h2 > 可行性论证报告 < / h2 >
< / div > < em > 成果提交 < / em >
< / div >
< div class = "report-guide" >
< p > 根据前两步的论证结构与信息 , 结合实训 4 – 6 , 撰写可行性论证报告 。 可参考以下方法 : < / p >
< ol >
< li > 根据报告模板 , 自行分析撰写可行性论证报告 。 < / li >
< li > 使用通用 AI 大模型辅助完成报告 , 参考步骤提示词见案例文档 。 < / li >
< / ol >
< / div >
< section class = "report-upload-panel" >
< input ref = "reportInput" class = "visually-hidden" type = "file"
accept = ".pdf,.doc,.docx,.png,.jpg,.jpeg" @ change = "handleReportUpload" / >
< template v-if ="!report.url" > < el -icon class = "upload-orbit" >
< Document / >
< / e l - i c o n >
< h3 > 上传可行性论证报告 < / h3 >
< p > 支持 PDF 、 Word 、 PNG 、 JPG , 文件不超过 20 MB 。 < / p > < button class = "action-button primary" type = "button"
: disabled = "uploading" @ click = "openReportPicker" > < el -icon >
< UploadFilled / >
< / e l - i c o n > { { u p l o a d i n g ? ' 上 传 中 … ' : ' 选 择 报 告 附 件 ' } } < / b u t t o n >
< / template >
< template v-else >
< div class = "report-file-card" >
< div class = "report-file-icon" > < el -icon >
< Document / >
< / e l - i c o n > < / d i v >
< div class = "report-file-meta" > < strong > { { report . name || '可行性论证报告' } } < / strong > < span > { {
reportKindLabel } } · { { readableSize ( report . size ) } } < / span > < / div >
< div class = "report-file-actions" > < button type = "button"
@ click = "openReportPreview" > < el -icon >
< View / >
< / e l - i c o n > 预 览 < / b u t t o n > < b u t t o n t y p e = " b u t t o n " @ c l i c k = " d o w n l o a d R e p o r t " > < e l - i c o n >
< Download / >
< / e l - i c o n > 下 载 < / b u t t o n > < b u t t o n t y p e = " b u t t o n " c l a s s = " d a n g e r "
@ click = "clearReport" > < el -icon >
< Delete / >
< / e l - i c o n > 移 除 < / b u t t o n > < / d i v >
< / div >
< div class = "report-replace" > < button class = "text-action" type = "button" :disabled ="uploading"
@ click = "openReportPicker" > < el -icon >
< UploadFilled / >
< / e l - i c o n > { { u p l o a d i n g ? ' 上 传 中 … ' : ' 更 换 附 件 ' } } < / b u t t o n > < s p a n > 附 件 已 上 传 , 可 随 时 预 览 或 下 载 核 对 。 < / s p a n >
< / div >
< / template >
< / section >
< div class = "step-actions" > < button class = "action-button primary" type = "button"
: disabled = "saving || uploading" @ click = "saveStepThree" > { { saving ? '校验并保存中…' : '保存并进入第 4 步'
} } < el -icon > < ArrowRight / > < / e l - i c o n > < / b u t t o n > < / d i v >
< el -dialog v -model = " reportPreviewVisible " class = "report-preview-dialog"
: title = "report.name || '可行性论证报告预览'" width = "min(1100px, 94vw)" destroy - on - close append - to - body >
< div class = "report-preview" > < el -image v -if = " reportKind = = = ' image ' " :src ="report.url"
fit = "contain" : preview - src - list = "[report.url]" preview - teleported / > < iframe v -else
: src = "reportPreviewUrl" title = "可行性论证报告预览" > < / iframe > < / div > < template # footer > < button
class = "action-button" type = "button" @ click = "downloadReport" > < el -icon >
< Download / >
< / e l - i c o n > 下 载 附 件 < / b u t t o n > < / t e m p l a t e >
< / e l - d i a l o g >
< / section >
< section v -else class = "step-card placeholder" >
< p > 第 { { currentStep } } 步将在后续实训页面中配置 。 < / p > < button class = "action-button" type = "button"
@ click = "currentStep = 3" > 返回第 3
步 < / button >
< / section >
< / section >
< / main >
< TrainingAiSidebar study -tip = " 先 从 案 例 资 料 中 提 炼 可 验 证 的 市 场 、 用 户 与 成 本 依 据 , 再 形 成 一 致 的 商 业 论 证 。 " reference -title = " 可 行 性 论 证 提 示 "
reference - text = "• 结论要有数据或前序分析支撑<br />• 价格、成本和毛利需要逻辑一致<br />• 风险应附带可执行的应对措施"
: progress - items = "progressItems" / >
< / div >
< / template >
< script setup >
import { ArrowRight , Check , CircleCheck , Delete , Document , Download , RefreshLeft , UploadFilled , View } from "@element-plus/icons-vue"
import { checkBusinessFeasibilityStepOne , checkBusinessFeasibilityStepTwo , checkBusinessFeasibilityStepThree , getStudentTrainingAnswer , saveStudentTrainingAnswer , uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"
import { getTrainingTaskByKey } from "@/api/trainingTask"
import { parseTrainingArray } from "@/views/training/taskKeyMap"
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue" ;
const TASK _KEY = "business-feasibility"
const DEFAULT _STEPS = [ "商业可行性论证" , "项目初步决策" , "可行性论证报告" , "决策结论" ]
const overviewFields = [
{ key : "projectBackground" , label : "项目背景" , placeholder : "示例:根据案例信息简述公司基本情况,再简要阐述智能家居、个护健康、美妆工具等相关市场发展情况和需求情况。" } ,
{ key : "productPositioning" , label : "产品定位" , placeholder : "示例:阐述项目产品针对的目标市场用户、核心卖点、使用场景、差异化理由以及用户收益。" } ,
{ key : "projectGoals" , label : "项目目标" , placeholder : "示例:填写 12 个月短期目标、24 个月中期目标和 36 个月长期目标。" } ,
{ key : "macroEnvironmentConclusion" , label : "宏观环境分析结论" , placeholder : "示例:项目产品 PEST 分析结论。" } ,
{ key : "competitiveLandscapeConclusion" , label : "竞争态势分析结论" , placeholder : "示例:项目产品 SWOT 分析结论。" } ,
]
const marketFields = [
{ key : "annualMarketSize" , label : "年度市场规模" , type : "number" , suffix : "亿元" , placeholder : "例如 80" } ,
{ key : "monthlyGrowthRate" , label : "月均增长率" , type : "number" , suffix : "%" , placeholder : "例如 2.5" } ,
{ key : "topBrandMarketShare" , label : "头部第一品牌市场份额" , type : "number" , suffix : "%" , placeholder : "例如 26" } ,
{ key : "competitorsPriceRange" , label : "主要竞品售价区间" , suffix : "元" , placeholder : "例如 299 - 899" } ,
{ key : "marketConcentrationCr3" , label : "市场集中度( CR3) " , type : "number" , suffix : "%" , placeholder : "例如 58" } ,
]
const targetUserExamples = [
{ userGroup : "示例: 18-35 岁一二线城市美妆爱好者、美妆博主" , scenario : "示例:美妆产品带货、线上美妆教程" , coreNeed : "示例: 补光效果、AI 测肤精度" , spendingPower : "示例:高" } ,
{ userGroup : "示例: 22-40 岁租房白领、早八通勤族" , scenario : "示例:早上上班前快速护肤化妆" , coreNeed : "示例:日常化妆、耐用" , spendingPower : "示例:中" } ,
{ userGroup : "示例: 18-28 岁学生群体、礼品采购人群" , scenario : "示例:宿舍美妆尝试、礼物赠送" , coreNeed : "示例:宿舍美妆工具、生日礼物" , spendingPower : "示例:低" } ,
]
const feasibilityOptions = [
{ value : "highly-feasible" , label : "高度可行,建议立即立项启动" , hint : "市场、收益与风险条件均具备" } ,
{ value : "feasible-after-optimization" , label : "基本可行,建议在优化部分条件后立项" , hint : "关键条件完善后再推进" } ,
{ value : "observe" , label : "可行性一般,建议暂缓立项继续观察" , hint : "保留机会并持续验证" } ,
{ value : "not-feasible" , label : "不可行,建议放弃该方向" , hint : "当前条件不支持投入" } ,
]
const priorityOptions = [
{ value : "hardware-cost-control" , label : "硬件成本控制" , detail : "降低 BOM 成本" } ,
{ value : "content-ecosystem" , label : "内容生态搭建" , detail : "相关教程课程合作" } ,
{ value : "channel-expansion" , label : "渠道拓展" , detail : "线上流量获取" } ,
{ value : "brand-differentiation" , label : "品牌差异化" , detail : "与头部品牌形成区隔" } ,
{ value : "supply-chain-management" , label : "供应链管理" , detail : "产能保障与品质控制" } ,
{ value : "capital-planning" , label : "资金筹措" , detail : "融资或分期投入" } ,
]
const defaultTask = { title : "商业可行性论证" , background : "在产品开发前,综合市场机会、用户需求、竞争态势、成本收益和风险,判断项目是否具有可推进的商业价值。" , goals : [ "掌握商业可行性论证的基本维度" , "形成市场、用户、成本与收益的一致判断" , "识别主要风险并提出应对措施" ] , requirement : "请以案例资料和前序分析为依据,完成表格全部内容;成本与价格数据需真实、合理且逻辑一致。" }
const { proxy } = getCurrentInstance ( )
const briefOpen = ref ( false )
const taskConfig = ref ( null ) ; const currentStep = ref ( 1 ) ; const saving = ref ( false ) ; const uploading = ref ( false ) ; const completed = ref ( false ) ; const stepTwoCompleted = ref ( false ) ; const stepThreeCompleted = ref ( false ) ; const reportInput = ref ( null ) ; const reportPreviewVisible = ref ( false ) ; const report = ref ( createReport ( ) ) ; const form = ref ( createForm ( ) ) ; const stepTwo = ref ( createStepTwo ( ) )
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 totalCost = computed ( ( ) => form . value . costs . reduce ( ( sum , cost ) => sum + numeric ( cost . amount ) , 0 ) ) ; const unitGrossProfit = computed ( ( ) => numeric ( form . value . targetPrice ) - totalCost . value ) ; const grossMargin = computed ( ( ) => totalCost . value > 0 ? unitGrossProfit . value / totalCost . value * 100 : null )
const progressItems = computed ( ( ) => [ { text : completed . value ? "项目论证已保存" : "待完成项目论证" , status : completed . value ? "done" : "" } , { text : totalCost . value > 0 ? "成本与毛利已计算" : "待填写成本与售价" , status : totalCost . value > 0 ? "done" : "" } , { text : form . value . risks . filter ( ( risk ) => risk . description && risk . response && risk . impact ) . length === 4 ? "风险评估已完成" : "待完成风险评估" , status : form . value . risks . filter ( ( risk ) => risk . description && risk . response && risk . impact ) . length === 4 ? "done" : "" } ] )
const selectedPriorityCount = computed ( ( ) => stepTwo . value . priorities . length )
const reportKind = computed ( ( ) => resolveReportKind ( report . value ) ) ; const reportKindLabel = computed ( ( ) => ( { image : "图片附件" , pdf : "PDF 文档" , word : "Word 文档" } [ reportKind . value ] || "报告附件" ) ) ; const reportPreviewUrl = computed ( ( ) => { if ( reportKind . value !== "word" ) return report . value . url ; const absoluteUrl = /^https?:\/\//i . test ( report . value . url ) ? report . value . url : new URL ( report . value . url , window . location . origin ) . href ; return ` https://view.officeapps.live.com/op/view.aspx?src= ${ encodeURIComponent ( absoluteUrl ) } ` } )
onMounted ( async ( ) => { await loadTaskConfig ( ) ; await loadSavedAnswer ( ) } )
function createForm ( ) { return { projectOverview : { projectBackground : "" , productPositioning : "" , projectGoals : "" , macroEnvironmentConclusion : "" , competitiveLandscapeConclusion : "" } , marketAnalysis : { annualMarketSize : "" , monthlyGrowthRate : "" , topBrandMarketShare : "" , competitorsPriceRange : "" , marketConcentrationCr3 : "" } , targetUsers : Array . from ( { length : 3 } , ( ) => ( { userGroup : "" , scenario : "" , coreNeed : "" , spendingPower : "" } ) ) , businessModels : [ { module : "产品销售渠道" , strategy : "" , explanation : "" } , { module : "增值服务" , strategy : "" , explanation : "" } ] , costs : [ { id : "hardware-bom" , label : "硬件 BOM 成本" , amount : "" } , { id : "manufacturing" , label : "生产制造费用" , amount : "" } , { id : "rd-amortization" , label : "研发费用摊销" , amount : "" } , { id : "channel-marketing" , label : "渠道与营销费用" , amount : "" } , { id : "logistics-warehouse" , label : "物流仓储费用" , amount : "" } ] , targetPrice : "" , risks : [ "技术风险" , "市场风险" , "供应链风险" , "资金风险" ] . map ( ( type ) => ( { type , description : "" , impact : "" , response : "" } ) ) } }
function createStepTwo ( ) { return { decision : "" , priorities : [ ] } }
function createReport ( ) { return { url : "" , name : "" , size : 0 } }
function numeric ( value ) { const number = Number ( value ) ; return Number . isFinite ( number ) ? number : 0 }
function currency ( value ) { return Number ( value || 0 ) . toFixed ( 2 ) }
function percent ( value ) { return value === null || ! Number . isFinite ( value ) ? "--" : ` ${ Number ( value ) . toFixed ( 2 ) } % ` }
function parseArray ( value , fallback ) { if ( Array . isArray ( value ) && value . length ) return value ; 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 ( _ ) { const items = String ( value ) . split ( /[, ,|]/ ) . map ( ( item ) => item . trim ( ) ) . filter ( Boolean ) ; return items . length ? items : fallback } }
async function loadTaskConfig ( ) { try { taskConfig . value = ( await getTrainingTaskByKey ( TASK _KEY ) ) ? . data || null } catch ( _ ) { taskConfig . value = null } }
async function loadSavedAnswer ( ) { try { const saved = await getStudentTrainingAnswer ( TASK _KEY ) ? . data ; if ( ! saved ) return ; if ( saved . step1Answer ) { const parsed = JSON . parse ( saved . step1Answer ) ; form . value = mergeForm ( parsed ? . form || parsed ) ; completed . value = true } if ( saved . step2Answer ) { const parsed = JSON . parse ( saved . step2Answer ) ; stepTwo . value = { ... createStepTwo ( ) , ... ( parsed ? . stepTwo || parsed || { } ) , priorities : Array . isArray ( parsed ? . stepTwo ? . priorities || parsed ? . priorities ) ? ( parsed ? . stepTwo ? . priorities || parsed ? . priorities ) : [ ] } ; stepTwoCompleted . value = true } if ( saved . step3Answer ) { const parsed = JSON . parse ( saved . step3Answer ) ; report . value = { ... createReport ( ) , ... ( parsed ? . report || parsed || { } ) } ; stepThreeCompleted . value = Boolean ( report . value . url ) } if ( Number ( saved . currentStep ) > 0 ) currentStep . value = Number ( saved . currentStep ) } catch ( _ ) { /* keep a new answer editable */ } }
function mergeForm ( value ) { const fallback = createForm ( ) ; const merged = { ... fallback , ... ( value || { } ) , projectOverview : { ... fallback . projectOverview , ... ( value ? . projectOverview || { } ) } , marketAnalysis : { ... fallback . marketAnalysis , ... ( value ? . marketAnalysis || { } ) } , targetUsers : Array . isArray ( value ? . targetUsers ) && value . targetUsers . length === 3 ? value . targetUsers : fallback . targetUsers , businessModels : Array . isArray ( value ? . businessModels ) && value . businessModels . length === 2 ? value . businessModels : fallback . businessModels , costs : fallback . costs . map ( ( cost ) => ( { ... cost , ... ( value ? . costs || [ ] ) . find ( ( item ) => item ? . id === cost . id ) } ) ) , risks : fallback . risks . map ( ( risk ) => ( { ... risk , ... ( value ? . risks || [ ] ) . find ( ( item ) => item ? . type === risk . type ) } ) ) } ; return merged }
function payload ( ) { return { ... form . value , costs : form . value . costs . map ( ( { id , amount } ) => ( { id , amount : amount === "" ? null : Number ( amount ) } ) ) , targetPrice : form . value . targetPrice === "" ? null : Number ( form . value . targetPrice ) } }
async function validateAndContinue ( ) { if ( saving . value ) return ; saving . value = true ; try { const result = await checkBusinessFeasibilityStepOne ( payload ( ) ) ; const calculated = result ? . data || { } ; form . value . costs = form . value . costs . map ( ( cost ) => ( { ... cost , amount : cost . amount === "" ? "" : Number ( cost . amount ) } ) ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step1Answer : JSON . stringify ( { form : form . value , calculations : { unitTotalCost : calculated . unitTotalCost ? ? totalCost . value , unitGrossProfit : calculated . unitGrossProfit ? ? unitGrossProfit . value , grossMargin : calculated . grossMargin ? ? grossMargin . value } } ) , currentStep : 2 , submitted : false , saveAction : "SAVE" } ) ; completed . value = true ; currentStep . value = 2 ; proxy ? . $modal ? . msgSuccess ( "已完成校验并保存,已进入第 2 步" ) } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请完整填写本步骤内容后再提交" ) } finally { saving . value = false } }
function resetForm ( ) { form . value = createForm ( ) ; completed . value = false ; proxy ? . $modal ? . msgSuccess ( "已清空第 1 步填写内容" ) }
async function saveStepTwo ( ) { if ( saving . value ) return ; saving . value = true ; try { await checkBusinessFeasibilityStepTwo ( { ... stepTwo . value } ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step2Answer : JSON . stringify ( { stepTwo : stepTwo . value } ) , currentStep : 3 , submitted : false , saveAction : "SAVE" } ) ; stepTwoCompleted . value = true ; currentStep . value = 3 ; proxy ? . $modal ? . msgSuccess ( "已完成校验并保存,已进入第 3 步" ) } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请选择总体评估,并按要求填写优先问题" ) } finally { saving . value = false } }
function resetStepTwo ( ) { stepTwo . value = createStepTwo ( ) ; stepTwoCompleted . value = false ; proxy ? . $modal ? . msgSuccess ( "已清空第 2 步填写内容" ) }
function handleDecisionChange ( ) { if ( stepTwo . value . decision === "not-feasible" ) stepTwo . value . priorities = [ ] }
function openReportPicker ( ) { reportInput . value ? . click ( ) }
async function handleReportUpload ( event ) { const file = event ? . target ? . files ? . [ 0 ] ; if ( event ? . target ) event . target . value = "" ; if ( ! file ) return ; const extension = file . name . split ( "." ) . pop ( ) ? . toLowerCase ( ) ; if ( ! [ "pdf" , "doc" , "docx" , "png" , "jpg" , "jpeg" ] . includes ( extension ) ) { proxy ? . $modal ? . msgError ( "仅支持 PDF、Word、PNG、JPG 格式的报告附件" ) ; return } if ( file . size > 20 * 1024 * 1024 ) { proxy ? . $modal ? . msgError ( "报告附件不能超过 20MB" ) ; return } uploading . value = true ; try { const data = new FormData ( ) ; data . append ( "file" , file ) ; const response = await uploadStudentTrainingFile ( data ) ; const url = response ? . url || response ? . data ? . url || "" ; if ( ! url ) throw new Error ( "上传未返回文件地址,请重新选择报告附件" ) ; report . value = { url , name : file . name , size : file . size } ; proxy ? . $modal ? . msgSuccess ( "报告附件上传成功,可点击预览核对" ) } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "报告附件上传失败,请重试" ) } finally { uploading . value = false } }
function resolveReportKind ( file ) { const name = ` ${ file ? . name || "" } ${ file ? . url || "" } ` . toLowerCase ( ) ; if ( /\.(png|jpe?g)(?:\?|$)/ . test ( name ) ) return "image" ; if ( /\.pdf(?:\?|$)/ . test ( name ) ) return "pdf" ; return "word" }
function readableSize ( size ) { const value = Number ( size || 0 ) ; return value > 1024 * 1024 ? ` ${ ( value / 1024 / 1024 ) . toFixed ( 1 ) } MB ` : value > 1024 ? ` ${ ( value / 1024 ) . toFixed ( 1 ) } KB ` : "附件已上传" }
function openReportPreview ( ) { if ( report . value . url ) reportPreviewVisible . value = true }
function downloadReport ( ) { if ( ! report . value . url ) return ; const link = document . createElement ( "a" ) ; link . href = report . value . url ; link . target = "_blank" ; link . rel = "noopener" ; link . download = report . value . name || "可行性论证报告" ; document . body . appendChild ( link ) ; link . click ( ) ; document . body . removeChild ( link ) }
function clearReport ( ) { report . value = createReport ( ) ; reportPreviewVisible . value = false ; stepThreeCompleted . value = false }
async function saveStepThree ( ) { if ( saving . value ) return ; saving . value = true ; try { await checkBusinessFeasibilityStepThree ( { reportUrl : report . value . url , reportName : report . value . name } ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step3Answer : JSON . stringify ( { report : report . value } ) , currentStep : 4 , submitted : false , saveAction : "SAVE" } ) ; stepThreeCompleted . value = true ; currentStep . value = 4 ; proxy ? . $modal ? . msgSuccess ( "报告已保存,已进入第 4 步" ) } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请先上传可行性论证报告" ) } finally { saving . value = false } }
function switchStep ( step ) { if ( step > 1 && ! completed . value ) { proxy ? . $modal ? . msgWarning ( "请先完成并保存第 1 步" ) ; return } if ( step > 2 && ! stepTwoCompleted . value ) { proxy ? . $modal ? . msgWarning ( "请先完成并保存第 2 步" ) ; return } if ( step > 3 && ! stepThreeCompleted . value ) { proxy ? . $modal ? . msgWarning ( "请先上传并保存第 3 步报告" ) ; return } currentStep . value = step }
function exportOutcome ( ) { const blob = new Blob ( [ JSON . stringify ( { task : taskTitle . value , step : 1 , ... payload ( ) , calculations : { unitTotalCost : totalCost . value , unitGrossProfit : unitGrossProfit . value , grossMargin : grossMargin . value } } , null , 2 ) ] , { type : "application/json;charset=utf-8" } ) ; const link = document . createElement ( "a" ) ; link . href = URL . createObjectURL ( blob ) ; link . download = "商业可行性论证-第1步.json" ; link . click ( ) ; URL . revokeObjectURL ( link . href ) }
< / script >
< style lang = "scss" scoped >
/* ===== 页面级令牌(教育蓝极简主题) ===== */
. feasibility - 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 ;
line - height : 1.55 ;
}
. feasibility - core {
min - width : 0 ;
display : grid ;
gap : 10 px ;
}
/* task-header 独立卡片 */
. 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 - head - main {
display : grid ;
justify - items : start ;
gap : 8 px ;
min - width : 0 ;
}
. task - action - strip {
margin : 0 ;
padding : 10 px 16 px ;
}
. task - action - strip : deep ( . training - task - toolbar ) {
display : flex ;
flex - wrap : wrap ;
align - items : center ;
justify - content : flex - end ;
gap : 8 px ;
}
. task - title {
margin : 0 ;
color : var ( -- student - text ) ;
font - size : 17 px ;
font - weight : 700 ;
line - height : 1.35 ;
}
. 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 ;
b {
color : var ( -- student - text ) ;
}
ul {
display : grid ;
gap : 4 px ;
margin : 0 ;
padding - left : 18 px ;
}
p {
margin : 0 ;
}
}
/* workbench: 扁平化容器 */
. workbench {
margin - top : 0 ;
padding : 0 ;
border : 0 ;
border - radius : 0 ;
background : transparent ;
display : grid ;
gap : 12 px ;
}
. workbench - actions {
display : flex ;
flex : 0 0 auto ;
justify - content : flex - end ;
gap : 8 px ;
}
/* action-button 基础样式(次按钮:白底描边) */
. action - button {
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 6 px ;
min - 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 : inherit ;
font - size : 13 px ;
font - weight : 600 ;
cursor : pointer ;
transition : transform 0.16 s ease , border - color 0.16 s ease , background 0.16 s ease , box - shadow 0.16 s ease ;
& : hover : not ( : disabled ) {
transform : translateY ( - 1 px ) ;
border - color : var ( -- student - accent ) ;
color : var ( -- student - accent ) ;
background : # eff6ff ;
box - shadow : 0 2 px 6 px rgba ( 37 , 99 , 235 , 0.12 ) ;
}
& . primary {
border - color : var ( -- student - accent ) ;
color : # ffffff ;
background : var ( -- student - accent ) ;
font - weight : 700 ;
box - shadow : 0 2 px 8 px rgba ( 37 , 99 , 235 , 0.22 ) ;
& : hover : not ( : disabled ) {
transform : translateY ( - 1 px ) ;
border - color : var ( -- student - accent - deep ) ;
background : var ( -- student - accent - deep ) ;
color : # ffffff ;
box - shadow : 0 4 px 12 px rgba ( 29 , 78 , 216 , 0.30 ) ;
}
}
& : disabled {
cursor : not - allowed ;
opacity : 0.5 ;
transform : none ;
}
}
/* step-card: 独立卡片 */
. step - card {
padding : 16 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 16 px ;
background : var ( -- student - card - bg ) ;
}
. step - heading {
display : flex ;
align - items : flex - start ;
justify - content : space - between ;
gap : 14 px ;
}
. section - eyebrow {
margin : 0 0 2 px ;
color : var ( -- student - text - secondary ) ;
font - size : 11 px ;
font - weight : 800 ;
letter - spacing : 0.12 em ;
}
. step - heading h2 {
margin : 0 ;
color : var ( -- student - text ) ;
font - size : 15 px ;
font - weight : 700 ;
}
. step - heading em {
padding : 4 px 10 px ;
border : 1 px solid # d6dae0 ;
border - radius : 999 px ;
color : var ( -- student - accent - light ) ;
background : var ( -- student - accent - soft ) ;
font - size : 11 px ;
font - style : normal ;
font - weight : 700 ;
white - space : nowrap ;
}
. intro {
margin : 10 px 0 0 ;
color : # 374151 ;
font - size : 13 px ;
line - height : 1.6 ;
}
. form - section {
margin - top : 24 px ;
}
. section - title {
display : flex ;
align - items : center ;
gap : 10 px ;
margin - bottom : 12 px ;
}
. section - title > span {
display : grid ;
width : 26 px ;
height : 26 px ;
place - items : center ;
border : 0 ;
border - radius : 6 px ;
color : # ffffff ;
background : var ( -- student - accent ) ;
font - size : 11 px ;
font - weight : 900 ;
flex - shrink : 0 ;
}
. section - title h3 {
margin : 0 ;
color : var ( -- student - text ) ;
font - size : 14 px ;
font - weight : 700 ;
}
. section - title p {
margin : 2 px 0 0 ;
color : var ( -- student - text - secondary ) ;
font - size : 12 px ;
line - height : 1.55 ;
}
/* form-table: 扁平化表格 */
. form - table {
display : grid ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 12 px ;
overflow : hidden ;
background : # ffffff ;
}
. form - table label {
display : grid ;
grid - template - columns : 180 px 1 fr ;
border - bottom : 1 px solid var ( -- student - panel - border ) ;
}
. form - table label : last - child {
border - bottom : 0 ;
}
. form - table label > span {
display : flex ;
align - items : center ;
justify - content : flex - start ;
padding : 10 px 12 px ;
color : var ( -- student - text ) ;
background : var ( -- student - panel - bg ) ;
font - weight : 700 ;
font - size : 13 px ;
}
. market - table {
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
}
. market - table label {
border - right : 1 px solid var ( -- student - panel - border ) ;
}
. market - table label : nth - child ( 2 n ) {
border - right : 0 ;
}
. market - table label : last - child {
border - bottom : 0 ;
}
. input - with - suffix {
display : flex ;
align - items : center ;
gap : 8 px ;
}
. input - with - suffix b {
color : var ( -- student - text - secondary ) ;
font - size : 12 px ;
white - space : nowrap ;
font - weight : 700 ;
}
/* table-wrap + Element Plus table 内的表单组件 */
. table - wrap {
overflow - x : auto ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 12 px ;
background : # ffffff ;
}
table {
width : 100 % ;
min - width : 780 px ;
border - collapse : collapse ;
}
th {
color : var ( -- student - text ) ;
font - size : 13 px ;
font - weight : 800 ;
background : var ( -- student - panel - bg ) ;
text - align : center ;
}
th ,
td {
border : 1 px solid var ( -- student - panel - border ) ;
padding : 8 px 10 px ;
vertical - align : middle ;
}
tr : last - child td {
border - bottom : 0 ;
}
th : last - child ,
td : last - child {
border - right : 0 ;
}
/* Element Plus 组件在表格中的紧凑样式 */
. table - wrap td : deep ( . el - textarea ) ,
. table - wrap td : deep ( . el - input ) ,
. table - wrap td : deep ( . el - select ) {
max - width : 100 % ;
}
. table - wrap td : deep ( . el - textarea _ _inner ) {
min - height : 60 px ! important ;
padding : 6 px 10 px ;
font - size : 13 px ;
line - height : 1.55 ;
}
. table - wrap td : deep ( . el - input _ _wrapper ) ,
. table - wrap td : deep ( . el - textarea _ _inner ) ,
. table - wrap td : deep ( . el - select _ _wrapper ) {
border - color : # d1d5db ! important ;
border - radius : 8 px ;
background : # ffffff ;
box - shadow : none ! important ;
& : hover {
border - color : var ( -- student - accent - light ) ! important ;
}
}
. table - wrap td : deep ( . el - input _ _wrapper . is - focus ) ,
. table - wrap td : deep ( . el - textarea _ _inner : focus ) ,
. table - wrap td : deep ( . el - select _ _wrapper . is - focused ) {
border - color : var ( -- student - accent ) ! important ;
box - shadow : 0 0 0 2 px rgba ( 59 , 130 , 246 , 0.15 ) ! important ;
}
. table - wrap td : deep ( . el - input - number ) {
width : 100 % ;
}
. table - wrap td : deep ( . el - input - number . el - input _ _wrapper ) {
padding : 0 8 px ;
}
. fixed - cell {
color : var ( -- student - text ) ;
background : var ( -- student - panel - bg ) ;
text - align : center ;
font - weight : 700 ;
font - size : 13 px ;
}
. computed - value {
display : inline - block ;
color : var ( -- student - text ) ;
font - weight : 700 ;
font - variant - numeric : tabular - nums ;
}
. total - row td {
color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
font - weight : 800 ;
font - size : 13 px ;
}
. step - actions {
display : flex ;
justify - content : center ;
gap : 10 px ;
margin - top : 24 px ;
}
/* placeholder */
. placeholder {
display : grid ;
min - height : 240 px ;
place - content : center ;
justify - items : center ;
gap : 14 px ;
color : var ( -- student - text - secondary ) ;
text - align : center ;
}
. placeholder p {
font - size : 15 px ;
font - weight : 700 ;
color : var ( -- student - text ) ;
}
/* ===== Step 2 决策区 ===== */
. decision - section {
margin - top : 24 px ;
padding : 12 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 14 px ;
background : var ( -- student - panel - bg ) ;
}
. decision - section h3 {
display : flex ;
align - items : center ;
gap : 8 px ;
margin : 0 ;
color : var ( -- student - text ) ;
font - size : 14 px ;
font - weight : 700 ;
}
. decision - section h3 > span {
display : grid ;
width : 22 px ;
height : 22 px ;
place - items : center ;
border : 0 ;
border - radius : 6 px ;
color : # ffffff ;
background : var ( -- student - accent ) ;
font - size : 10 px ;
font - weight : 900 ;
flex - shrink : 0 ;
}
/* decision-grid 使用 Element Plus el-radio-group */
. decision - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 10 px ;
margin - top : 12 px ;
}
: deep ( . decision - option . el - radio ) {
display : grid ;
grid - template - columns : auto 1 fr ;
gap : 8 px ;
align - items : start ;
padding : 12 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 10 px ;
color : # 374151 ;
background : # ffffff ;
transition : border - color 0.16 s ease , background 0.16 s ease , transform 0.16 s ease ;
margin - right : 0 ;
height : auto ;
strong {
color : var ( -- student - text ) ;
font - size : 13 px ;
font - weight : 700 ;
}
small {
display : block ;
margin - top : 3 px ;
color : var ( -- student - text - secondary ) ;
font - size : 11 px ;
line - height : 1.5 ;
}
& : hover {
border - color : var ( -- student - accent - light ) ;
transform : translateY ( - 1 px ) ;
}
}
/* 墨黑单选圆点 */
: deep ( . decision - option . el - radio . el - radio _ _input . el - radio _ _inner ) {
border - color : # 9 ca3af ;
}
: deep ( . decision - option . el - radio . el - radio _ _input . is - checked . el - radio _ _inner ) {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent ) ;
}
: deep ( . decision - option . el - radio . el - radio _ _input . is - checked . el - radio _ _inner : : after ) {
background : # ffffff ;
}
: deep ( . decision - option . el - radio . is - checked ) {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
}
: deep ( . decision - option . el - radio . is - checked . el - radio _ _label ) {
color : var ( -- student - accent ) ;
}
/* priority-grid */
. priority - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 10 px ;
margin - top : 12 px ;
}
: deep ( . priority - option . el - checkbox ) {
display : grid ;
grid - template - columns : auto 1 fr ;
gap : 8 px ;
align - items : start ;
padding : 10 px 12 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 10 px ;
color : # 374151 ;
background : # ffffff ;
transition : border - color 0.16 s ease , background 0.16 s ease , transform 0.16 s ease ;
margin - right : 0 ;
height : auto ;
strong {
display : block ;
color : var ( -- student - text ) ;
font - size : 13 px ;
font - weight : 700 ;
}
small {
display : block ;
margin - top : 3 px ;
color : var ( -- student - text - secondary ) ;
font - size : 11 px ;
line - height : 1.5 ;
}
& : hover : not ( . is - disabled ) {
border - color : var ( -- student - accent - light ) ;
transform : translateY ( - 1 px ) ;
}
& . is - disabled {
opacity : 0.45 ;
cursor : not - allowed ;
}
}
/* 墨黑复选框 */
: deep ( . priority - option . el - checkbox . el - checkbox _ _inner ) {
border - color : # 9 ca3af ;
background : # ffffff ;
}
: deep ( . priority - option . el - checkbox . is - checked . el - checkbox _ _inner ) {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent ) ;
}
. priority - section {
transition : opacity 0.16 s ease ;
}
. priority - section . muted {
opacity : 0.45 ;
}
. priority - heading {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 14 px ;
}
. priority - heading b {
min - width : 44 px ;
padding : 4 px 8 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 999 px ;
color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
text - align : center ;
font - size : 11 px ;
font - weight : 800 ;
}
. hint {
margin : 10 px 0 0 ;
color : var ( -- student - text - secondary ) ;
font - size : 12 px ;
line - height : 1.55 ;
}
/* ===== Step 3 报告区 ===== */
. report - guide {
margin - top : 16 px ;
padding : 12 px 14 px ;
border - left : 3 px solid var ( -- student - accent ) ;
color : # 374151 ;
background : var ( -- student - accent - soft ) ;
line - height : 1.7 ;
font - size : 13 px ;
border - radius : 0 10 px 10 px 0 ;
}
. report - guide p ,
. report - guide ol {
margin : 0 ;
}
. report - guide ol {
margin - top : 6 px ;
padding - left : 18 px ;
}
. report - guide li + li {
margin - top : 3 px ;
}
. report - upload - panel {
display : grid ;
min - height : 260 px ;
place - content : center ;
justify - items : center ;
gap : 10 px ;
margin - top : 20 px ;
padding : 30 px ;
border : 1 px dashed # d1d5db ;
border - radius : 14 px ;
background : var ( -- student - panel - bg ) ;
text - align : center ;
}
. upload - orbit {
display : grid ;
width : 56 px ;
height : 56 px ;
place - items : center ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 16 px ;
color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
font - size : 26 px ;
}
. report - upload - panel h3 {
margin : 4 px 0 0 ;
color : var ( -- student - text ) ;
font - size : 15 px ;
font - weight : 700 ;
}
. report - upload - panel > p {
margin : 0 ;
color : var ( -- student - text - secondary ) ;
font - size : 12 px ;
}
. report - file - card {
display : flex ;
align - items : center ;
width : min ( 720 px , 100 % ) ;
gap : 12 px ;
padding : 14 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 12 px ;
background : # ffffff ;
text - align : left ;
}
. report - file - icon {
display : grid ;
flex : 0 0 40 px ;
width : 40 px ;
height : 40 px ;
place - items : center ;
border - radius : 10 px ;
color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
font - size : 22 px ;
}
. report - file - meta {
min - width : 0 ;
flex : 1 ;
}
. report - file - meta strong ,
. report - file - meta span {
display : block ;
overflow : hidden ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
. report - file - meta strong {
color : var ( -- student - text ) ;
font - size : 13 px ;
font - weight : 700 ;
}
. report - file - meta span {
margin - top : 3 px ;
color : var ( -- student - text - secondary ) ;
font - size : 11 px ;
}
. report - file - actions {
display : flex ;
gap : 6 px ;
flex - shrink : 0 ;
}
. report - file - actions button ,
. text - action {
display : inline - flex ;
align - items : center ;
gap : 4 px ;
border : 0 ;
color : var ( -- student - accent ) ;
background : transparent ;
font : inherit ;
font - size : 12 px ;
font - weight : 600 ;
cursor : pointer ;
padding : 4 px 6 px ;
border - radius : 6 px ;
transition : background 0.16 s ease ;
& : hover {
background : var ( -- student - accent - soft ) ;
}
& . danger {
color : var ( -- student - danger ) ;
& : hover {
background : rgba ( 217 , 85 , 76 , 0.08 ) ;
}
}
}
. report - replace {
display : flex ;
align - items : center ;
gap : 10 px ;
color : var ( -- student - text - secondary ) ;
font - size : 11 px ;
}
. text - action : disabled {
cursor : not - allowed ;
opacity : 0.5 ;
}
. visually - hidden {
position : absolute ;
width : 1 px ;
height : 1 px ;
overflow : hidden ;
clip : rect ( 0 , 0 , 0 , 0 ) ;
white - space : nowrap ;
}
. report - preview {
display : grid ;
min - height : 64 vh ;
place - items : center ;
background : # ffffff ;
}
. report - preview . el - image {
width : 100 % ;
height : 64 vh ;
}
. report - preview iframe {
width : 100 % ;
height : 64 vh ;
border : 0 ;
background : # ffffff ;
}
/* 风险评估下拉框宽度 */
. risk - impact - select {
width : 100 % ;
}
/* ===== 响应式 ===== */
@ media ( max - width : 1280 px ) {
. step - indicator {
overflow - x : auto ;
}
. step - tab {
min - width : 140 px ;
}
. market - table {
grid - template - columns : 1 fr ;
}
. market - table label {
border - right : 0 ;
}
}
@ media ( max - width : 900 px ) {
. form - table label {
grid - template - columns : 1 fr ;
}
. form - table label > span {
justify - content : flex - start ;
padding : 8 px 12 px ;
}
. decision - grid ,
. priority - grid {
grid - template - columns : 1 fr ;
}
. step - heading em {
display : none ;
}
. step - actions {
flex - wrap : wrap ;
}
}
@ media ( max - width : 720 px ) {
. workbench - actions {
justify - content : flex - start ;
flex - wrap : wrap ;
}
. report - upload - panel {
padding : 20 px 14 px ;
}
. report - file - card {
align - items : flex - start ;
flex - wrap : wrap ;
}
. report - file - actions {
width : 100 % ;
justify - content : flex - end ;
}
. report - replace {
flex - wrap : wrap ;
justify - content : center ;
}
}
< / style >
< style lang = "scss" >
/* ===== 步骤导航: 分段连接胶囊( SKILL.md 规格) ===== */
# app . studentLayout section . app - main : has ( . feasibility - 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 - tab {
flex : 1 1 0 ! important ;
max - width : 240 px ! important ;
display : flex ! important ;
flex - direction : row ! important ;
align - items : center ! important ;
justify - content : center ! important ;
gap : 8 px ! important ;
min - width : 0 ! 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 ;
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 - bar {
display : none ! important ;
}
. step - no {
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 - 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 ;
& : : after {
display : none ! important ;
}
}
. step - name {
color : inherit ! important ;
}
& : hover : not ( . active ) : not ( . complete ) {
border - color : # 2563 eb ! important ;
background : # eff6ff ! important ;
color : # 2563 eb ! important ;
z - index : 1 ! important ;
. step - no {
background : # 2563 eb ! important ;
color : # ffffff ! important ;
}
}
& . complete : not ( . active ) {
border - color : # e5e7eb ! important ;
color : # 374151 ! important ;
. step - no {
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 - no {
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 - no {
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 ( . feasibility - page ) . step - indicator {
justify - content : flex - start ! important ;
overflow - x : auto ! important ;
}
# app . studentLayout section . app - main : has ( . feasibility - page ) . step - indicator . step - tab {
flex : 0 0 140 px ! important ;
max - width : none ! important ;
}
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 顶部工具栏 : TrainingMaterialButton 资料按钮 → 教育蓝
* === === === === === === === === === === === === === === === === === === === === * /
. feasibility - page . workbench - actions . 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 .action -button > )
* === === === === === === === === === === === === === === === === === === === === * /
# app . studentLayout section . app - main : has ( . feasibility - page ) . workbench - actions > button . action - button {
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 ( . feasibility - page ) . workbench - actions > button . action - button : 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 >