|
|
<template>
|
|
|
<div class="feasibility-page student-training-shell">
|
|
|
<main class="feasibility-core">
|
|
|
<section class="task-header">
|
|
|
<div class="task-head-main">
|
|
|
<h1 class="task-title">{{ taskTitle }}</h1>
|
|
|
<p class="task-brief">
|
|
|
<span class="task-brief-text">{{ taskBackground }}</span>
|
|
|
<button type="button" class="task-brief-toggle" @click="briefOpen = !briefOpen">
|
|
|
{{ briefOpen ? "收起" : "展开目标与要求" }}
|
|
|
</button>
|
|
|
<span v-if="briefOpen" class="task-brief-more">
|
|
|
<b>实训目标</b>
|
|
|
<ul>
|
|
|
<li v-for="goal in taskGoals" :key="goal">{{ goal }}</li>
|
|
|
</ul>
|
|
|
<b>实训要求</b>
|
|
|
<p>{{ taskRequirement }}</p>
|
|
|
</span>
|
|
|
</p>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="task-action-strip">
|
|
|
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="saveStepThree" :on-export="exportOutcome" />
|
|
|
</section>
|
|
|
|
|
|
<section class="workbench">
|
|
|
|
|
|
<nav class="step-indicator" aria-label="商业可行性论证步骤">
|
|
|
<button v-for="(step, index) in trainingSteps" :key="step" type="button"
|
|
|
:class="['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]"
|
|
|
:aria-label="`第 ${index + 1} 步:${step}`"
|
|
|
:aria-current="currentStep === index + 1 ? 'step' : undefined" :title="step"
|
|
|
@click="switchStep(index + 1)">
|
|
|
<span class="step-bar" aria-hidden="true"></span>
|
|
|
<span class="step-no" aria-hidden="true">{{ String(index + 1).padStart(2, '0') }}</span>
|
|
|
<span class="step-name">{{ step }}</span>
|
|
|
</button>
|
|
|
</nav>
|
|
|
|
|
|
<section v-if="currentStep === 1" class="step-card">
|
|
|
<div class="step-heading">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step 01</p>
|
|
|
<h2>{{ trainingSteps[0] || "商业可行性论证" }}</h2>
|
|
|
</div><em>结构化论证</em>
|
|
|
</div>
|
|
|
<p class="intro">商业可行性论证需要从多个维度系统展开。以下提供了可行性分析的各个子模块,根据案例信息,完善填写以下信息。</p>
|
|
|
|
|
|
<section class="form-section">
|
|
|
<div class="section-heading"><span>01</span>
|
|
|
<div>
|
|
|
<h3>项目概述</h3>
|
|
|
<p>结合前序 PEST、SWOT 分析,说明项目为何值得推进。</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="form-table overview-table">
|
|
|
<label v-for="field in overviewFields" :key="field.key"><span>{{ field.label
|
|
|
}}</span><el-input v-model="form.projectOverview[field.key]" type="textarea"
|
|
|
:autosize="{ minRows: 2, maxRows: 6 }" :placeholder="field.placeholder" /></label>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="form-section">
|
|
|
<div class="section-heading"><span>02</span>
|
|
|
<div>
|
|
|
<h3>市场规模与竞争格局</h3>
|
|
|
<p>填写可验证的市场数据,比例项目以百分数填写,例如 12.5。</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="form-table market-table">
|
|
|
<label v-for="field in marketFields" :key="field.key"><span>{{ field.label }}</span>
|
|
|
<div class="input-with-suffix"><el-input-number v-if="field.type === 'number'"
|
|
|
v-model="form.marketAnalysis[field.key]" :min="0" :step="0.01"
|
|
|
controls-position="right" :placeholder="field.placeholder" /><el-input v-else
|
|
|
v-model="form.marketAnalysis[field.key]" :placeholder="field.placeholder" /><b
|
|
|
v-if="field.suffix">{{ field.suffix }}</b></div>
|
|
|
</label>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="form-section">
|
|
|
<div class="section-heading"><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-heading"><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-heading"><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-heading"><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-heading"><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="低" /></el-select>
|
|
|
</td>
|
|
|
<td><el-input v-model="risk.response" type="textarea"
|
|
|
:autosize="{ minRows: 2, maxRows: 4 }" placeholder="填写应对措施" /></td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
<section v-else-if="currentStep === 2" class="step-card">
|
|
|
<div class="step-heading">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step 02</p>
|
|
|
<h2>{{ trainingSteps[1] || "项目初步决策" }}</h2>
|
|
|
</div><em>立项判断</em>
|
|
|
</div>
|
|
|
<p class="intro">综合以上所有分析,初步判断项目的可行性,并明确如果立项后最需要优先解决的关键问题。</p>
|
|
|
<section class="decision-section">
|
|
|
<div class="section-heading"><span>01</span>
|
|
|
<div>
|
|
|
<h3>该项目商业可行性总体评估为:</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<el-radio-group v-model="stepTwo.decision" class="decision-grid" @change="handleDecisionChange">
|
|
|
<el-radio v-for="option in feasibilityOptions" :key="option.value" :label="option.value"
|
|
|
class="decision-option">
|
|
|
<strong>{{ option.label }}</strong><small>{{ option.hint }}</small>
|
|
|
</el-radio>
|
|
|
</el-radio-group>
|
|
|
</section>
|
|
|
<section class="decision-section priority-section"
|
|
|
:class="{ muted: stepTwo.decision === 'not-feasible' }">
|
|
|
<div class="priority-heading">
|
|
|
<div class="section-heading"><span>02</span>
|
|
|
<div>
|
|
|
<h3>如果立项,你认为最需要优先解决的 3 个关键问题是:</h3>
|
|
|
</div>
|
|
|
</div><b>{{ selectedPriorityCount }}/3</b>
|
|
|
</div>
|
|
|
<p v-if="stepTwo.decision === 'not-feasible'" class="hint">已选择"不建议立项",本项无需填写。</p>
|
|
|
<p v-else class="hint">请选择且仅选择 3 项,作为后续立项推进的优先事项。</p>
|
|
|
<el-checkbox-group v-model="stepTwo.priorities" class="priority-grid">
|
|
|
<el-checkbox v-for="item in priorityOptions" :key="item.value" :label="item.value"
|
|
|
:disabled="stepTwo.decision === 'not-feasible' || (!stepTwo.priorities.includes(item.value) && selectedPriorityCount >= 3)"
|
|
|
class="priority-option">
|
|
|
<span><strong>{{ item.label }}</strong><small>{{ item.detail }}</small></span>
|
|
|
</el-checkbox>
|
|
|
</el-checkbox-group>
|
|
|
</section>
|
|
|
</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>{{ trainingSteps[2] || "可行性论证报告" }}</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>
|
|
|
<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>
|
|
|
|
|
|
<div class="step-navigation">
|
|
|
<div class="step-nav-left">
|
|
|
<button v-if="currentStep > 1" type="button" class="btn-nav btn-ghost" @click="prevStep">
|
|
|
<el-icon>
|
|
|
<ArrowLeft />
|
|
|
</el-icon>
|
|
|
上一步
|
|
|
</button>
|
|
|
<button type="button" class="btn-nav btn-save" :disabled="saving || uploading"
|
|
|
@click="saveCurrentProgress">
|
|
|
<el-icon>
|
|
|
<CircleCheck />
|
|
|
</el-icon>
|
|
|
保存
|
|
|
</button>
|
|
|
</div>
|
|
|
<div class="step-nav-right">
|
|
|
<span class="step-progress">Step {{ String(currentStep).padStart(2, "0") }} / {{
|
|
|
String(trainingSteps.length).padStart(2, "0") }}</span>
|
|
|
<button v-if="currentStep < trainingSteps.length" type="button" class="btn-nav btn-primary"
|
|
|
:disabled="saving || uploading" @click="nextStep">
|
|
|
{{ saving ? "处理中…" : "下一步" }}
|
|
|
<el-icon>
|
|
|
<ArrowRight />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
</main>
|
|
|
<TrainingAiSidebar study-tip="先从案例资料中提炼可验证的市场、用户与成本依据,再形成一致的商业论证。" reference-title="可行性论证提示"
|
|
|
reference-text="• 结论要有数据或前序分析支撑<br />• 价格、成本和毛利需要逻辑一致<br />• 风险应附带可执行的应对措施"
|
|
|
:progress-items="progressItems" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { ArrowLeft, ArrowRight, Check, CircleCheck, 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 TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"
|
|
|
const TASK_KEY = "business-feasibility"
|
|
|
const DEFAULT_STEPS = ["商业可行性论证", "项目初步决策", "可行性论证报告", "决策结论"]
|
|
|
const overviewFields = [
|
|
|
{ key: "projectBackground", label: "项目背景", placeholder: "示例:根据案例信息简述公司基本情况,再简要阐述智能家居、个护健康、美妆工具等相关市场发展情况和需求情况。" },
|
|
|
{ key: "productPositioning", label: "产品定位", placeholder: "示例:阐述项目产品针对的目标市场用户、核心卖点、使用场景、差异化理由以及用户收益。" },
|
|
|
{ key: "projectGoals", label: "项目目标", placeholder: "示例:填写 12 个月短期目标、24 个月中期目标和 36 个月长期目标。" },
|
|
|
{ key: "macroEnvironmentConclusion", label: "宏观环境分析结论", placeholder: "示例:项目产品 PEST 分析结论。" },
|
|
|
{ key: "competitiveLandscapeConclusion", label: "竞争态势分析结论", placeholder: "示例:项目产品 SWOT 分析结论。" },
|
|
|
]
|
|
|
const marketFields = [
|
|
|
{ key: "annualMarketSize", label: "年度市场规模", type: "number", suffix: "亿元", placeholder: "例如 80" },
|
|
|
{ key: "monthlyGrowthRate", label: "月均增长率", type: "number", suffix: "%", placeholder: "例如 2.5" },
|
|
|
{ key: "topBrandMarketShare", label: "头部第一品牌市场份额", type: "number", suffix: "%", placeholder: "例如 26" },
|
|
|
{ key: "competitorsPriceRange", label: "主要竞品售价区间", suffix: "元", placeholder: "例如 299 - 899" },
|
|
|
{ key: "marketConcentrationCr3", label: "市场集中度(CR3)", type: "number", suffix: "%", placeholder: "例如 58" },
|
|
|
]
|
|
|
const targetUserExamples = [
|
|
|
{ userGroup: "示例:18-35 岁一二线城市美妆爱好者、美妆博主", scenario: "示例:美妆产品带货、线上美妆教程", coreNeed: "示例:补光效果、AI 测肤精度", spendingPower: "示例:高" },
|
|
|
{ userGroup: "示例:22-40 岁租房白领、早八通勤族", scenario: "示例:早上上班前快速护肤化妆", coreNeed: "示例:日常化妆、耐用", spendingPower: "示例:中" },
|
|
|
{ userGroup: "示例:18-28 岁学生群体、礼品采购人群", scenario: "示例:宿舍美妆尝试、礼物赠送", coreNeed: "示例:宿舍美妆工具、生日礼物", spendingPower: "示例:低" },
|
|
|
]
|
|
|
const feasibilityOptions = [
|
|
|
{ value: "highly-feasible", label: "高度可行,建议立即立项启动", hint: "市场、收益与风险条件均具备" },
|
|
|
{ value: "feasible-after-optimization", label: "基本可行,建议在优化部分条件后立项", hint: "关键条件完善后再推进" },
|
|
|
{ value: "observe", label: "可行性一般,建议暂缓立项继续观察", hint: "保留机会并持续验证" },
|
|
|
{ value: "not-feasible", label: "不可行,建议放弃该方向", hint: "当前条件不支持投入" },
|
|
|
]
|
|
|
const priorityOptions = [
|
|
|
{ value: "hardware-cost-control", label: "硬件成本控制", detail: "降低 BOM 成本" },
|
|
|
{ value: "content-ecosystem", label: "内容生态搭建", detail: "相关教程课程合作" },
|
|
|
{ value: "channel-expansion", label: "渠道拓展", detail: "线上流量获取" },
|
|
|
{ value: "brand-differentiation", label: "品牌差异化", detail: "与头部品牌形成区隔" },
|
|
|
{ value: "supply-chain-management", label: "供应链管理", detail: "产能保障与品质控制" },
|
|
|
{ value: "capital-planning", label: "资金筹措", detail: "融资或分期投入" },
|
|
|
]
|
|
|
const defaultTask = { title: "商业可行性论证", background: "在产品开发前,综合市场机会、用户需求、竞争态势、成本收益和风险,判断项目是否具有可推进的商业价值。", goals: ["掌握商业可行性论证的基本维度", "形成市场、用户、成本与收益的一致判断", "识别主要风险并提出应对措施"], requirement: "请以案例资料和前序分析为依据,完成表格全部内容;成本与价格数据需真实、合理且逻辑一致。" }
|
|
|
const { proxy } = getCurrentInstance()
|
|
|
const briefOpen = ref(true)
|
|
|
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 prevStep() { if (currentStep.value > 1) currentStep.value -= 1 }
|
|
|
async function saveCurrentProgress() { if (saving.value) return; saving.value = true; try { const step = currentStep.value; const payload = { currentStep: step, submitted: false, saveAction: "SAVE" }; if (step === 1) payload.step1Answer = JSON.stringify({ form: form.value, calculations: { unitTotalCost: totalCost.value, unitGrossProfit: unitGrossProfit.value, grossMargin: grossMargin.value } }); else if (step === 2) payload.step2Answer = JSON.stringify({ stepTwo: stepTwo.value }); else if (step === 3) payload.step3Answer = JSON.stringify({ report: report.value }); await saveStudentTrainingAnswer(TASK_KEY, payload); proxy?.$modal?.msgSuccess("已保存") } catch (error) { proxy?.$modal?.msgError?.(error?.message || "保存失败,请稍后重试") } finally { saving.value = false } }
|
|
|
async function nextStep() { if (currentStep.value === 1) return validateAndContinue(); if (currentStep.value === 2) return saveStepTwo(); if (currentStep.value === 3) return saveStepThree() }
|
|
|
function exportOutcome() { const blob = new Blob([JSON.stringify({ task: taskTitle.value, step: 1, ...payload(), calculations: { unitTotalCost: totalCost.value, unitGrossProfit: unitGrossProfit.value, grossMargin: grossMargin.value } }, null, 2)], { type: "application/json;charset=utf-8" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "商业可行性论证-第1步.json"; link.click(); URL.revokeObjectURL(link.href) }
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
/* ===== 页面级令牌(教育蓝极简主题) ===== */
|
|
|
.feasibility-page {
|
|
|
--student-accent: #2563eb;
|
|
|
--student-accent-deep: #1d4ed8;
|
|
|
--student-accent-light: #3b82f6;
|
|
|
--student-accent-soft: #eff6ff;
|
|
|
--student-danger: #ef4444;
|
|
|
--student-success: #2ba471;
|
|
|
--student-layer-1: #1d4ed8;
|
|
|
--student-layer-2: #3b82f6;
|
|
|
--student-layer-3: #93c5fd;
|
|
|
--student-panel-border: #e5e7eb;
|
|
|
--student-card-bg: #ffffff;
|
|
|
--student-panel-bg: #fafbfc;
|
|
|
--student-text: #1f2937;
|
|
|
--student-text-secondary: #6b7280;
|
|
|
--el-color-primary: #2563eb;
|
|
|
--el-color-primary-light-3: #60a5fa;
|
|
|
--el-color-primary-light-5: #93c5fd;
|
|
|
--el-color-primary-light-7: #bfdbfe;
|
|
|
--el-color-primary-light-8: #dbeafe;
|
|
|
--el-color-primary-light-9: #eff6ff;
|
|
|
--el-color-primary-dark-2: #1d4ed8;
|
|
|
--el-input-hover-border-color: #3b82f6;
|
|
|
--el-input-focus-border-color: #2563eb;
|
|
|
--el-input-focus-ring-color: rgba(59, 130, 246, 0.15);
|
|
|
--el-textarea-hover-border-color: #3b82f6;
|
|
|
--el-textarea-focus-border-color: #2563eb;
|
|
|
/* 字号梯度统一继承 .student-training-shell,页面不再重复声明 */
|
|
|
|
|
|
position: relative;
|
|
|
z-index: 2;
|
|
|
display: grid;
|
|
|
grid-template-columns: minmax(720px, 1fr) 340px;
|
|
|
gap: 24px;
|
|
|
width: 100%;
|
|
|
max-width: 100%;
|
|
|
min-height: calc(100vh - 106px);
|
|
|
margin: 0;
|
|
|
padding: 24px 24px 32px;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.feasibility-core {
|
|
|
min-width: 0;
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
/* task-header 独立卡片 */
|
|
|
.task-header {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
margin: 0;
|
|
|
padding: 18px 20px 18px 24px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: var(--student-card-bg);
|
|
|
overflow: hidden;
|
|
|
|
|
|
&::before {
|
|
|
content: '';
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
bottom: 0;
|
|
|
left: 0;
|
|
|
width: 4px;
|
|
|
background: var(--student-accent);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.task-head-main {
|
|
|
display: grid;
|
|
|
justify-items: start;
|
|
|
gap: 8px;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
.task-action-strip {
|
|
|
margin: 0;
|
|
|
padding: 10px 16px;
|
|
|
}
|
|
|
|
|
|
.task-action-strip :deep(.training-task-toolbar) {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
justify-content: flex-end;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
|
|
|
.task-title {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
.task-brief {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.task-brief-text {
|
|
|
display: block;
|
|
|
}
|
|
|
|
|
|
.task-brief-toggle {
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
color: var(--student-accent-deep);
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover {
|
|
|
text-decoration: underline;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.task-brief-more {
|
|
|
display: grid;
|
|
|
gap: 6px;
|
|
|
margin-top: 8px;
|
|
|
|
|
|
b {
|
|
|
color: var(--student-text);
|
|
|
}
|
|
|
|
|
|
ul {
|
|
|
display: grid;
|
|
|
gap: 4px;
|
|
|
margin: 0;
|
|
|
padding-left: 18px;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* workbench:扁平化容器 */
|
|
|
.workbench {
|
|
|
margin-top: 0;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
display: grid;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.workbench-actions {
|
|
|
display: flex;
|
|
|
flex: 0 0 auto;
|
|
|
justify-content: flex-end;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
/* action-button 基础样式(次按钮:白底描边) */
|
|
|
.action-button {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
transform: translateY(-1px);
|
|
|
border-color: var(--student-accent);
|
|
|
color: var(--student-accent);
|
|
|
background: #eff6ff;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
|
|
|
&.primary {
|
|
|
border-color: var(--student-accent);
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font-weight: 700;
|
|
|
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.22);
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
transform: translateY(-1px);
|
|
|
border-color: var(--student-accent-deep);
|
|
|
background: var(--student-accent-deep);
|
|
|
color: #ffffff;
|
|
|
box-shadow: 0 4px 12px rgba(29, 78, 216, 0.30);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
transform: none;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* step-card:独立卡片 */
|
|
|
.step-card {
|
|
|
padding: 16px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 16px;
|
|
|
background: var(--student-card-bg);
|
|
|
}
|
|
|
|
|
|
.step-heading {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 14px;
|
|
|
}
|
|
|
|
|
|
.section-eyebrow {
|
|
|
margin: 0 0 2px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 800;
|
|
|
letter-spacing: 0.12em;
|
|
|
}
|
|
|
|
|
|
.step-heading h2 {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.step-heading em {
|
|
|
padding: 4px 10px;
|
|
|
border: 1px solid #d6dae0;
|
|
|
border-radius: 999px;
|
|
|
color: var(--student-accent-light);
|
|
|
background: var(--student-accent-soft);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-style: normal;
|
|
|
font-weight: 700;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.intro {
|
|
|
margin: 10px 0 0;
|
|
|
color: #374151;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.form-section {
|
|
|
margin-top: 24px;
|
|
|
}
|
|
|
|
|
|
/* form-table:扁平化表格 */
|
|
|
.form-table {
|
|
|
display: grid;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
overflow: hidden;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.form-table label {
|
|
|
display: grid;
|
|
|
grid-template-columns: 180px 1fr;
|
|
|
border-bottom: 1px solid var(--student-panel-border);
|
|
|
}
|
|
|
|
|
|
.form-table label:last-child {
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.form-table label>span {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: flex-start;
|
|
|
padding: 10px 12px;
|
|
|
color: var(--student-text);
|
|
|
background: var(--student-panel-bg);
|
|
|
font-weight: 700;
|
|
|
font-size: var(--student-fs-body);
|
|
|
}
|
|
|
|
|
|
.market-table {
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
}
|
|
|
|
|
|
.market-table label {
|
|
|
border-right: 1px solid var(--student-panel-border);
|
|
|
}
|
|
|
|
|
|
.market-table label:nth-child(2n) {
|
|
|
border-right: 0;
|
|
|
}
|
|
|
|
|
|
.market-table label:last-child {
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.input-with-suffix {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
.input-with-suffix b {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
white-space: nowrap;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
/* table-wrap + Element Plus table 内的表单组件 */
|
|
|
.table-wrap {
|
|
|
overflow-x: auto;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
table {
|
|
|
width: 100%;
|
|
|
min-width: 780px;
|
|
|
border-collapse: collapse;
|
|
|
}
|
|
|
|
|
|
th {
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 800;
|
|
|
background: var(--student-panel-bg);
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
th,
|
|
|
td {
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
padding: 8px 10px;
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
|
|
|
tr:last-child td {
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
|
|
|
th:last-child,
|
|
|
td:last-child {
|
|
|
border-right: 0;
|
|
|
}
|
|
|
|
|
|
/* Element Plus 组件在表格中的紧凑样式 */
|
|
|
.table-wrap td :deep(.el-textarea),
|
|
|
.table-wrap td :deep(.el-input),
|
|
|
.table-wrap td :deep(.el-select) {
|
|
|
max-width: 100%;
|
|
|
}
|
|
|
|
|
|
.table-wrap td :deep(.el-textarea__inner) {
|
|
|
min-height: 60px !important;
|
|
|
padding: 6px 10px;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.table-wrap td :deep(.el-input__wrapper),
|
|
|
.table-wrap td :deep(.el-textarea__inner),
|
|
|
.table-wrap td :deep(.el-select__wrapper) {
|
|
|
border-color: #d1d5db !important;
|
|
|
border-radius: 8px;
|
|
|
background: #ffffff;
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: var(--student-accent-light) !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.table-wrap td :deep(.el-input__wrapper.is-focus),
|
|
|
.table-wrap td :deep(.el-textarea__inner:focus),
|
|
|
.table-wrap td :deep(.el-select__wrapper.is-focused) {
|
|
|
border-color: var(--student-accent) !important;
|
|
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15) !important;
|
|
|
}
|
|
|
|
|
|
.table-wrap td :deep(.el-input-number) {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.table-wrap td :deep(.el-input-number .el-input__wrapper) {
|
|
|
padding: 0 8px;
|
|
|
}
|
|
|
|
|
|
.fixed-cell {
|
|
|
color: var(--student-text);
|
|
|
background: var(--student-panel-bg);
|
|
|
text-align: center;
|
|
|
font-weight: 700;
|
|
|
font-size: var(--student-fs-body);
|
|
|
}
|
|
|
|
|
|
.computed-value {
|
|
|
display: inline-block;
|
|
|
color: var(--student-text);
|
|
|
font-weight: 700;
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
}
|
|
|
|
|
|
.total-row td {
|
|
|
color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
font-weight: 800;
|
|
|
font-size: var(--student-fs-body);
|
|
|
}
|
|
|
|
|
|
/* ===== 底部导航:工具栏式 ===== */
|
|
|
.step-navigation {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 10px;
|
|
|
margin-top: 16px;
|
|
|
padding: 12px 16px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
flex-wrap: nowrap;
|
|
|
}
|
|
|
|
|
|
.step-nav-right {
|
|
|
margin-left: auto;
|
|
|
}
|
|
|
|
|
|
.btn-nav {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border-radius: 8px;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: var(--student-fs-h2);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-primary {
|
|
|
border: 1px solid var(--student-accent);
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.2), 0 2px 8px rgba(37, 99, 235, 0.15);
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent-deep);
|
|
|
background: var(--student-accent-deep);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.3), 0 4px 12px rgba(29, 78, 216, 0.22);
|
|
|
}
|
|
|
|
|
|
&:active:not(:disabled) {
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-ghost {
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-save {
|
|
|
border: 1px solid #e5e7eb;
|
|
|
color: #4b5563;
|
|
|
background: #f9fafb;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: #d1d5db;
|
|
|
background: #f3f4f6;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-primary:disabled,
|
|
|
.btn-ghost:disabled,
|
|
|
.btn-save:disabled,
|
|
|
.btn-nav:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
transform: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.step-progress {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.05em;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
/* placeholder */
|
|
|
.placeholder {
|
|
|
display: grid;
|
|
|
min-height: 240px;
|
|
|
place-content: center;
|
|
|
justify-items: center;
|
|
|
gap: 14px;
|
|
|
color: var(--student-text-secondary);
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.placeholder p {
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
color: var(--student-text);
|
|
|
}
|
|
|
|
|
|
/* ===== Step 2 决策区 ===== */
|
|
|
.decision-section {
|
|
|
margin-top: 24px;
|
|
|
padding: 12px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 14px;
|
|
|
background: var(--student-panel-bg);
|
|
|
}
|
|
|
|
|
|
/* decision-grid 使用 Element Plus el-radio-group */
|
|
|
.decision-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
gap: 10px;
|
|
|
margin-top: 12px;
|
|
|
}
|
|
|
|
|
|
:deep(.decision-option.el-radio) {
|
|
|
display: grid;
|
|
|
grid-template-columns: auto 1fr;
|
|
|
gap: 8px;
|
|
|
align-items: start;
|
|
|
padding: 12px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
color: #374151;
|
|
|
background: #ffffff;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease;
|
|
|
margin-right: 0;
|
|
|
height: auto;
|
|
|
|
|
|
strong {
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
small {
|
|
|
display: block;
|
|
|
margin-top: 3px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
line-height: 1.5;
|
|
|
}
|
|
|
|
|
|
&:hover {
|
|
|
border-color: var(--student-accent-light);
|
|
|
transform: translateY(-1px);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 墨黑单选圆点 */
|
|
|
:deep(.decision-option.el-radio .el-radio__input .el-radio__inner) {
|
|
|
border-color: #9ca3af;
|
|
|
}
|
|
|
|
|
|
:deep(.decision-option.el-radio .el-radio__input.is-checked .el-radio__inner) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
:deep(.decision-option.el-radio .el-radio__input.is-checked .el-radio__inner::after) {
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
:deep(.decision-option.el-radio.is-checked) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
}
|
|
|
|
|
|
:deep(.decision-option.el-radio.is-checked .el-radio__label) {
|
|
|
color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
/* priority-grid */
|
|
|
.priority-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
gap: 10px;
|
|
|
margin-top: 12px;
|
|
|
}
|
|
|
|
|
|
:deep(.priority-option.el-checkbox) {
|
|
|
display: grid;
|
|
|
grid-template-columns: auto 1fr;
|
|
|
gap: 8px;
|
|
|
align-items: start;
|
|
|
padding: 10px 12px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
color: #374151;
|
|
|
background: #ffffff;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease;
|
|
|
margin-right: 0;
|
|
|
height: auto;
|
|
|
|
|
|
strong {
|
|
|
display: block;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
small {
|
|
|
display: block;
|
|
|
margin-top: 3px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
line-height: 1.5;
|
|
|
}
|
|
|
|
|
|
&:hover:not(.is-disabled) {
|
|
|
border-color: var(--student-accent-light);
|
|
|
transform: translateY(-1px);
|
|
|
}
|
|
|
|
|
|
&.is-disabled {
|
|
|
opacity: 0.45;
|
|
|
cursor: not-allowed;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 墨黑复选框 */
|
|
|
:deep(.priority-option.el-checkbox .el-checkbox__inner) {
|
|
|
border-color: #9ca3af;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
:deep(.priority-option.el-checkbox.is-checked .el-checkbox__inner) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
.priority-section {
|
|
|
transition: opacity 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.priority-section.muted {
|
|
|
opacity: 0.45;
|
|
|
}
|
|
|
|
|
|
.priority-heading {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 14px;
|
|
|
}
|
|
|
|
|
|
.priority-heading .section-heading {
|
|
|
flex: 1 1 auto;
|
|
|
margin-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.priority-heading b {
|
|
|
min-width: 44px;
|
|
|
padding: 4px 8px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 999px;
|
|
|
color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
text-align: center;
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 800;
|
|
|
}
|
|
|
|
|
|
.hint {
|
|
|
margin: 10px 0 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
/* ===== Step 3 报告区 ===== */
|
|
|
.report-guide {
|
|
|
margin-top: 16px;
|
|
|
padding: 12px 14px;
|
|
|
border-left: 3px solid var(--student-accent);
|
|
|
color: #374151;
|
|
|
background: var(--student-accent-soft);
|
|
|
line-height: 1.7;
|
|
|
font-size: var(--student-fs-body);
|
|
|
border-radius: 0 10px 10px 0;
|
|
|
}
|
|
|
|
|
|
.report-guide p,
|
|
|
.report-guide ol {
|
|
|
margin: 0;
|
|
|
}
|
|
|
|
|
|
.report-guide ol {
|
|
|
margin-top: 6px;
|
|
|
padding-left: 18px;
|
|
|
}
|
|
|
|
|
|
.report-guide li+li {
|
|
|
margin-top: 3px;
|
|
|
}
|
|
|
|
|
|
.report-upload-panel {
|
|
|
display: grid;
|
|
|
min-height: 260px;
|
|
|
place-content: center;
|
|
|
justify-items: center;
|
|
|
gap: 10px;
|
|
|
margin-top: 20px;
|
|
|
padding: 30px;
|
|
|
border: 1px dashed #d1d5db;
|
|
|
border-radius: 14px;
|
|
|
background: var(--student-panel-bg);
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.upload-orbit {
|
|
|
display: grid;
|
|
|
width: 56px;
|
|
|
height: 56px;
|
|
|
place-items: center;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 16px;
|
|
|
color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
font-size: 26px;
|
|
|
}
|
|
|
|
|
|
.report-upload-panel h3 {
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.report-upload-panel>p {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.report-file-card {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
width: min(720px, 100%);
|
|
|
gap: 12px;
|
|
|
padding: 14px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
.report-file-icon {
|
|
|
display: grid;
|
|
|
flex: 0 0 40px;
|
|
|
width: 40px;
|
|
|
height: 40px;
|
|
|
place-items: center;
|
|
|
border-radius: 10px;
|
|
|
color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
font-size: 22px;
|
|
|
}
|
|
|
|
|
|
.report-file-meta {
|
|
|
min-width: 0;
|
|
|
flex: 1;
|
|
|
}
|
|
|
|
|
|
.report-file-meta strong,
|
|
|
.report-file-meta span {
|
|
|
display: block;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.report-file-meta strong {
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.report-file-meta span {
|
|
|
margin-top: 3px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
}
|
|
|
|
|
|
.report-file-actions {
|
|
|
display: flex;
|
|
|
gap: 6px;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.report-file-actions button,
|
|
|
.text-action {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: 4px;
|
|
|
border: 0;
|
|
|
color: var(--student-accent);
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
padding: 4px 6px;
|
|
|
border-radius: 6px;
|
|
|
transition: background 0.16s ease;
|
|
|
|
|
|
&:hover {
|
|
|
background: var(--student-accent-soft);
|
|
|
}
|
|
|
|
|
|
&.danger {
|
|
|
color: var(--student-danger);
|
|
|
|
|
|
&:hover {
|
|
|
background: rgba(217, 85, 76, 0.08);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.report-replace {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
}
|
|
|
|
|
|
.text-action:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
}
|
|
|
|
|
|
.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: #ffffff;
|
|
|
}
|
|
|
|
|
|
.report-preview .el-image {
|
|
|
width: 100%;
|
|
|
height: 64vh;
|
|
|
}
|
|
|
|
|
|
.report-preview iframe {
|
|
|
width: 100%;
|
|
|
height: 64vh;
|
|
|
border: 0;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
/* 风险评估下拉框宽度 */
|
|
|
.risk-impact-select {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
/* ===== 响应式 ===== */
|
|
|
@media (max-width: 1280px) {
|
|
|
.step-indicator {
|
|
|
overflow-x: auto;
|
|
|
}
|
|
|
|
|
|
.step-tab {
|
|
|
min-width: 140px;
|
|
|
}
|
|
|
|
|
|
.market-table {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.market-table label {
|
|
|
border-right: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
.form-table label {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.form-table label>span {
|
|
|
justify-content: flex-start;
|
|
|
padding: 8px 12px;
|
|
|
}
|
|
|
|
|
|
.decision-grid,
|
|
|
.priority-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.step-heading em {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.step-navigation {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.btn-nav {
|
|
|
width: 100%;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
.workbench-actions {
|
|
|
justify-content: flex-start;
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.report-upload-panel {
|
|
|
padding: 20px 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;
|
|
|
}
|
|
|
}
|
|
|
/* ===== 步骤内小节标题(蓝底序号,统一规范) ===== */
|
|
|
.section-heading {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
gap: 12px;
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
>span {
|
|
|
display: grid;
|
|
|
flex: 0 0 28px;
|
|
|
width: 28px;
|
|
|
height: 28px;
|
|
|
place-items: center;
|
|
|
border-radius: 6px;
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent, #2563eb);
|
|
|
font-size: var(--student-fs-body, 14px);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
h3 {
|
|
|
margin: 0;
|
|
|
color: var(--student-text, #1f2937);
|
|
|
font-size: var(--student-fs-h1, 18px);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text-secondary, #6b7280);
|
|
|
font-size: var(--student-fs-body, 14px);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<style lang="scss">
|
|
|
/* ===== 步骤导航:分段连接胶囊(SKILL.md 规格) ===== */
|
|
|
#app .studentLayout section.app-main:has(.feasibility-page) .step-indicator {
|
|
|
display: flex !important;
|
|
|
flex-wrap: nowrap !important;
|
|
|
align-items: stretch !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 0 !important;
|
|
|
width: 100% !important;
|
|
|
margin: 0 0 20px !important;
|
|
|
padding: 0 !important;
|
|
|
border: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: transparent !important;
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
.step-tab {
|
|
|
|
|
|
|
|
|
display: flex !important;
|
|
|
flex-direction: row !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 8px !important;
|
|
|
min-width: 0 !important;
|
|
|
height: auto !important;
|
|
|
min-height: 40px !important;
|
|
|
margin: 0 !important;
|
|
|
padding: 8px 14px !important;
|
|
|
border: 1px solid #e5e7eb !important;
|
|
|
border-right: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-size: var(--student-fs-body) !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1.3 !important;
|
|
|
text-align: center !important;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease !important;
|
|
|
|
|
|
&:first-child {
|
|
|
border-radius: 8px 0 0 8px !important;
|
|
|
}
|
|
|
|
|
|
&:last-child {
|
|
|
border-radius: 0 8px 8px 0 !important;
|
|
|
border-right: 1px solid #e5e7eb !important;
|
|
|
}
|
|
|
|
|
|
.step-bar {
|
|
|
display: none !important;
|
|
|
}
|
|
|
|
|
|
.step-no {
|
|
|
position: static !important;
|
|
|
flex: 0 0 auto !important;
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
min-width: 22px !important;
|
|
|
height: 22px !important;
|
|
|
padding: 0 5px !important;
|
|
|
border-radius: 6px !important;
|
|
|
background: #f3f4f6 !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-family: var(--student-font-data), ui-monospace, "SF Mono", Consolas, monospace !important;
|
|
|
font-size: var(--student-fs-body-sm) !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: all 0.16s ease !important;
|
|
|
|
|
|
&::after {
|
|
|
display: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.step-name {
|
|
|
color: inherit !important;
|
|
|
}
|
|
|
|
|
|
&:hover:not(.active):not(.complete) {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
color: #2563eb !important;
|
|
|
z-index: 1 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.complete:not(.active) {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #374151 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: rgba(43, 164, 113, 0.12) !important;
|
|
|
color: #16915f !important;
|
|
|
}
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #2ba471 !important;
|
|
|
background: #f0fdf6 !important;
|
|
|
z-index: 1 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: #2ba471 !important;
|
|
|
color: #ffffff !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.active {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
font-weight: 700 !important;
|
|
|
z-index: 2 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: #ffffff !important;
|
|
|
color: #2563eb !important;
|
|
|
}
|
|
|
|
|
|
&:hover {
|
|
|
background: #1d4ed8 !important;
|
|
|
border-color: #1d4ed8 !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 窄屏:横向滑动 */
|
|
|
@media (max-width: 900px) {
|
|
|
#app .studentLayout section.app-main:has(.feasibility-page) .step-indicator {
|
|
|
justify-content: flex-start !important;
|
|
|
overflow-x: auto !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feasibility-page) .step-indicator .step-tab {
|
|
|
flex: 0 0 140px !important;
|
|
|
max-width: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 顶部工具栏:TrainingMaterialButton 资料按钮 → 教育蓝
|
|
|
* ============================================================ */
|
|
|
.feasibility-page .workbench-actions .material-button.el-button {
|
|
|
height: 34px !important;
|
|
|
min-height: 34px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: var(--student-fs-body) !important;
|
|
|
font-weight: 600 !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
|
|
|
&.is-plain {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #9ca3af !important;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #9ca3af !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 顶部导出按钮(原生 <button.action-button>)
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.feasibility-page) .workbench-actions>button.action-button {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 6px !important;
|
|
|
height: 34px !important;
|
|
|
min-height: 34px !important;
|
|
|
padding: 0 14px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: var(--student-fs-body) !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feasibility-page) .workbench-actions>button.action-button:hover {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
transform: none !important;
|
|
|
}
|
|
|
</style>
|