@ -12,40 +12,34 @@
< / button >
< span v-if ="briefOpen" class="task-brief-more" >
< b > 实训目标 < / b >
< ul > < li v-for ="goal in taskGoals" :key ="goal" > {{ goal }} < / li > < / ul >
< 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"
< 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 >
: 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"
< 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)"
>
: 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 >
@ -53,127 +47,367 @@
< / 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 >
< 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 = "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 >
< 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 = "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 >
< 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 >
< 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 >
< 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 >
< 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 >
< 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 >
< div class = "section-title" > < span > 07 < / span >
< div >
< h3 > 风险识别与评估 < / h3 >
< p > 识别项目主要风险 , 并明确影响程度及可执行的应对措施 。 < / p >
< / div >
< / div >
< div class = "table-wrap" >
< table >
< thead >
< tr >
< th > 风险类型 < / th >
< th > 具体风险描述 < / th >
< th > 影响程度 < / th >
< th > 应对措施 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="risk in form.risks" :key="risk.type" >
< td class = "fixed-cell" > { { risk . type } } < / td >
< td > < el -input v -model = " risk.description " type = "textarea"
: autosize = "{ minRows: 2, maxRows: 4 }" placeholder = "填写具体风险" / > < / td >
< td > < el -select v -model = " risk.impact " placeholder = "请选择" size = "default"
class = "risk-impact-select" > < el -option label = "高" value = "高" / > < el -option
label = "中" value = "中" / > < el -option label = "低" value = "低" / > < / e l - s e l e c t >
< / td >
< td > < el -input v -model = " risk.response " type = "textarea"
: autosize = "{ minRows: 2, maxRows: 4 }" placeholder = "填写应对措施" / > < / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
< div class = "step-actions" > < button class = "action-button primary" type = "button" :disabled ="saving" @click ="validateAndContinue" > {{ saving ? ' 校 验 并 保 存 中 … ' : ' 校 验 并 进 入 第 2 步 ' }} < / button > < button class = "action-button" type = "button" :disabled ="saving" @click ="resetForm" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< 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 >
< 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" >
< 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 >
< 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" >
< 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 >
< 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 >
< 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 >
< input ref = "reportInput" class = "visually-hidden" type = "file"
accept = ".pdf,.doc,.docx,.png,.jpg,.jpeg" @ change = "handleReportUpload" / >
< template v-if ="!report.url" > < el -icon class = "upload-orbit" >
< Document / >
< / e l - i c o n >
< h3 > 上传可行性论证报告 < / h3 >
< p > 支持 PDF 、 Word 、 PNG 、 JPG , 文件不超过 20 MB 。 < / p > < button class = "action-button primary" type = "button"
: disabled = "uploading" @ click = "openReportPicker" > < el -icon >
< UploadFilled / >
< / e l - i c o n > { { u p l o a d i n g ? ' 上 传 中 … ' : ' 选 择 报 告 附 件 ' } } < / b u t t o n >
< / template >
< template v-else >
< div class = "report-file-card" >
< div class = "report-file-icon" > < el -icon >
< Document / >
< / e l - i c o n > < / d i v >
< div class = "report-file-meta" > < strong > { { report . name || '可行性论证报告' } } < / strong > < span > { {
reportKindLabel } } · { { readableSize ( report . size ) } } < / span > < / div >
< div class = "report-file-actions" > < button type = "button"
@ click = "openReportPreview" > < el -icon >
< View / >
< / e l - i c o n > 预 览 < / b u t t o n > < b u t t o n t y p e = " b u t t o n " @ c l i c k = " d o w n l o a d R e p o r t " > < e l - i c o n >
< Download / >
< / e l - i c o n > 下 载 < / b u t t o n > < b u t t o n t y p e = " b u t t o n " c l a s s = " d a n g e r "
@ click = "clearReport" > < el -icon >
< Delete / >
< / e l - i c o n > 移 除 < / b u t t o n > < / d i v >
< / div >
< div class = "report-replace" > < button class = "text-action" type = "button" :disabled ="uploading"
@ click = "openReportPicker" > < el -icon >
< UploadFilled / >
< / e l - i c o n > { { u p l o a d i n g ? ' 上 传 中 … ' : ' 更 换 附 件 ' } } < / b u t t o n > < s p a n > 附 件 已 上 传 , 可 随 时 预 览 或 下 载 核 对 。 < / s p a n >
< / div >
< / template >
< / section >
< div class = "step-actions" > < button class = "action-button primary" type = "button" : disabled = "saving || uploading" @click ="saveStepThree" > {{ saving ? ' 校 验 并 保 存 中 … ' : ' 保 存 并 进 入 第 4 步 ' }} < / 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 >
< 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 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 " / >
< 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 = [ "商业可行性论证" , "项目初步决策" , "可行性论证报告" , "决策结论" ] ;
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 : "相关教程课程合作" } ,
@ -181,44 +415,44 @@ const priorityOptions = [
{ 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 ) ; }
]
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 >
@ -507,7 +741,9 @@ function exportOutcome() { const blob = new Blob([JSON.stringify({ task: taskTit
border - bottom : 1 px solid var ( -- student - panel - border ) ;
}
. form - table label : last - child { border - bottom : 0 ; }
. form - table label : last - child {
border - bottom : 0 ;
}
. form - table label > span {
display : flex ;
@ -520,17 +756,21 @@ function exportOutcome() { const blob = new Blob([JSON.stringify({ task: taskTit
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 { 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 ; }
. 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 ;
@ -567,14 +807,21 @@ th {
text - align : center ;
}
th , td {
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 ; }
tr : last - child td {
border - bottom : 0 ;
}
th : last - child ,
td : last - child {
border - right : 0 ;
}
/* Element Plus 组件在表格中的紧凑样式 */
. table - wrap td : deep ( . el - textarea ) ,
@ -821,8 +1068,13 @@ th:last-child, td:last-child { border-right: 0; }
background : var ( -- student - accent ) ;
}
. priority - section { transition : opacity 0.16 s ease ; }
. priority - section . muted { opacity : 0.45 ; }
. priority - section {
transition : opacity 0.16 s ease ;
}
. priority - section . muted {
opacity : 0.45 ;
}
. priority - heading {
display : flex ;
@ -862,9 +1114,19 @@ th:last-child, td:last-child { border-right: 0; }
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 - 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 ;
@ -929,9 +1191,13 @@ th:last-child, td:last-child { border-right: 0; }
font - size : 22 px ;
}
. report - file - meta { min - width : 0 ; flex : 1 ; }
. report - file - meta {
min - width : 0 ;
flex : 1 ;
}
. report - file - meta strong , . report - file - meta span {
. report - file - meta strong ,
. report - file - meta span {
display : block ;
overflow : hidden ;
text - overflow : ellipsis ;
@ -950,9 +1216,14 @@ th:last-child, td:last-child { border-right: 0; }
font - size : 11 px ;
}
. report - file - actions { display : flex ; gap : 6 px ; flex - shrink : 0 ; }
. report - file - actions {
display : flex ;
gap : 6 px ;
flex - shrink : 0 ;
}
. report - file - actions button , . text - action {
. report - file - actions button ,
. text - action {
display : inline - flex ;
align - items : center ;
gap : 4 px ;
@ -973,7 +1244,10 @@ th:last-child, td:last-child { border-right: 0; }
& . danger {
color : var ( -- student - danger ) ;
& : hover { background : rgba ( 217 , 85 , 76 , 0.08 ) ; }
& : hover {
background : rgba ( 217 , 85 , 76 , 0.08 ) ;
}
}
}
@ -985,7 +1259,10 @@ th:last-child, td:last-child { border-right: 0; }
font - size : 11 px ;
}
. text - action : disabled { cursor : not - allowed ; opacity : 0.5 ; }
. text - action : disabled {
cursor : not - allowed ;
opacity : 0.5 ;
}
. visually - hidden {
position : absolute ;
@ -1022,26 +1299,71 @@ th:last-child, td:last-child { border-right: 0; }
/* ===== 响应式 ===== */
@ 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 ; }
. 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 ; }
. 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 ; }
. 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 >
@ -1088,7 +1410,9 @@ th:last-child, td:last-child { border-right: 0; }
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 - bar {
display : none ! important ;
}
. step - no {
position : static ! important ;
@ -1109,30 +1433,43 @@ th:last-child, td:last-child { border-right: 0; }
letter - spacing : 0 ! important ;
transition : background - color 0.16 s ease , color 0.16 s ease ! important ;
& : : after { display : none ! important ; }
& : : after {
display : none ! important ;
}
}
. step - name { color : inherit ! 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 ; }
. 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 ; }
. 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 ; }
. step - no {
background : # 4 a9d6e ! important ;
color : # ffffff ! important ;
}
}
}
@ -1142,7 +1479,10 @@ th:last-child, td:last-child { border-right: 0; }
color : # ffffff ! important ;
font - weight : 700 ! important ;
. step - no { background : # ffffff ! important ; color : # 1 f2937 ! important ; }
. step - no {
background : # ffffff ! important ;
color : # 1 f2937 ! important ;
}
& : hover {
background : # 111827 ! important ;