< template >
< div class = "feasibility-page student-training-shell" >
< main class = "feasibility-core" >
< section class = "task-header" >
< div class = "task-head-main" >
< span class = "task-badge" > DEMAND INSIGHT · BUSINESS CASE < / span >
< 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 >
< div class = "workbench-actions" >
< TrainingMaterialButton
: material - name = "taskConfig?.materialName"
: material - url = "taskConfig?.materialUrl"
: training - data - name = "taskConfig?.trainingDataName"
: training - data - url = "taskConfig?.trainingDataUrl"
: guide - manual - name = "taskConfig?.guideManualName"
: guide - manual - url = "taskConfig?.guideManualUrl"
: ai - manual - name = "taskConfig?.aiManualName"
: ai - manual - url = "taskConfig?.aiManualUrl"
/ >
< button class = "action-button" type = "button" @click ="exportOutcome" > < el -icon > < Download / > < / e l - i c o n > 导 出 实 训 成 果 < / b u t t o n >
< / div >
< / 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 > < / t d > < t d > < e l - i n p u t v - m o d e l = " r i s k . r e s p o n s e " t y p e = " t e x t a r e a " : a u t o s i z e = " { m i n R o w s : 2 , m a x R o w s : 4 } " p l a c e h o l d e r = " 填 写 应 对 措 施 " / > < / t d > < / t r > < / t b o d y > < / t a b l e > < / d i v >
< / 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 步 ' }} < / button > < button 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 > < h 3 > 上 传 可 行 性 论 证 报 告 < / h 3 > < p > 支 持 P D F 、 W o r d 、 P N G 、 J P G , 文 件 不 超 过 2 0 M B 。 < / p > < b u t t o n c l a s s = " a c t i o n - b u t t o n p r i m a r y " t y p e = " b u t t o n " : d i s a b l e d = " u p l o a d i n g " @ c l i c k = " o p e n R e p o r t P i c k e r " > < e l - i c o n > < U p l o a d F i l l e d / > < / e l - i c o n > { { u p l o a d i n g ? ' 上 传 中 … ' : ' 选 择 报 告 附 件 ' } } < / b u t t o n > < / t e m p l a t e >
< 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 > < d i v c l a s s = " r e p o r t - f i l e - m e t a " > < s t r o n g > { { r e p o r t . n a m e | | ' 可 行 性 论 证 报 告 ' } } < / s t r o n g > < s p a n > { { r e p o r t K i n d L a b e l } } · { { r e a d a b l e S i z e ( r e p o r t . s i z e ) } } < / s p a n > < / d i v > < d i v c l a s s = " r e p o r t - f i l e - a c t i o n s " > < b u t t o n t y p e = " b u t t o n " @ c l i c k = " o p e n R e p o r t P r e v i e w " > < e l - i c o n > < V i e w / > < / 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 > < D o w n l o a d / > < / 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 " @ c l i c k = " c l e a r R e p o r t " > < e l - i c o n > < D e l e t e / > < / e l - i c o n > 移 除 < / b u t t o n > < / d i v > < / d i v > < d i v c l a s s = " r e p o r t - r e p l a c e " > < b u t t o n c l a s s = " t e x t - a c t i o n " t y p e = " b u t t o n " : d i s a b l e d = " u p l o a d i n g " @ c l i c k = " o p e n R e p o r t P i c k e r " > < e l - i c o n > < U p l o a d F i l l e d / > < / 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 > < / d i v > < / t e m p l a t e >
< / section >
< div class = "step-actions" > < button class = "action-button primary" type = "button" : disabled = "saving || uploading" @click ="saveStepThree" > {{ saving ? ' 校 验 并 保 存 中 … ' : ' 保 存 并 进 入 第 4 步 ' }} < / button > < / div >
< 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 { Check , 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 TrainingMaterialButton from "@/views/components/TrainingMaterialButton.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 : # 1 f2937 ;
-- student - accent - deep : # 111827 ;
-- student - accent - light : # 4 b5563 ;
-- student - accent - soft : # f3f4f6 ;
-- student - text : # 1 f2937 ;
-- student - text - secondary : # 6 b7280 ;
-- student - panel - border : # e5e7eb ;
-- student - panel - bg : # fafbfc ;
-- student - card - bg : # ffffff ;
-- student - danger : # d9554c ;
-- student - success : # 4 a9d6e ;
min - width : 0 ;
color : var ( -- student - text ) ;
background : # ffffff ;
line - height : 1.55 ;
}
. feasibility - core {
min - width : 0 ;
display : grid ;
gap : 10 px ;
}
/* task-header 独立卡片 */
. task - header {
display : flex ;
align - items : flex - start ;
justify - content : space - between ;
gap : 12 px ;
margin : 0 ;
padding : 16 px ;
border : 1 px solid # e7e9ed ;
border - radius : 16 px ;
background : # ffffff ;
}
. task - head - main {
display : grid ;
justify - items : start ;
gap : 8 px ;
min - width : 0 ;
}
. task - badge {
display : inline - flex ;
align - items : center ;
padding : 4 px 12 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 999 px ;
color : var ( -- student - accent - deep ) ;
background : var ( -- student - accent - soft ) ;
font - size : 12 px ;
font - weight : 700 ;
letter - spacing : 0 ;
}
. 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 # d1d5db ;
border - radius : 8 px ;
color : # 374151 ;
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 : # f9fafb ;
box - shadow : 0 2 px 6 px rgba ( 17 , 24 , 39 , 0.1 ) ;
}
& . primary {
border - color : var ( -- student - accent ) ;
color : # ffffff ;
background : var ( -- student - accent ) ;
font - weight : 700 ;
box - shadow : 0 2 px 8 px rgba ( 17 , 24 , 39 , 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 ( 17 , 24 , 39 , 0.3 ) ;
}
}
& : 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 ;
}
. form - table label : deep ( . el - textarea ) ,
. form - table label : deep ( . el - input - number ) ,
. form - table label : deep ( . el - input ) {
margin : 8 px 10 px ;
max - width : calc ( 100 % - 20 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 ( 31 , 41 , 55 , 0.08 ) ! 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" >
/* ===== 步骤导航: 按钮切换风( foundation 同款) ===== */
# 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 : 10 px ! important ;
width : 100 % ! important ;
margin : 4 px 0 20 px ! important ;
padding : 0 ! important ;
border : none ! important ;
border - radius : 0 ! important ;
background : transparent ! important ;
box - shadow : none ! important ;
. step - tab {
flex : 0 0 auto ! important ;
min - width : auto ! important ;
max - width : none ! 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 : 42 px ! important ;
margin : 0 ! important ;
padding : 8 px 14 px ! important ;
border : 1 px solid # e5e7eb ! important ;
border - radius : 8 px ! 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 : nowrap ! important ;
text - align : center ! important ;
cursor : pointer ;
transition : background - color 0.16 s ease , color 0.16 s ease , border - color 0.16 s ease ! 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 : ui - monospace , "SF Mono" , Consolas , monospace ! important ;
font - size : 12 px ! important ;
font - weight : 700 ! important ;
line - height : 1 ! important ;
letter - spacing : 0 ! important ;
transition : background - color 0.16 s ease , color 0.16 s ease ! important ;
& : : after { display : none ! important ; }
}
. step - name { color : inherit ! important ; }
& : hover : not ( . active ) : not ( . complete ) {
border - color : # 1 f2937 ! important ;
background : # f9fafb ! important ;
color : # 1 f2937 ! important ;
. step - no { background : # 1 f2937 ! important ; color : # ffffff ! important ; }
}
& . complete : not ( . active ) {
border - color : # dce9e2 ! important ;
color : # 374151 ! important ;
. step - no { background : rgba ( 74 , 157 , 110 , 0.12 ) ! important ; color : # 3 f8a5f ! important ; }
& : hover {
border - color : # 4 a9d6e ! important ;
background : # f9fafb ! important ;
. step - no { background : # 4 a9d6e ! important ; color : # ffffff ! important ; }
}
}
& . active {
border - color : # 1 f2937 ! important ;
background : # 1 f2937 ! important ;
color : # ffffff ! important ;
font - weight : 700 ! important ;
. step - no { background : # ffffff ! important ; color : # 1 f2937 ! important ; }
& : hover {
background : # 111827 ! important ;
border - color : # 111827 ! important ;
}
}
}
}
/* 窄屏:横向滑动 */
@ media ( max - width : 900 px ) {
# app . studentLayout section . app - main : has ( . feasibility - page ) . step - indicator {
overflow - x : auto ! important ;
flex - wrap : nowrap ! important ;
justify - content : flex - start ! important ;
}
# app . studentLayout section . app - main : has ( . feasibility - page ) . step - indicator . step - tab {
flex : 0 0 auto ! important ;
max - width : none ! important ;
}
}
< / style >