@ -96,10 +96,9 @@
< 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 >
< div class = "step-actions" > < button class = "action-button primary" type = "button" : disabled = "saving || uploading" @click ="saveStepThree" > {{ saving ? ' 校 验 并 提交 中 … ' : ' 保 存 并 提 交 任 务 ' }} < / 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 " / >
@ -116,7 +115,7 @@ import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue" ;
const TASK _KEY = "business-feasibility" ;
const DEFAULT _STEPS = [ "商业可行性论证" , "项目初步决策" , "可行性论证报告" , "决策结论" ];
const DEFAULT _STEPS = [ "商业可行性论证" , "项目初步决策" , "可行性论证报告" ];
const overviewFields = [
{ key : "projectBackground" , label : "项目背景" , placeholder : "示例:根据案例信息简述公司基本情况,再简要阐述智能家居、个护健康、美妆工具等相关市场发展情况和需求情况。" } ,
{ key : "productPositioning" , label : "产品定位" , placeholder : "示例:阐述项目产品针对的目标市场用户、核心卖点、使用场景、差异化理由以及用户收益。" } ,
@ -153,7 +152,8 @@ const priorityOptions = [
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 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 ) . slice ( 0 , 3 ) ) ;
const finalStep = computed ( ( ) => Math . max ( 1 , trainingSteps . value . length ) ) ;
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 ) ;
@ -167,8 +167,9 @@ function numeric(value) { const number = Number(value); return Number.isFinite(n
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 clampStep ( value ) { return Math . min ( Math . max ( 1 , Number ( value ) || 1 ) , finalStep . value ) ; }
async function loadTaskConfig ( ) { try { taskConfig . value = ( await getTrainingTaskByKey ( TASK _KEY ) ) ? . data || null ; currentStep . value = clampStep ( currentStep . value ) ; } 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 = clampStep ( 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 ; } }
@ -183,8 +184,8 @@ function readableSize(size) { const value = Number(size || 0); return value > 10
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 && ! stepTwoC ompleted. value ) { proxy ? . $modal ? . msgWarning ( "请先完成并保存第 2 步") ; return ; } if ( step > 3 && ! stepThree Completed. value ) { proxy ? . $modal ? . msgWarning ( "请先 上传并保存第 3 步报告 ") ; return ; } currentStep . value = s tep; }
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 : finalStep . value , submitted : true , saveAction : "SUBMIT " } ) ; stepThreeCompleted . value = true ; currentStep . value = finalStep . value ; proxy ? . $modal ? . msgSuccess ( "报告已保存, 任务已提交 ") ; } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请先上传可行性论证报告" ) ; } finally { saving . value = false ; } }
function switchStep ( step ) { const targetStep = clampStep ( step ) ; if ( targetStep > 1 && ! c ompleted. value ) { proxy ? . $modal ? . msgWarning ( "请先完成并保存第 1 步") ; return ; } if ( targetStep > 2 && ! stepTwo Completed. value ) { proxy ? . $modal ? . msgWarning ( "请先 完成并保存第 2 步 ") ; return ; } currentStep . value = targetS tep; }
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 >