< template >
< div class = "feasibility-page student-training-shell" >
< main class = "feasibility-core" >
< header class = "task-header" >
< span > DEMAND INSIGHT · BUSINESS CASE < / span >
< h1 > { { taskTitle } } < / h1 >
< / header >
< TrainingTaskBrief :background ="taskBackground" :goals ="taskGoals" :requirements ="taskRequirement" :show-material ="false" / >
< section class = "workbench" >
< div class = "workbench-actions" >
< TrainingMaterialButton :material-name ="taskConfig?.materialName" :material-url ="taskConfig?.materialUrl" / >
< button class = "action-button" type = "button" @click ="exportOutcome" > < el -icon > < Download / > < / e l - i c o n > 导 出 实 训 成 果 < / b u t t o n >
< / div >
< nav class = "step-indicator" aria -label = " 商 业 可 行 性 论 证 步 骤 " >
< template v-for ="(step, index) in trainingSteps" :key ="step" >
< button type = "button" : class = "['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]" @ click = "switchStep(index + 1)" >
< b > { { String ( index + 1 ) . padStart ( 2 , '0' ) } } < / b > < span > < strong > { { step } } < / strong > < small > { { currentStep === index + 1 ? '当前步骤' : '点击切换' } } → < / small > < / span >
< / button > < i v -if = " index < trainingSteps.length - 1 " / >
< / template >
< / nav >
< section v-if ="currentStep === 1" class="step-card" >
< div class = "step-heading" > < div > < p > 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 > < textarea v -model .trim = " form.projectOverview [ field.key ] " :placeholder ="field.placeholder" rows = "3" / > < / 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"><input v-model="form.marketAnalysis[field.key]" :type="field.type || 'text'" :min="field.type === 'number' ? 0 : undefined" :step="field.type === 'number' ? '0.01' : undefined" :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 > < textarea v -model .trim = " user.userGroup " :placeholder ="targetUserExamples[index].userGroup" / > < / td > < td > < textarea v -model .trim = " user.scenario " :placeholder ="targetUserExamples[index].scenario" / > < / td > < td > < textarea v -model .trim = " user.coreNeed " :placeholder ="targetUserExamples[index].coreNeed" / > < / td > < td > < input v -model .trim = " 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 > < textarea v -model .trim = " model.strategy " placeholder = "填写渠道、产品组合或服务策略" / > < / td > < td > < textarea v -model .trim = " model.explanation " 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><input v-model="cost.amount" type="number" min="0" step="0.01" placeholder="填写金额" /></td></tr><tr class="total-row" > < td > 单位总成本 < / td > < td > < input :value ="currency(totalCost)" readonly aria -label = " 单 位 总 成 本 " / > < / 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 > < input v-model ="form.targetPrice" type="number" min="0" step="0.01" placeholder="填写目标售价" /></td></tr><tr class="total-row"><td>单位毛利(元/台)</td><td><input :value="currency(unitGrossProfit)" readonly aria-label="单位毛利" /></td></tr><tr class="total-row" > < td > 毛利率 ( % ) < / td > < td > < input :value ="percent(grossMargin)" readonly aria -label = " 毛 利 率 " / > < / 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><textarea v-model.trim="risk.description" placeholder="填写具体风险" /></td><td><select v-model="risk.impact"><option value="">请选择</option><option value="高">高</option><option value="中">中</option><option value="低" > 低 < / option > < / select > < / td > < td > < textarea v -model .trim = " risk.response " 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 > Step 02 < / p > < h2 > 项目可行性初步决策 < / h2 > < / div > < em > 立项判断 < / em > < / div >
< p class = "intro" > 综合以上所有分析 , 初步判断项目的可行性 , 并明确如果立项后最需要优先解决的关键问题 。 < / p >
< section class = "decision-section" >
< h3 > < span > 01 < / span > 该项目商业可行性总体评估为 : < / h3 >
< div class = "decision-grid" role = "radiogroup" aria -label = " 商 业 可 行 性 总 体 评 估 " >
< label v-for ="option in feasibilityOptions" :key="option.value" :class="['decision-option', { selected: stepTwo.decision === option.value }]"><input v-model="stepTwo.decision" type="radio" :value="option.value" name="business-feasibility-decision" @change="handleDecisionChange" /><span class="radio-mark" > < / span > < strong > { { option . label } } < / strong > < small > { { option . hint } } < / small > < / label >
< / div >
< / 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 >
< div class = "priority-grid" > < label v-for ="item in priorityOptions" :key="item.value" :class="['priority-option', { selected: stepTwo.priorities.includes(item.value) }]"><input v-model="stepTwo.priorities" type="checkbox" :value="item.value" :disabled="stepTwo.decision === 'not-feasible' || (!stepTwo.priorities.includes(item.value) && selectedPriorityCount >= 3)" /><span class="checkbox-mark"><el-icon v-if="stepTwo.priorities.includes(item.value)" > < Check / > < / e l - i c o n > < / s p a n > < s p a n > < s t r o n g > { { i t e m . l a b e l } } < / s t r o n g > < s m a l l > { { i t e m . d e t a i l } } < / s m a l l > < / s p a n > < / l a b e l > < / d i v >
< / 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 > 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" ;
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.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 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 { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) 310 px ; gap : 20 px ; min - height : 100 % ; padding : 22 px ; color : # 2 c4a6a ; background : radial - gradient ( circle at 22 % 2 % , # e3f2fd , transparent 34 % ) , # ffffff ; } . feasibility - core { min - width : 0 ; } . task - header { position : relative ; overflow : hidden ; padding : 28 px 32 px ; border : 1 px solid # cfe2f5 ; border - radius : 20 px ; background : linear - gradient ( 108 deg , # f0f6fd , # f0f6fd ) ; box - shadow : 0 20 px 46 px rgba ( 30 , 136 , 229 , 0.10 ) ; } . task - header : : after { position : absolute ; top : - 50 px ; right : 8 % ; width : 180 px ; height : 180 px ; border : 1 px solid # cfe2f5 ; border - radius : 50 % ; content : "" ; } . task - header span { color : # 42 a5f5 ; font - size : var ( -- student - fs - caption ) ; font - weight : 800 ; letter - spacing : .16 em ; } . task - header h1 { margin : 8 px 0 0 ; color : # 1 e3a5f ; font - size : 32 px ; letter - spacing : .03 em ; } . workbench { margin - top : 20 px ; padding : 24 px ; border : 1 px solid # cfe2f5 ; border - radius : 20 px ; background : # f0f6fd ; } . workbench - actions { display : flex ; justify - content : flex - end ; gap : 12 px ; } . action - button { display : inline - flex ; align - items : center ; justify - content : center ; gap : 8 px ; min - height : 40 px ; padding : 0 17 px ; border : 1 px solid # cfe2f5 ; border - radius : 10 px ; color : # 1 e3a5f ; background : # e3f2fd ; font : inherit ; font - weight : 800 ; cursor : pointer ; transition : .2 s ; } . action - button : hover : not ( : disabled ) { transform : translateY ( - 1 px ) ; border - color : # 1 e88e5 ; background : # bbdefb ; } . action - button . primary { border - color : # 1 e88e5 ; background : linear - gradient ( 105 deg , # 1 e88e5 , # 1565 c0 ) ; } . action - button : disabled { cursor : not - allowed ; opacity : .62 ; } . step - indicator { display : flex ; align - items : stretch ; margin : 26 px 0 ; padding : 10 px ; border : 1 px solid rgba ( 69 , 201 , 248 , 0.2 ) ; border - radius : 15 px ; background : # f0f6fd ; } . step - tab { display : flex ; flex : 1 ; align - items : center ; gap : 11 px ; min - width : 0 ; padding : 11 px 12 px ; border : 0 ; border - radius : 11 px ; color : # 5 a7a9a ; background : transparent ; text - align : left ; cursor : pointer ; } . step - tab b { display : grid ; flex : 0 0 35 px ; width : 35 px ; height : 35 px ; place - items : center ; border : 1 px solid currentColor ; border - radius : 50 % ; } . step - tab span { min - width : 0 ; } . step - tab strong , . step - tab small { display : block ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; } . step - tab strong { color : inherit ; font - size : var ( -- student - fs - body ) ; } . step - tab small { margin - top : 4 px ; color : # 5 a7a9a ; font - size : var ( -- student - fs - caption ) ; } . step - tab . active { color : # 1 e3a5f ; background : linear - gradient ( 90 deg , # bbdefb , # bbdefb ) ; box - shadow : 0 7 px 18 px rgba ( 30 , 136 , 229 , 0.15 ) ; } . step - tab . active b { border - color : # 42 a5f5 ; background : # e3f2fd ; } . step - tab . complete { color : # 42 a5f5 ; } . step - indicator i { width : 1 px ; margin : 10 px 3 px ; background : # cfe2f5 ; } . step - card { padding : 24 px ; border : 1 px solid # cfe2f5 ; border - radius : 16 px ; background : linear - gradient ( 145 deg , # e3f2fd , # f0f6fd ) ; } . step - heading { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 14 px ; } . step - heading p { margin : 0 0 4 px ; color : # 42 a5f5 ; font - size : var ( -- student - fs - body - sm ) ; font - weight : 800 ; letter - spacing : .1 em ; } . step - heading h2 { margin : 0 ; color : # 1 e3a5f ; font - size : var ( -- student - fs - h1 ) ; } . step - heading em { padding : 7 px 11 px ; border : 1 px solid # cfe2f5 ; border - radius : 999 px ; color : # 42 a5f5 ; font - size : var ( -- student - fs - caption ) ; font - style : normal ; } . intro { margin : 15 px 0 0 ; color : # 5 a7a9a ; line - height : 1.8 ; } . form - section { margin - top : 34 px ; } . section - title { display : flex ; align - items : center ; gap : 12 px ; margin - bottom : 14 px ; } . section - title > span { display : grid ; width : 31 px ; height : 31 px ; place - items : center ; border : 1 px solid # cfe2f5 ; border - radius : 9 px ; color : # 42 a5f5 ; background : # e3f2fd ; font - weight : 900 ; } . section - title h3 { margin : 0 ; color : # 1 e3a5f ; font - size : var ( -- student - fs - h1 ) ; } . section - title p { margin : 3 px 0 0 ; color : # 5 a7a9a ; font - size : var ( -- student - fs - body - sm ) ; } . form - table { display : grid ; border : 1 px solid # cfe2f5 ; border - radius : 12 px ; overflow : hidden ; background : # f0f6fd ; } . form - table label { display : grid ; grid - template - columns : 190 px 1 fr ; border - bottom : 1 px solid # cfe2f5 ; } . form - table label : last - child { border - bottom : 0 ; } . form - table label > span , . table - wrap th { display : flex ; align - items : center ; justify - c
. decision - section { margin - top : 34 px ; padding : 22 px ; border : 1 px solid # cfe2f5 ; border - radius : 14 px ; background : linear - gradient ( 135 deg , # f0f6fd , # f0f6fd ) ; } . decision - section h3 { display : flex ; align - items : center ; gap : 10 px ; margin : 0 ; color : # 1 e3a5f ; font - size : var ( -- student - fs - h1 ) ; } . decision - section h3 > span { display : grid ; width : 28 px ; height : 28 px ; place - items : center ; border : 1 px solid # cfe2f5 ; border - radius : 8 px ; color : # 42 a5f5 ; font - size : var ( -- student - fs - body - sm ) ; } . decision - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 12 px ; margin - top : 20 px ; } . decision - option { position : relative ; display : grid ; grid - template - columns : 20 px 1 fr ; column - gap : 10 px ; align - items : center ; min - height : 76 px ; padding : 15 px ; border : 1 px solid # cfe2f5 ; border - radius : 11 px ; background : # f0f6fd ; cursor : pointer ; transition : .2 s ; } . decision - option : hover { border - color : # cfe2f5 ; transform : translateY ( - 1 px ) ; } . decision - option input , . priority - option input { position : absolute ; opacity : 0 ; pointer - events : none ; } . radio - mark , . checkbox - mark { display : grid ; width : 18 px ; height : 18 px ; place - items : center ; border : 1 px solid # cfe2f5 ; border - radius : 50 % ; color : # 1 e3a5f ; background : transparent ; } . decision - option . selected { border - color : # 1 e88e5 ; background : linear - gradient ( 115 deg , # bbdefb , # f0f6fd ) ; box - shadow : inset 0 0 0 1 px # cfe2f5 ; } . decision - option . selected . radio - mark { border : 5 px solid # 1 e88e5 ; background : # e3f2fd ; } . decision - option strong { color : # 1 e3a5f ; font - size : var ( -- student - fs - body ) ; } . decision - option small { grid - column : 2 ; margin - top : 4 px ; color : # 5 a7a9a ; font - size : var ( -- student - fs - caption ) ; } . priority - section { transition : .2 s ; } . priority - section . muted { opacity : .56 ; } . priority - heading { display : flex ; align - items : center ; justify - content : space - between ; gap : 14 px ; } . priority - heading b { min - width : 47 px ; padding : 6 px 9 px ; border : 1 px solid # cfe2f5 ; border - radius : 999 px ; color : # 42 a5f5 ; background : # e3f2fd ; text - align : center ; } . hint { margin : 12 px 0 0 38 px ; color : # 5 a7a9a ; font - size : var ( -- student - fs - body - sm ) ; } . priority - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 12 px ; margin - top : 18 px ; } . priority - option { display : grid ; grid - template - columns : 20 px 1 fr ; gap : 11 px ; align - items : start ; min - height : 66 px ; padding : 14 px ; border : 1 px solid # cfe2f5 ; border - radius : 10 px ; background : # f0f6fd ; cursor : pointer ; transition : .2 s ; } . priority - option : hover { border - color : rgba ( 100 , 223 , 253 , 0.75 ) ; } . priority - option . selected { border - color : # 1 e88e5 ; background : # e3f2fd ; } . priority - option : has ( input : disabled ) : not ( . selected ) { cursor : not - allowed ; opacity : .52 ; } . checkbox - mark { margin - top : 2 px ; border - radius : 5 px ; } . priority - option . selected . checkbox - mark { border - color : # 42 a5f5 ; color : # 1 e3a5f ; background : # 42 a5f5 ; } . priority - option strong , . priority - option small { display : block ; } . priority - option strong { color : # 1 e3a5f ; font - size : var ( -- student - fs - body ) ; } . priority - option small { margin - top : 4 px ; color : # 5 a7a9a ; font - size : var ( -- student - fs - caption ) ; }
. report - guide { margin - top : 24 px ; padding : 18 px 20 px ; border - left : 3 px solid # 42 a5f5 ; color : # 2 c4a6a ; background : # e3f2fd ; line - height : 1.85 ; } . report - guide p , . report - guide ol { margin : 0 ; } . report - guide ol { margin - top : 9 px ; padding - left : 23 px ; } . report - guide li + li { margin - top : 6 px ; } . report - upload - panel { display : grid ; min - height : 300 px ; place - content : center ; justify - items : center ; gap : 12 px ; margin - top : 30 px ; padding : 36 px ; border : 1 px dashed rgba ( 83 , 218 , 255 , 0.72 ) ; border - radius : 16 px ; background : radial - gradient ( circle at 50 % 22 % , # e3f2fd , transparent 42 % ) , # f0f6fd ; text - align : center ; } . upload - orbit { display : grid ; width : 68 px ; height : 68 px ; place - items : center ; border : 1 px solid # cfe2f5 ; border - radius : 22 px ; color : # 42 a5f5 ; background : # e3f2fd ; font - size : 32 px ; box - shadow : 0 0 0 9 px rgba ( 45 , 193 , 233 , 0.06 ) ; } . report - upload - panel h3 { margin : 8 px 0 0 ; color : # 1 e3a5f ; font - size : 21 px ; } . report - upload - panel > p { margin : 0 ; color : # 5 a7a9a ; } . report - file - card { display : flex ; align - items : center ; width : min ( 760 px , 100 % ) ; gap : 15 px ; padding : 17 px ; border : 1 px solid rgba ( 93 , 217 , 252 , 0.45 ) ; border - radius : 13 px ; background : # f0f6fd ; text - align : left ; } . report - file - icon { display : grid ; flex : 0 0 48 px ; width : 48 px ; height : 48 px ; place - items : center ; border - radius : 12 px ; color : # 1565 c0 ; background : # e3f2fd ; font - size : 25 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 : # 1 e3a5f ; } . report - file - meta span { margin - top : 5 px ; color : # 5 a7a9a ; font - size : var ( -- student - fs - caption ) ; } . report - file - actions { display : flex ; gap : 8 px ; } . report - file - actions button , . text - action { display : inline - flex ; align - items : center ; gap : 5 px ; border : 0 ; color : # 1565 c0 ; background : transparent ; font : inherit ; font - size : var ( -- student - fs - body - sm ) ; cursor : pointer ; } . report - file - actions . danger { color : # ffaaa1 ; } . report - replace { display : flex ; align - items : center ; gap : 13 px ; color : # 5 a7a9a ; font - size : var ( -- student - fs - caption ) ; } . text - action : disabled { cursor : not - allowed ; opacity : .6 ; } . 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 : # 1 e3a5f ; }
@ media ( max - width : 1280 px ) { . feasibility - page { grid - template - columns : 1 fr ; } . step - indicator { overflow - x : auto ; } . step - tab { min - width : 170 px ; } . market - table { grid - template - columns : 1 fr ; } . market - table label { border - right : 0 ; } . cost - layout { grid - template - columns : 1 fr ; } }
@ media ( max - width : 720 px ) { . feasibility - page { padding : 10 px ; } . task - header , . workbench , . step - card { padding : 16 px ; } . task - header h1 { font - size : 25 px ; } . workbench - actions { justify - content : flex - start ; flex - wrap : wrap ; } . form - table label { grid - template - columns : 1 fr ; } . form - table label > span { justify - content : flex - start ; } . step - heading em { display : none ; } . step - actions { flex - wrap : wrap ; } . decision - grid , . priority - grid { grid - template - columns : 1 fr ; } . decision - section { padding : 16 px ; } . hint { margin - left : 0 ; } . report - upload - panel { padding : 22 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 ; } }
. table - wrap th { display : table - cell ; text - align : center ; }
< / style >