You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/demand/business-feasibility.vue

196 lines
43 KiB
Vue

<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 /></el-icon>导出实训成果</button>
</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 /></el-icon>清空填写</button></div>
</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 /></el-icon></span><span><strong>{{ item.label }}</strong><small>{{ item.detail }}</small></span></label></div>
</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 /></el-icon>清空填写</button></div>
</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 /></el-icon><h3>上传可行性论证报告</h3><p>支持 PDF、Word、PNG、JPG,文件不超过 20MB。</p><button class="action-button primary" type="button" :disabled="uploading" @click="openReportPicker"><el-icon><UploadFilled /></el-icon>{{ uploading ? '上传中…' : '选择报告附件' }}</button></template>
<template v-else><div class="report-file-card"><div class="report-file-icon"><el-icon><Document /></el-icon></div><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 /></el-icon>预览</button><button type="button" @click="downloadReport"><el-icon><Download /></el-icon>下载</button><button type="button" class="danger" @click="clearReport"><el-icon><Delete /></el-icon>移除</button></div></div><div class="report-replace"><button class="text-action" type="button" :disabled="uploading" @click="openReportPicker"><el-icon><UploadFilled /></el-icon>{{ uploading ? '上传中…' : '更换附件' }}</button><span>附件已上传,可随时预览或下载核对。</span></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 /></el-icon>下载附件</button></template></el-dialog>
</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,1fr) 310px; gap:20px; min-height:100%; padding:22px; color:#d9f2fb; background:radial-gradient(circle at 22% 2%,rgba(21,135,181,.18),transparent 34%),#061521; }.feasibility-core { min-width:0; }.task-header { position:relative; overflow:hidden; padding:28px 32px; border:1px solid rgba(63,204,255,.28); border-radius:20px; background:linear-gradient(108deg,rgba(9,43,63,.97),rgba(5,25,41,.88)); box-shadow:0 20px 46px rgba(0,0,0,.22); }.task-header::after { position:absolute; top:-50px; right:8%; width:180px; height:180px; border:1px solid rgba(79,221,255,.22); border-radius:50%; content:""; }.task-header span { color:#58d9ff; font-size: var(--student-fs-caption); font-weight:800; letter-spacing:.16em; }.task-header h1 { margin:8px 0 0; color:#fff; font-size:32px; letter-spacing:.03em; }.workbench { margin-top:20px; padding:24px; border:1px solid rgba(73,200,248,.25); border-radius:20px; background:rgba(4,27,43,.82); }.workbench-actions { display:flex; justify-content:flex-end; gap:12px; }.action-button { display:inline-flex; align-items:center; justify-content:center; gap:8px; min-height:40px; padding:0 17px; border:1px solid rgba(101,218,255,.52); border-radius:10px; color:#e9fbff; background:rgba(14,109,153,.32); font:inherit; font-weight:800; cursor:pointer; transition:.2s; }.action-button:hover:not(:disabled) { transform:translateY(-1px); border-color:#6ee5ff; background:rgba(20,148,203,.48); }.action-button.primary { border-color:#42d4ff; background:linear-gradient(105deg,#0ca8dc,#08739f); }.action-button:disabled { cursor:not-allowed; opacity:.62; }.step-indicator { display:flex; align-items:stretch; margin:26px 0; padding:10px; border:1px solid rgba(69,201,248,.2); border-radius:15px; background:rgba(1,17,29,.52); }.step-tab { display:flex; flex:1; align-items:center; gap:11px; min-width:0; padding:11px 12px; border:0; border-radius:11px; color:#8eb7c8; background:transparent; text-align:left; cursor:pointer; }.step-tab b { display:grid; flex:0 0 35px; width:35px; height:35px; place-items:center; border:1px 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:4px; color:#5791aa; font-size: var(--student-fs-caption); }.step-tab.active { color:#fff; background:linear-gradient(90deg,rgba(20,170,223,.82),rgba(10,111,155,.72)); box-shadow:0 7px 18px rgba(4,130,184,.22); }.step-tab.active b { border-color:#96edff; background:rgba(5,48,74,.28); }.step-tab.complete { color:#5cddf9; }.step-indicator i { width:1px; margin:10px 3px; background:rgba(88,190,223,.24); }.step-card { padding:24px; border:1px solid rgba(75,205,248,.26); border-radius:16px; background:linear-gradient(145deg,rgba(10,50,72,.64),rgba(3,24,39,.76)); }.step-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:14px; }.step-heading p { margin:0 0 4px; color:#57d8fa; font-size: var(--student-fs-body-sm); font-weight:800; letter-spacing:.1em; }.step-heading h2 { margin:0; color:#fff; font-size: var(--student-fs-h1); }.step-heading em { padding:7px 11px; border:1px solid rgba(83,219,255,.45); border-radius:999px; color:#81e6ff; font-size: var(--student-fs-caption); font-style:normal; }.intro { margin:15px 0 0; color:#b7d8e4; line-height:1.8; }.form-section { margin-top:34px; }.section-title { display:flex; align-items:center; gap:12px; margin-bottom:14px; }.section-title > span { display:grid; width:31px; height:31px; place-items:center; border:1px solid rgba(91,220,255,.6); border-radius:9px; color:#6ee8ff; background:rgba(14,117,160,.24); font-weight:900; }.section-title h3 { margin:0; color:#fff; font-size: var(--student-fs-h1); }.section-title p { margin:3px 0 0; color:#87b6c7; font-size: var(--student-fs-body-sm); }.form-table { display:grid; border:1px solid rgba(96,202,235,.28); border-radius:12px; overflow:hidden; background:rgba(1,18
.decision-section { margin-top:34px; padding:22px; border:1px solid rgba(81,205,243,.27); border-radius:14px; background:linear-gradient(135deg,rgba(5,37,57,.78),rgba(5,20,32,.58)); }.decision-section h3 { display:flex; align-items:center; gap:10px; margin:0; color:#eefcff; font-size: var(--student-fs-h1); }.decision-section h3 > span { display:grid; width:28px; height:28px; place-items:center; border:1px solid rgba(99,225,255,.68); border-radius:8px; color:#70e4ff; font-size: var(--student-fs-body-sm); }.decision-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; margin-top:20px; }.decision-option { position:relative; display:grid; grid-template-columns:20px 1fr; column-gap:10px; align-items:center; min-height:76px; padding:15px; border:1px solid rgba(101,197,225,.3); border-radius:11px; background:rgba(2,18,30,.54); cursor:pointer; transition:.2s; }.decision-option:hover { border-color:rgba(105,227,255,.72); transform:translateY(-1px); }.decision-option input,.priority-option input { position:absolute; opacity:0; pointer-events:none; }.radio-mark,.checkbox-mark { display:grid; width:18px; height:18px; place-items:center; border:1px solid rgba(150,224,244,.7); border-radius:50%; color:#04202f; background:transparent; }.decision-option.selected { border-color:#55ddff; background:linear-gradient(115deg,rgba(15,151,199,.38),rgba(5,56,82,.42)); box-shadow:inset 0 0 0 1px rgba(109,228,255,.1); }.decision-option.selected .radio-mark { border:5px solid #65e4ff; background:#113b51; }.decision-option strong { color:#e8fbff; font-size: var(--student-fs-body); }.decision-option small { grid-column:2; margin-top:4px; color:#84b5c5; font-size: var(--student-fs-caption); }.priority-section { transition:.2s; }.priority-section.muted { opacity:.56; }.priority-heading { display:flex; align-items:center; justify-content:space-between; gap:14px; }.priority-heading b { min-width:47px; padding:6px 9px; border:1px solid rgba(91,219,255,.45); border-radius:999px; color:#78e5ff; background:rgba(13,122,165,.18); text-align:center; }.hint { margin:12px 0 0 38px; color:#84b9ca; font-size: var(--student-fs-body-sm); }.priority-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; margin-top:18px; }.priority-option { display:grid; grid-template-columns:20px 1fr; gap:11px; align-items:start; min-height:66px; padding:14px; border:1px solid rgba(101,197,225,.28); border-radius:10px; background:rgba(1,18,30,.45); cursor:pointer; transition:.2s; }.priority-option:hover { border-color:rgba(100,223,253,.75); }.priority-option.selected { border-color:#52dcff; background:rgba(13,127,171,.26); }.priority-option:has(input:disabled):not(.selected) { cursor:not-allowed; opacity:.52; }.checkbox-mark { margin-top:2px; border-radius:5px; }.priority-option.selected .checkbox-mark { border-color:#68e7ff; color:#092838; background:#68e7ff; }.priority-option strong,.priority-option small { display:block; }.priority-option strong { color:#e5faff; font-size: var(--student-fs-body); }.priority-option small { margin-top:4px; color:#86b7c8; font-size: var(--student-fs-caption); }
.report-guide { margin-top:24px; padding:18px 20px; border-left:3px solid #39d4f8; color:#c5e6f0; background:rgba(8,83,115,.2); line-height:1.85; }.report-guide p,.report-guide ol { margin:0; }.report-guide ol { margin-top:9px; padding-left:23px; }.report-guide li + li { margin-top:6px; }.report-upload-panel { display:grid; min-height:300px; place-content:center; justify-items:center; gap:12px; margin-top:30px; padding:36px; border:1px dashed rgba(83,218,255,.72); border-radius:16px; background:radial-gradient(circle at 50% 22%,rgba(15,149,201,.23),transparent 42%),rgba(1,21,34,.55); text-align:center; }.upload-orbit { display:grid; width:68px; height:68px; place-items:center; border:1px solid rgba(104,225,255,.72); border-radius:22px; color:#74e9ff; background:rgba(9,120,164,.22); font-size:32px; box-shadow:0 0 0 9px rgba(45,193,233,.06); }.report-upload-panel h3 { margin:8px 0 0; color:#f1fcff; font-size:21px; }.report-upload-panel > p { margin:0; color:#8ebfce; }.report-file-card { display:flex; align-items:center; width:min(760px,100%); gap:15px; padding:17px; border:1px solid rgba(93,217,252,.45); border-radius:13px; background:rgba(2,17,29,.7); text-align:left; }.report-file-icon { display:grid; flex:0 0 48px; width:48px; height:48px; place-items:center; border-radius:12px; color:#83e8ff; background:rgba(10,129,176,.28); font-size:25px; }.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:#effdff; }.report-file-meta span { margin-top:5px; color:#83b7c8; font-size: var(--student-fs-caption); }.report-file-actions { display:flex; gap:8px; }.report-file-actions button,.text-action { display:inline-flex; align-items:center; gap:5px; border:0; color:#93eaff; 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:13px; color:#85b7c7; font-size: var(--student-fs-caption); }.text-action:disabled { cursor:not-allowed; opacity:.6; }.visually-hidden { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; }.report-preview { display:grid; min-height:64vh; place-items:center; background:#0a1520; }.report-preview .el-image { width:100%; height:64vh; }.report-preview iframe { width:100%; height:64vh; border:0; background:#fff; }
@media (max-width:1280px) { .feasibility-page { grid-template-columns:1fr; }.step-indicator { overflow-x:auto; }.step-tab { min-width:170px; }.market-table { grid-template-columns:1fr; }.market-table label { border-right:0; }.cost-layout { grid-template-columns:1fr; } }
@media (max-width:720px) { .feasibility-page { padding:10px; }.task-header,.workbench,.step-card { padding:16px; }.task-header h1 { font-size:25px; }.workbench-actions { justify-content:flex-start; flex-wrap:wrap; }.form-table label { grid-template-columns:1fr; }.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:1fr; }.decision-section { padding:16px; }.hint { margin-left:0; }.report-upload-panel { padding:22px 14px; }.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>