|
|
<template>
|
|
|
<div class="product-positioning-page student-training-shell">
|
|
|
<main class="product-positioning-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="submitTask" :on-reset="resetTraining"
|
|
|
:on-export="exportOutcome" />
|
|
|
</section>
|
|
|
|
|
|
<section class="task-card">
|
|
|
<nav v-if="configuredSteps.length" class="step-indicator" aria-label="产品价值主张及定位步骤">
|
|
|
<template v-for="(step, index) in configuredSteps" :key="`${step}-${index}`">
|
|
|
<button type="button"
|
|
|
:class="['step-tab', { active: form.activeStep === index + 1, complete: form.activeStep > index + 1 }]"
|
|
|
:aria-current="form.activeStep === index + 1 ? 'step' : undefined"
|
|
|
:aria-label="`第${index + 1}步:${step}`" @click="setCurrentStep(index + 1)">
|
|
|
<span class="step-marker">{{ String(index + 1).padStart(2, '0') }}</span>
|
|
|
<span class="step-copy">
|
|
|
<strong class="step-name" :title="step">{{ step }}</strong>
|
|
|
<span class="step-action">{{ form.activeStep === index + 1 ? "当前步骤" : "点击切换" }} <span
|
|
|
aria-hidden="true">→</span></span>
|
|
|
</span>
|
|
|
</button>
|
|
|
<span v-if="index < configuredSteps.length - 1" class="step-connector"
|
|
|
aria-hidden="true"></span>
|
|
|
</template>
|
|
|
</nav>
|
|
|
|
|
|
<section v-show="isSectionVisible(1)" class="step-card">
|
|
|
<div v-if="activeStepName" class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step {{ String(form.activeStep).padStart(2, '0') }}</p>
|
|
|
<h3>{{ activeStepName }}</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="core-pain-intro">
|
|
|
<p>价值主张的起点不是产品功能,而是用户痛点。不要试图满足所有人,只抓占 80% 的核心用户最痛的 3 个问题。</p>
|
|
|
<p>参考实训 11 的需求优先级评估结果,整理出 Top3 核心痛点,同时识别弱需求和伪需求——它们不作为核心价值主张,只作为加分项。</p>
|
|
|
</div>
|
|
|
<div class="core-pain-table-wrap">
|
|
|
<table class="core-pain-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>序号</th>
|
|
|
<th>需求类型</th>
|
|
|
<th>需求描述</th>
|
|
|
<th>场景描述</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="(row, index) in form.corePainRows" :key="row.id">
|
|
|
<td>{{ index + 1 }}</td>
|
|
|
<td class="pain-type-cell">{{ row.type }}</td>
|
|
|
<td><textarea v-model="row.description"
|
|
|
:placeholder="index === 0 ? '示例:入睡困难' : '填写需求描述'"
|
|
|
:aria-label="`${row.type}需求描述`" /></td>
|
|
|
<td><textarea v-model="row.scene"
|
|
|
:placeholder="index === 0 ? '示例:高压职场人群睡前大脑兴奋难以切换到放松状态,传统吸顶灯只有冷白光切换,缺乏渐进式助眠的光环境引导,导致入睡时间平均超过40分钟' : '填写场景描述'"
|
|
|
:aria-label="`${row.type}场景描述`" /></td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
<p v-if="corePainValidationMessage" class="core-pain-validation-message" role="status">{{
|
|
|
corePainValidationMessage
|
|
|
}}</p>
|
|
|
</section>
|
|
|
|
|
|
<section v-show="isSectionVisible(2)" class="step-card">
|
|
|
<div v-if="activeStepName" class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step {{ String(form.activeStep).padStart(2, '0') }}</p>
|
|
|
<h3>{{ activeStepName }}</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="value-proposition-intro">
|
|
|
根据细分用户、具体场景、独特价值和用户收益提炼核心价值主张,运用价值主张画布从“用户档案”和“产品价值地图”两个维度找到匹配点。
|
|
|
</div>
|
|
|
|
|
|
<div class="canvas-section-heading">
|
|
|
<div class="section-heading"><span>01</span>
|
|
|
<div>
|
|
|
<h3>用户档案 <small>Customer Profile</small></h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<button type="button" class="canvas-export-button"
|
|
|
@click="exportCanvasSection('用户档案', customerProfileExportContent)">
|
|
|
<el-icon>
|
|
|
<Download />
|
|
|
</el-icon>导出
|
|
|
</button>
|
|
|
</div>
|
|
|
<div class="value-canvas-table-wrap">
|
|
|
<table class="value-canvas-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>维度</th>
|
|
|
<th>说明</th>
|
|
|
<th>内容</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr>
|
|
|
<td>用户目标(Jobs)</td>
|
|
|
<td>用户在相关场景中试图完成的任务</td>
|
|
|
<td><textarea v-model="form.customerProfile.jobs"
|
|
|
placeholder="示例:快速入睡、提升睡眠质量、温和唤醒不犯困、起夜不刺眼" /></td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td>痛点(Pains)</td>
|
|
|
<td>用户遇到的障碍、风险和负面体验</td>
|
|
|
<td><textarea v-model="form.customerProfile.pains" placeholder="填写用户痛点" /></td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td>收益期望(Gains)</td>
|
|
|
<td>用户期望的结果和额外收益</td>
|
|
|
<td><textarea v-model="form.customerProfile.gains" placeholder="填写用户收益期望" /></td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<div class="canvas-section-heading canvas-section-heading--spaced">
|
|
|
<div class="section-heading"><span>02</span>
|
|
|
<div>
|
|
|
<h3>产品价值地图 <small>Value Map</small></h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<button type="button" class="canvas-export-button"
|
|
|
@click="exportCanvasSection('产品价值地图', valueMapExportContent)">
|
|
|
<el-icon>
|
|
|
<Download />
|
|
|
</el-icon>导出
|
|
|
</button>
|
|
|
</div>
|
|
|
<div class="value-canvas-table-wrap">
|
|
|
<table class="value-canvas-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>维度</th>
|
|
|
<th>说明</th>
|
|
|
<th>内容</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr>
|
|
|
<td>产品与服务</td>
|
|
|
<td>我们提供给用户的产品和服务清单</td>
|
|
|
<td><textarea v-model="form.valueMap.products"
|
|
|
placeholder="示例:智能声光伴眠吸顶灯(含 App 控制)、白噪音声库、睡眠监测服务、节律光算法" /></td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td>痛点缓释器(Pain Relievers)</td>
|
|
|
<td>产品如何消除或缓解用户痛点</td>
|
|
|
<td><textarea v-model="form.valueMap.painRelievers" placeholder="填写痛点缓释器" /></td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td>增益创造器(Gain Creators)</td>
|
|
|
<td>产品如何创造额外价值和惊喜体验</td>
|
|
|
<td><textarea v-model="form.valueMap.gainCreators" placeholder="填写增益创造器" /></td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<div class="canvas-drawing-section">
|
|
|
<div class="section-heading"><span>03</span>
|
|
|
<div>
|
|
|
<h3>绘制产品价值主张画布</h3>
|
|
|
<p>检查“产品价值地图”与“用户档案”是否实现匹配(Fit):每个痛点应有对应的痛点缓释器,每个收益期望应有对应的增益创造器。根据上表绘制产品价值主张画布。</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<input ref="valueCanvasInput" class="visually-hidden" type="file"
|
|
|
accept="image/png,image/jpeg,image/webp" @change="handleValueCanvasUpload" />
|
|
|
<div v-if="form.valuePropositionCanvas.url" class="value-canvas-preview">
|
|
|
<el-image :src="form.valuePropositionCanvas.url"
|
|
|
:preview-src-list="[form.valuePropositionCanvas.url]" fit="cover" preview-teleported />
|
|
|
<div><strong>{{ form.valuePropositionCanvas.name || '产品价值主张画布'
|
|
|
}}</strong><span>点击缩略图可放大预览</span><button type="button" class="text-button"
|
|
|
@click="clearValueCanvas">移除图片</button></div>
|
|
|
</div>
|
|
|
<button v-else type="button" class="canvas-upload-trigger" :disabled="uploadingValueCanvas"
|
|
|
@click="openValueCanvasPicker">
|
|
|
<el-icon>
|
|
|
<UploadFilled />
|
|
|
</el-icon>{{ uploadingValueCanvas ? '正在上传...' : '上传产品价值主张画布' }}
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
<p v-if="valueCanvasValidationMessage" class="core-pain-validation-message" role="status">{{
|
|
|
valueCanvasValidationMessage }}</p>
|
|
|
</section>
|
|
|
|
|
|
<section v-show="isSectionVisible(3)" class="step-card">
|
|
|
<div v-if="activeStepName" class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step {{ String(form.activeStep).padStart(2, '0') }}</p>
|
|
|
<h3>{{ activeStepName }}</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="positioning-intro">
|
|
|
<p>根据前面实训分析,使用经典定位陈述格式撰写简洁有力的产品价值定位陈述。</p>
|
|
|
<p>经典定位陈述格式:对于【目标市场/用户】,我们的【产品名称】是一个【产品类别】,它能提供【核心价值/关键收益】,因为它具有【主要差异化理由】。</p>
|
|
|
</div>
|
|
|
<article v-for="(statement, index) in form.positioningStatements" :key="statement.id"
|
|
|
class="positioning-version-card">
|
|
|
<header><span>版本{{ numberToChinese(index + 1) }}</span><small>至少设计两个不同目标用户版本</small></header>
|
|
|
<p class="statement-preview">对于 <strong>{{ statement.targetUser || '【目标市场/用户】' }}</strong>,我们的
|
|
|
<strong>{{
|
|
|
statement.productName || '【产品名称】' }}</strong> 是一个 <strong>{{ statement.productCategory
|
|
|
|| '【产品类别】'
|
|
|
}}</strong>,它能提供 <strong>{{ statement.coreValue || '【核心价值/关键收益】' }}</strong>,因为它具有
|
|
|
<strong>{{
|
|
|
statement.differentiationReason || '【主要差异化理由】' }}</strong>。
|
|
|
</p>
|
|
|
<div class="positioning-fields">
|
|
|
<label><span>目标市场/用户</span><input v-model="statement.targetUser"
|
|
|
:aria-label="`版本${index + 1}目标市场或用户`" placeholder="填写目标市场/用户" /></label>
|
|
|
<label><span>产品名称</span><input v-model="statement.productName"
|
|
|
:aria-label="`版本${index + 1}产品名称`" placeholder="填写产品名称" /></label>
|
|
|
<label><span>产品类别</span><input v-model="statement.productCategory"
|
|
|
:aria-label="`版本${index + 1}产品类别`" placeholder="填写产品类别" /></label>
|
|
|
<label><span>核心价值/关键收益</span><input v-model="statement.coreValue"
|
|
|
:aria-label="`版本${index + 1}核心价值或关键收益`" placeholder="填写核心价值/关键收益" /></label>
|
|
|
<label class="positioning-fields__wide"><span>主要差异化理由</span><textarea
|
|
|
v-model="statement.differentiationReason" :aria-label="`版本${index + 1}主要差异化理由`"
|
|
|
placeholder="填写主要差异化理由" /></label>
|
|
|
</div>
|
|
|
</article>
|
|
|
<p v-if="positioningValidationMessage" class="core-pain-validation-message" role="status">{{
|
|
|
positioningValidationMessage }}</p>
|
|
|
</section>
|
|
|
|
|
|
<section v-show="isSectionVisible(4)" class="step-card">
|
|
|
<div v-if="activeStepName" class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step {{ String(form.activeStep).padStart(2, '0') }}</p>
|
|
|
<h3>{{ activeStepName }}</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="mrd-intro">
|
|
|
基于前述步骤的分析成果,撰写一份完整的产品市场需求说明书(MRD)。MRD
|
|
|
文档全面分析产品的市场机会、用户需求及竞争格局,明确产品核心定位与功能方向,为后续产品开发、资源投入及战略决策提供数据支撑与逻辑依据。
|
|
|
</div>
|
|
|
<section v-for="section in mrdSections" :key="section.key" class="mrd-section">
|
|
|
<div class="section-heading"><span>{{ chineseNumerals[section.order - 1] }}</span>
|
|
|
<div>
|
|
|
<h3>{{ section.title }}</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="mrd-table-wrap">
|
|
|
<table class="mrd-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>序号</th>
|
|
|
<th v-if="section.hasModule">模块</th>
|
|
|
<th>标题</th>
|
|
|
<th>内容</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="(row, index) in form.mrd[section.key]" :key="row.id">
|
|
|
<td>{{ index + 1 }}</td>
|
|
|
<td v-if="section.hasModule" class="mrd-module">{{ row.module }}</td>
|
|
|
<td class="mrd-row-title">{{ row.title }}</td>
|
|
|
<td><textarea v-model="row.content"
|
|
|
:aria-label="`${section.title}${row.title}内容`"
|
|
|
:placeholder="`填写${row.title}内容`" /></td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
<section class="mrd-positioning-section">
|
|
|
<div class="section-heading"><span>06</span>
|
|
|
<div>
|
|
|
<h3>产品定位</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<textarea v-model="form.mrd.productPositioning" aria-label="产品定位"
|
|
|
placeholder="综合市场、用户、竞品和需求分析,填写产品定位" />
|
|
|
</section>
|
|
|
<p v-if="mrdValidationMessage" class="core-pain-validation-message" role="status">{{
|
|
|
mrdValidationMessage }}</p>
|
|
|
</section>
|
|
|
|
|
|
<section v-show="hasConfiguredSteps && form.activeStep > 4" class="step-card completion-card">
|
|
|
<div class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step {{ String(form.activeStep).padStart(2, '0') }}</p>
|
|
|
<h3>{{ activeStepName }}</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<p class="step-desc">请检查已填写的调研、定位画布、价值主张和定位陈述,再保存或提交实训成果。</p>
|
|
|
</section>
|
|
|
|
|
|
<div v-if="hasConfiguredSteps" class="step-navigation">
|
|
|
<div class="step-nav-left">
|
|
|
<button v-if="form.activeStep > 1" type="button" class="btn-nav btn-ghost"
|
|
|
@click="goPreviousStep">
|
|
|
<el-icon>
|
|
|
<ArrowLeft />
|
|
|
</el-icon>
|
|
|
上一步
|
|
|
</button>
|
|
|
<button type="button" class="btn-nav btn-save" :disabled="saving" @click="saveCurrentProgress">
|
|
|
<el-icon>
|
|
|
<CircleCheck />
|
|
|
</el-icon>
|
|
|
保存
|
|
|
</button>
|
|
|
</div>
|
|
|
<div class="step-nav-right">
|
|
|
<span class="step-progress">Step {{ String(form.activeStep).padStart(2, "0") }} / {{
|
|
|
String(configuredSteps.length).padStart(2, "0") }}</span>
|
|
|
<button v-if="form.activeStep < configuredSteps.length" type="button"
|
|
|
class="btn-nav btn-primary" :disabled="saving" @click="goNextStep">
|
|
|
下一步
|
|
|
<el-icon>
|
|
|
<ArrowRight />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
</main>
|
|
|
|
|
|
<TrainingAiSidebar study-tip="当前任务建议:先明确目标人群和核心痛点,再把定位画布、价值主张画布和定位陈述串成一条完整逻辑。" reference-title="定位分析参考"
|
|
|
reference-text="· 市场调研:竞品、用户评价、需求痛点<br />· 定位画布:用户、场景、差异化机会<br />· 价值主张:客户任务、痛点、收益与解决方案<br />· 定位陈述:目标用户 + 核心价值 + 差异化理由"
|
|
|
:progress-items="progressItems" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { computed, getCurrentInstance, onMounted, ref } from "vue"
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, Download, Operation, UploadFilled } from "@element-plus/icons-vue"
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"
|
|
|
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask"
|
|
|
import { checkProductValuePositioningStepOne, checkProductValuePositioningStepTwo, checkProductValuePositioningStepThree, checkProductValuePositioningStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap"
|
|
|
import { isStudentDemo } from "@/utils/studentDemo"
|
|
|
|
|
|
const TASK_KEY = "product-value-positioning"
|
|
|
const { proxy } = getCurrentInstance()
|
|
|
const saving = ref(false)
|
|
|
const checkingCorePain = ref(false)
|
|
|
const corePainValidationMessage = ref("")
|
|
|
const checkingValueCanvas = ref(false)
|
|
|
const valueCanvasValidationMessage = ref("")
|
|
|
const checkingPositioning = ref(false)
|
|
|
const positioningValidationMessage = ref("")
|
|
|
const checkingMrd = ref(false)
|
|
|
const mrdValidationMessage = ref("")
|
|
|
const uploadingValueCanvas = ref(false)
|
|
|
const valueCanvasInput = ref(null)
|
|
|
const taskConfig = ref(null)
|
|
|
|
|
|
const corePainTypes = ["核心痛点1", "核心痛点2", "核心痛点3", "弱需求", "伪需求"]
|
|
|
const positioningStatementFields = ["targetUser", "productName", "productCategory", "coreValue", "differentiationReason"]
|
|
|
const mrdSections = [
|
|
|
{ key: "marketInsights", order: 1, title: "市场问题与机会", rows: [{ title: "市场痛点" }, { title: "市场机会" }, { title: "发展趋势" }] },
|
|
|
{ key: "marketOverview", order: 2, title: "市场概述", rows: [{ title: "目标市场" }, { title: "市场特征" }] },
|
|
|
{ key: "userAnalysis", order: 3, title: "用户与购买者分析", rows: [{ title: "目标用户画像" }, { title: "用户场景与需求" }, { title: "购买者与决策者" }] },
|
|
|
{ key: "competitorAnalysis", order: 4, title: "竞品分析", rows: [{ title: "竞品对比分析" }, { title: "差异化机会" }] },
|
|
|
{ key: "productRequirements", order: 5, title: "产品需求说明", hasModule: true, rows: [{ module: "功能需求", title: "核心功能" }, { module: "功能需求", title: "辅助功能" }, { module: "非功能需求", title: "性能需求" }, { module: "非功能需求", title: "安全需求" }, { module: "非功能需求", title: "兼容性" }] },
|
|
|
]
|
|
|
const chineseNumerals = ["01", "02", "03", "04", "05", "06"]
|
|
|
|
|
|
function createPositioningStatement(id) {
|
|
|
return { id, targetUser: "", productName: "", productCategory: "", coreValue: "", differentiationReason: "" }
|
|
|
}
|
|
|
|
|
|
function createMrd() {
|
|
|
const mrd = { productPositioning: "" }
|
|
|
mrdSections.forEach((section) => {
|
|
|
mrd[section.key] = section.rows.map((row, index) => ({ id: index + 1, module: row.module || "", title: row.title, content: "" }))
|
|
|
})
|
|
|
return mrd
|
|
|
}
|
|
|
|
|
|
const defaultRequirement =
|
|
|
"产品价值主张及定位需要在市场调研和用户洞察基础上,明确目标用户、核心价值、差异化优势和定位表达。"
|
|
|
const defaultBackground =
|
|
|
"某智能家居公司计划推出一款面向家庭用户的 AI 智能健身镜,希望通过该产品进入家庭健身市场。作为产品经理,需要基于市场调研与用户洞察完成系统化定位。"
|
|
|
const defaultGoals = [
|
|
|
"理解产品定位的概念、作用与核心要素。",
|
|
|
"掌握定位画布与价值主张画布的系统化工具。",
|
|
|
"运用产品定位方法完成一次 AI 智能健身镜的定位设计。",
|
|
|
"撰写清晰、有力的产品定位陈述。",
|
|
|
]
|
|
|
|
|
|
function createForm() {
|
|
|
return {
|
|
|
activeStep: 1,
|
|
|
corePainRows: corePainTypes.map((type, index) => ({ id: index + 1, type, description: "", scene: "" })),
|
|
|
research: { marketAnalysis: "", userResearch: "", persona: "" },
|
|
|
positionCanvas: { targetUsers: "", needsAndPains: "", competitorOpportunity: "" },
|
|
|
customerProfile: { jobs: "", pains: "", gains: "" },
|
|
|
valueMap: { products: "", painRelievers: "", gainCreators: "" },
|
|
|
valuePropositionCanvas: { url: "", name: "" },
|
|
|
positioningStatements: [createPositioningStatement(1), createPositioningStatement(2)],
|
|
|
mrd: createMrd(),
|
|
|
valueCanvas: { customerJobs: "", pains: "", gains: "", products: "", painRelievers: "", gainCreators: "" },
|
|
|
statement: { targetUsers: "", valueProposition: "", differentiation: "", positioning: "" },
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const form = ref(createForm())
|
|
|
const taskTitle = computed(() => taskConfig.value?.taskName || "产品价值主张及定位")
|
|
|
const briefOpen = ref(true)
|
|
|
const taskBackground = computed(() => taskConfig.value?.background || defaultBackground)
|
|
|
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement)
|
|
|
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives, defaultGoals))
|
|
|
const configuredSteps = computed(() => parseTrainingArray(taskConfig.value?.steps))
|
|
|
const hasConfiguredSteps = computed(() => configuredSteps.value.length > 0)
|
|
|
const activeStepName = computed(() => configuredSteps.value[form.value.activeStep - 1] || "")
|
|
|
const progressItems = computed(() =>
|
|
|
configuredSteps.value.map((step, index) => ({
|
|
|
text: step,
|
|
|
status: index + 1 < form.value.activeStep ? "done" : index + 1 === form.value.activeStep ? "doing" : "",
|
|
|
}))
|
|
|
)
|
|
|
|
|
|
onMounted(async () => {
|
|
|
await loadTaskConfig()
|
|
|
await loadSavedAnswer()
|
|
|
})
|
|
|
|
|
|
async function loadTaskConfig() {
|
|
|
try {
|
|
|
const res = await getTrainingTaskByKey(TASK_KEY)
|
|
|
taskConfig.value = res?.data || null
|
|
|
form.value.activeStep = normalizeActiveStep(form.value.activeStep)
|
|
|
} catch (error) {
|
|
|
taskConfig.value = null
|
|
|
form.value.activeStep = 1
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function loadSavedAnswer() {
|
|
|
try {
|
|
|
const res = await getStudentTrainingAnswer(TASK_KEY)
|
|
|
if (res?.data?.step1Answer) applySavedAnswer(res.data.step1Answer)
|
|
|
} catch (error) {
|
|
|
// Keep the local form when no historical answer is available.
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function normalizeActiveStep(value) {
|
|
|
if (!configuredSteps.value.length) return 1
|
|
|
const step = Number(value) || 1
|
|
|
return Math.min(Math.max(step, 1), configuredSteps.value.length)
|
|
|
}
|
|
|
|
|
|
async function setCurrentStep(step) {
|
|
|
const targetStep = normalizeActiveStep(step)
|
|
|
if (targetStep > form.value.activeStep) {
|
|
|
if (form.value.activeStep === 1 && !(await ensureCorePainValid())) return
|
|
|
if (form.value.activeStep === 2 && !(await ensureValueCanvasValid())) return
|
|
|
if (form.value.activeStep === 3 && !(await ensurePositioningValid())) return
|
|
|
if (form.value.activeStep === 4 && !(await ensureMrdValid())) return
|
|
|
}
|
|
|
form.value.activeStep = targetStep
|
|
|
}
|
|
|
|
|
|
function isSectionVisible(section) {
|
|
|
return !hasConfiguredSteps.value || form.value.activeStep === section
|
|
|
}
|
|
|
|
|
|
async function goPreviousStep() {
|
|
|
await setCurrentStep(form.value.activeStep - 1)
|
|
|
}
|
|
|
|
|
|
async function goNextStep() {
|
|
|
await setCurrentStep(form.value.activeStep + 1)
|
|
|
}
|
|
|
|
|
|
function applySavedAnswer(value) {
|
|
|
try {
|
|
|
const parsed = JSON.parse(value)
|
|
|
if (!parsed || typeof parsed !== "object") return
|
|
|
form.value = normalizeForm(parsed)
|
|
|
} catch (error) {
|
|
|
// Ignore malformed historical answers.
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function normalizeForm(value = {}) {
|
|
|
const base = createForm()
|
|
|
return {
|
|
|
...base,
|
|
|
...value,
|
|
|
activeStep: normalizeActiveStep(value.activeStep),
|
|
|
corePainRows: normalizeCorePainRows(value.corePainRows),
|
|
|
research: { ...base.research, ...(value.research || {}) },
|
|
|
positionCanvas: { ...base.positionCanvas, ...(value.positionCanvas || {}) },
|
|
|
customerProfile: { ...base.customerProfile, ...(value.customerProfile || {}) },
|
|
|
valueMap: { ...base.valueMap, ...(value.valueMap || {}) },
|
|
|
valuePropositionCanvas: { ...base.valuePropositionCanvas, ...(value.valuePropositionCanvas || {}) },
|
|
|
positioningStatements: normalizePositioningStatements(value.positioningStatements),
|
|
|
mrd: normalizeMrd(value.mrd),
|
|
|
valueCanvas: { ...base.valueCanvas, ...(value.valueCanvas || {}) },
|
|
|
statement: { ...base.statement, ...(value.statement || {}) },
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function normalizeCorePainRows(rows = []) {
|
|
|
const savedRows = Array.isArray(rows) ? rows : []
|
|
|
return corePainTypes.map((type, index) => ({
|
|
|
id: index + 1,
|
|
|
type,
|
|
|
description: String(savedRows[index]?.description || ""),
|
|
|
scene: String(savedRows[index]?.scene || ""),
|
|
|
}))
|
|
|
}
|
|
|
|
|
|
function normalizePositioningStatements(statements = []) {
|
|
|
const savedStatements = Array.isArray(statements) ? statements : []
|
|
|
return [1, 2].map((id, index) => {
|
|
|
const saved = savedStatements[index] || {}
|
|
|
return positioningStatementFields.reduce((result, field) => ({ ...result, [field]: String(saved[field] || "") }), { id })
|
|
|
})
|
|
|
}
|
|
|
|
|
|
function normalizeMrd(mrd = {}) {
|
|
|
const base = createMrd()
|
|
|
mrdSections.forEach((section) => {
|
|
|
const savedRows = Array.isArray(mrd?.[section.key]) ? mrd[section.key] : []
|
|
|
base[section.key] = section.rows.map((row, index) => ({
|
|
|
id: index + 1,
|
|
|
module: row.module || "",
|
|
|
title: row.title,
|
|
|
content: String(savedRows[index]?.content || ""),
|
|
|
}))
|
|
|
})
|
|
|
base.productPositioning = String(mrd?.productPositioning || "")
|
|
|
return base
|
|
|
}
|
|
|
|
|
|
async function ensureCorePainValid() {
|
|
|
if (checkingCorePain.value) return false
|
|
|
checkingCorePain.value = true
|
|
|
try {
|
|
|
const response = await checkProductValuePositioningStepOne({ rows: form.value.corePainRows })
|
|
|
const result = response?.data || {}
|
|
|
corePainValidationMessage.value = result.message || (result.valid ? "核心痛点分类填写完整。" : "请完成 3 个核心痛点、弱需求和伪需求的描述与场景。")
|
|
|
return result.valid === true
|
|
|
} catch (error) {
|
|
|
corePainValidationMessage.value = error?.message || "核心痛点校验失败,请稍后重试。"
|
|
|
return false
|
|
|
} finally {
|
|
|
checkingCorePain.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function ensureValueCanvasValid() {
|
|
|
if (checkingValueCanvas.value) return false
|
|
|
checkingValueCanvas.value = true
|
|
|
try {
|
|
|
const response = await checkProductValuePositioningStepTwo({
|
|
|
customerProfile: form.value.customerProfile,
|
|
|
valueMap: form.value.valueMap,
|
|
|
canvasUrl: form.value.valuePropositionCanvas.url,
|
|
|
})
|
|
|
const result = response?.data || {}
|
|
|
valueCanvasValidationMessage.value = result.message || (result.valid ? "价值主张画布填写完整。" : "请完整填写两张画布表并上传产品价值主张画布。")
|
|
|
return result.valid === true
|
|
|
} catch (error) {
|
|
|
valueCanvasValidationMessage.value = error?.message || "价值主张画布校验失败,请稍后重试。"
|
|
|
return false
|
|
|
} finally { checkingValueCanvas.value = false }
|
|
|
}
|
|
|
|
|
|
async function ensurePositioningValid() {
|
|
|
if (checkingPositioning.value) return false
|
|
|
checkingPositioning.value = true
|
|
|
try {
|
|
|
const response = await checkProductValuePositioningStepThree({ statements: form.value.positioningStatements })
|
|
|
const result = response?.data || {}
|
|
|
positioningValidationMessage.value = result.message || (result.valid ? "两版定位陈述填写完整。" : "请完整填写两个产品价值定位陈述版本。")
|
|
|
return result.valid === true
|
|
|
} catch (error) {
|
|
|
positioningValidationMessage.value = error?.message || "定位陈述校验失败,请稍后重试。"
|
|
|
return false
|
|
|
} finally { checkingPositioning.value = false }
|
|
|
}
|
|
|
|
|
|
async function ensureMrdValid() {
|
|
|
if (checkingMrd.value) return false
|
|
|
checkingMrd.value = true
|
|
|
try {
|
|
|
const response = await checkProductValuePositioningStepFour(form.value.mrd)
|
|
|
const result = response?.data || {}
|
|
|
mrdValidationMessage.value = result.message || (result.valid ? "MRD 文档填写完整。" : "请完整填写 MRD 的五组分析内容和产品定位。")
|
|
|
return result.valid === true
|
|
|
} catch (error) {
|
|
|
mrdValidationMessage.value = error?.message || "MRD 校验失败,请稍后重试。"
|
|
|
return false
|
|
|
} finally { checkingMrd.value = false }
|
|
|
}
|
|
|
|
|
|
function buildOutcome() {
|
|
|
return {
|
|
|
taskKey: TASK_KEY,
|
|
|
taskId: taskConfig.value?.id || "",
|
|
|
taskName: taskTitle.value,
|
|
|
currentStep: form.value.activeStep,
|
|
|
configuredSteps: configuredSteps.value,
|
|
|
corePainRows: form.value.corePainRows,
|
|
|
research: form.value.research,
|
|
|
positionCanvas: form.value.positionCanvas,
|
|
|
customerProfile: form.value.customerProfile,
|
|
|
valueMap: form.value.valueMap,
|
|
|
valuePropositionCanvas: form.value.valuePropositionCanvas,
|
|
|
positioningStatements: form.value.positioningStatements,
|
|
|
mrd: form.value.mrd,
|
|
|
valueCanvas: form.value.valueCanvas,
|
|
|
statement: form.value.statement,
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function buildAnswerPayload(status) {
|
|
|
return {
|
|
|
saveAction: status === "SUBMITTED" ? "SUBMIT" : status === "RESET" ? "RESET" : "SAVE",
|
|
|
currentStep: form.value.activeStep,
|
|
|
step1Answer: JSON.stringify(buildOutcome()),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function persist(status = "IN_PROGRESS") {
|
|
|
if (saving.value) return
|
|
|
if (status !== "RESET") {
|
|
|
if (form.value.activeStep === 1 && !(await ensureCorePainValid())) return false
|
|
|
if (form.value.activeStep === 2 && !(await ensureValueCanvasValid())) return false
|
|
|
if (form.value.activeStep === 3 && !(await ensurePositioningValid())) return false
|
|
|
if (form.value.activeStep === 4 && !(await ensureMrdValid())) return false
|
|
|
}
|
|
|
saving.value = true
|
|
|
try {
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status))
|
|
|
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功")
|
|
|
return true
|
|
|
} finally {
|
|
|
saving.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function saveCurrentProgress() {
|
|
|
return persist("IN_PROGRESS")
|
|
|
}
|
|
|
|
|
|
function submitTask() {
|
|
|
return persist("SUBMITTED")
|
|
|
}
|
|
|
|
|
|
async function resetTraining() {
|
|
|
clearValueCanvas()
|
|
|
form.value = createForm()
|
|
|
await persist("RESET")
|
|
|
}
|
|
|
|
|
|
function exportOutcome() {
|
|
|
const blob = new Blob([JSON.stringify(buildOutcome(), null, 2)], { type: "application/json;charset=utf-8" })
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
const link = document.createElement("a")
|
|
|
link.href = url
|
|
|
link.download = `${taskTitle.value}-实训成果.json`
|
|
|
link.click()
|
|
|
URL.revokeObjectURL(url)
|
|
|
}
|
|
|
|
|
|
function numberToChinese(number) { return ["一", "二", "三", "四"][number - 1] || String(number) }
|
|
|
|
|
|
const customerProfileExportContent = computed(() => `# 用户档案(Customer Profile)\n\n## 用户目标(Jobs)\n${form.value.customerProfile.jobs}\n\n## 痛点(Pains)\n${form.value.customerProfile.pains}\n\n## 收益期望(Gains)\n${form.value.customerProfile.gains}\n`)
|
|
|
const valueMapExportContent = computed(() => `# 产品价值地图(Value Map)\n\n## 产品与服务\n${form.value.valueMap.products}\n\n## 痛点缓释器(Pain Relievers)\n${form.value.valueMap.painRelievers}\n\n## 增益创造器(Gain Creators)\n${form.value.valueMap.gainCreators}\n`)
|
|
|
|
|
|
function downloadText(content, fileName) {
|
|
|
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" })
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
const link = document.createElement("a")
|
|
|
link.href = url
|
|
|
link.download = fileName
|
|
|
link.click()
|
|
|
URL.revokeObjectURL(url)
|
|
|
}
|
|
|
|
|
|
function exportCanvasSection(name, content) { downloadText(content, `${taskTitle.value}-${name}.md`) }
|
|
|
|
|
|
function downloadBuiltInFile(url, fileName) {
|
|
|
const link = document.createElement("a")
|
|
|
link.href = url
|
|
|
link.download = fileName
|
|
|
document.body.appendChild(link)
|
|
|
link.click()
|
|
|
document.body.removeChild(link)
|
|
|
}
|
|
|
|
|
|
function openValueCanvasPicker() { valueCanvasInput.value?.click() }
|
|
|
|
|
|
function clearValueCanvas() {
|
|
|
if (form.value?.valuePropositionCanvas?.url?.startsWith("blob:")) URL.revokeObjectURL(form.value.valuePropositionCanvas.url)
|
|
|
if (form.value) form.value.valuePropositionCanvas = { url: "", name: "" }
|
|
|
}
|
|
|
|
|
|
async function handleValueCanvasUpload(event) {
|
|
|
const file = event.target?.files?.[0]
|
|
|
if (!file) return
|
|
|
if (!["image/jpeg", "image/png", "image/webp"].includes(file.type)) {
|
|
|
proxy?.$modal?.msgError?.("请上传 PNG、JPG 或 WEBP 格式的图片"); event.target.value = ""; return
|
|
|
}
|
|
|
if (file.size > 5 * 1024 * 1024) { proxy?.$modal?.msgError?.("图片大小不能超过 5MB"); event.target.value = ""; return }
|
|
|
uploadingValueCanvas.value = true
|
|
|
try {
|
|
|
if (isStudentDemo()) { clearValueCanvas(); form.value.valuePropositionCanvas = { url: URL.createObjectURL(file), name: file.name } }
|
|
|
else {
|
|
|
const formData = new FormData(); formData.append("file", file)
|
|
|
const result = await uploadStudentTrainingFile(formData)
|
|
|
const url = result?.url || result?.data?.url
|
|
|
if (!url) throw new Error("图片上传失败,请重试")
|
|
|
form.value.valuePropositionCanvas = { url, name: file.name }
|
|
|
}
|
|
|
valueCanvasValidationMessage.value = ""
|
|
|
proxy?.$modal?.msgSuccess?.("产品价值主张画布上传成功")
|
|
|
} catch (error) { proxy?.$modal?.msgError?.(error?.message || "图片上传失败,请重试") }
|
|
|
finally { uploadingValueCanvas.value = false; event.target.value = "" }
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
/* ===== 页面级令牌(教育蓝极简主题) ===== */
|
|
|
.product-positioning-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;
|
|
|
}
|
|
|
|
|
|
/* 核心容器:透明,对齐 .industry-core / .analysis-core */
|
|
|
.product-positioning-core {
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
align-content: start;
|
|
|
min-width: 0;
|
|
|
border: 0;
|
|
|
background: transparent;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
/* 任务头:独立卡片 + 左侧教育蓝竖条 */
|
|
|
.task-header {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 16px;
|
|
|
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;
|
|
|
flex: 1 1 auto;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.task-title {
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-display);
|
|
|
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;
|
|
|
padding-top: 8px;
|
|
|
border-top: 1px solid var(--student-panel-border);
|
|
|
|
|
|
b {
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
ul {
|
|
|
display: grid;
|
|
|
gap: 4px;
|
|
|
margin: 0;
|
|
|
padding-left: 18px;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* task-action-strip:TrainingTaskToolbar 横条 */
|
|
|
.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-card / step-card 层:透明容器 */
|
|
|
.task-card,
|
|
|
.step-card {
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
/* 顶部任务工具栏(保留 DOM 结构) */
|
|
|
.workbench-head {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 18px;
|
|
|
margin-bottom: 18px;
|
|
|
padding-bottom: 0;
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.workbench-head--actions-only {
|
|
|
justify-content: flex-end;
|
|
|
margin-bottom: 18px;
|
|
|
padding-bottom: 0;
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.workbench-actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: flex-end;
|
|
|
margin-left: auto;
|
|
|
gap: 10px;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.outline-action {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 8px;
|
|
|
height: 36px;
|
|
|
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-size: var(--student-fs-body);
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
text-shadow: none;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.outline-action:hover {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
/* step-title-row + section-eyebrow */
|
|
|
.step-title-row {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: flex-start;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.step-title-row h3 {
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
.section-eyebrow {
|
|
|
margin: 0 0 6px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-weight: 700;
|
|
|
font-size: var(--student-fs-caption);
|
|
|
letter-spacing: 0.12em;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
.step-desc {
|
|
|
margin: 14px 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
/* ===== 引导区(intro) ===== */
|
|
|
.core-pain-intro,
|
|
|
.value-proposition-intro,
|
|
|
.positioning-intro,
|
|
|
.mrd-intro {
|
|
|
margin: 14px 0 12px;
|
|
|
padding: 12px 14px;
|
|
|
border-left: 3px solid var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.core-pain-intro p,
|
|
|
.positioning-intro p {
|
|
|
margin: 0 0 6px;
|
|
|
color: var(--student-text-secondary);
|
|
|
}
|
|
|
|
|
|
.core-pain-intro p+p,
|
|
|
.positioning-intro p+p {
|
|
|
margin-top: 6px;
|
|
|
}
|
|
|
|
|
|
/* ===== 表格 ===== */
|
|
|
.core-pain-table-wrap,
|
|
|
.value-canvas-table-wrap,
|
|
|
.mrd-table-wrap {
|
|
|
overflow-x: auto;
|
|
|
}
|
|
|
|
|
|
.core-pain-table,
|
|
|
.value-canvas-table,
|
|
|
.mrd-table {
|
|
|
width: 100%;
|
|
|
border-collapse: collapse;
|
|
|
}
|
|
|
|
|
|
.core-pain-table {
|
|
|
min-width: 820px;
|
|
|
table-layout: fixed;
|
|
|
}
|
|
|
|
|
|
.value-canvas-table {
|
|
|
min-width: 820px;
|
|
|
table-layout: fixed;
|
|
|
}
|
|
|
|
|
|
.mrd-table {
|
|
|
min-width: 760px;
|
|
|
table-layout: fixed;
|
|
|
}
|
|
|
|
|
|
.core-pain-table th,
|
|
|
.core-pain-table td,
|
|
|
.value-canvas-table th,
|
|
|
.value-canvas-table td,
|
|
|
.mrd-table th,
|
|
|
.mrd-table td {
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
|
|
|
.core-pain-table th,
|
|
|
.value-canvas-table th,
|
|
|
.mrd-table th {
|
|
|
padding: 10px 12px;
|
|
|
color: var(--student-text);
|
|
|
background: #fafbfc;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 800;
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
.core-pain-table td,
|
|
|
.value-canvas-table td,
|
|
|
.mrd-table td {
|
|
|
padding: 8px 10px;
|
|
|
color: var(--student-text);
|
|
|
background: transparent;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.core-pain-table th:nth-child(1),
|
|
|
.core-pain-table td:nth-child(1) {
|
|
|
width: 7%;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.core-pain-table th:nth-child(2),
|
|
|
.core-pain-table td:nth-child(2) {
|
|
|
width: 15%;
|
|
|
text-align: center;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.core-pain-table th:nth-child(3),
|
|
|
.core-pain-table td:nth-child(3) {
|
|
|
width: 32%;
|
|
|
}
|
|
|
|
|
|
.pain-type-cell {
|
|
|
color: var(--student-text);
|
|
|
font-weight: 700;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
.value-canvas-table th:nth-child(1),
|
|
|
.value-canvas-table td:nth-child(1) {
|
|
|
width: 19%;
|
|
|
text-align: center;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.value-canvas-table th:nth-child(2),
|
|
|
.value-canvas-table td:nth-child(2) {
|
|
|
width: 24%;
|
|
|
}
|
|
|
|
|
|
.value-canvas-table td:nth-child(2) {
|
|
|
color: var(--student-text-secondary);
|
|
|
}
|
|
|
|
|
|
.mrd-table th:nth-child(1),
|
|
|
.mrd-table td:nth-child(1) {
|
|
|
width: 8%;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.mrd-table th:nth-child(2),
|
|
|
.mrd-table td:nth-child(2) {
|
|
|
width: 17%;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.mrd-row-title {
|
|
|
color: var(--student-text);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.mrd-module {
|
|
|
color: var(--student-text-secondary);
|
|
|
}
|
|
|
|
|
|
/* 表格内 textarea:透明底 */
|
|
|
.core-pain-table textarea,
|
|
|
.value-canvas-table textarea,
|
|
|
.mrd-table textarea,
|
|
|
.mrd-positioning-section textarea {
|
|
|
box-sizing: border-box;
|
|
|
display: block;
|
|
|
width: 100%;
|
|
|
min-height: 72px;
|
|
|
resize: vertical;
|
|
|
border: 0;
|
|
|
padding: 8px 10px;
|
|
|
color: var(--student-text);
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.core-pain-table textarea:focus,
|
|
|
.value-canvas-table textarea:focus,
|
|
|
.mrd-table textarea:focus,
|
|
|
.mrd-positioning-section textarea:focus {
|
|
|
outline: 2px solid rgba(59, 130, 246, 0.15);
|
|
|
outline-offset: -2px;
|
|
|
}
|
|
|
|
|
|
/* ===== 画布区 ===== */
|
|
|
.canvas-section-heading {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 16px;
|
|
|
margin: 18px 0 10px;
|
|
|
|
|
|
.section-heading {
|
|
|
flex: 1 1 auto;
|
|
|
min-width: 0;
|
|
|
margin-bottom: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.canvas-section-heading--spaced {
|
|
|
margin-top: 28px;
|
|
|
}
|
|
|
|
|
|
.canvas-section-heading small {
|
|
|
margin-left: 6px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
/* 导出 / 上传按钮:白底灰边(对齐 .outline-action 风格) */
|
|
|
.canvas-export-button,
|
|
|
.canvas-upload-trigger {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 7px;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
cursor: pointer;
|
|
|
font: inherit;
|
|
|
font-weight: 700;
|
|
|
text-shadow: none;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.canvas-export-button {
|
|
|
min-width: 92px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
|
|
|
.canvas-upload-trigger {
|
|
|
min-width: 218px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
font-size: var(--student-fs-body);
|
|
|
}
|
|
|
|
|
|
.canvas-export-button:hover,
|
|
|
.canvas-upload-trigger:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
.canvas-upload-trigger:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
}
|
|
|
|
|
|
.canvas-drawing-section {
|
|
|
margin-top: 28px;
|
|
|
padding: 18px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
box-shadow: 0 1px 3px rgba(37, 99, 235, 0.06);
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.canvas-drawing-section .section-heading {
|
|
|
margin-bottom: 18px;
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
.visually-hidden {
|
|
|
position: absolute;
|
|
|
width: 1px;
|
|
|
height: 1px;
|
|
|
overflow: hidden;
|
|
|
clip: rect(0 0 0 0);
|
|
|
white-space: nowrap;
|
|
|
clip-path: inset(50%);
|
|
|
}
|
|
|
|
|
|
.value-canvas-preview {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 14px;
|
|
|
margin: 0 auto;
|
|
|
text-align: left;
|
|
|
|
|
|
:deep(.el-image) {
|
|
|
width: 116px;
|
|
|
height: 76px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
cursor: zoom-in;
|
|
|
}
|
|
|
|
|
|
div {
|
|
|
display: grid;
|
|
|
gap: 5px;
|
|
|
}
|
|
|
|
|
|
strong {
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
span {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.text-button {
|
|
|
width: fit-content;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
color: var(--student-accent);
|
|
|
background: transparent;
|
|
|
cursor: pointer;
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-caption);
|
|
|
text-decoration: underline;
|
|
|
}
|
|
|
|
|
|
|
|
|
/* ===== 定位陈述卡片 ===== */
|
|
|
.positioning-version-card {
|
|
|
overflow: hidden;
|
|
|
margin-top: 16px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
box-shadow: 0 1px 3px rgba(37, 99, 235, 0.06);
|
|
|
transition: border-color 0.16s ease, box-shadow 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.positioning-version-card:hover {
|
|
|
border-color: var(--student-accent);
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.08);
|
|
|
}
|
|
|
|
|
|
.positioning-version-card header {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 14px;
|
|
|
padding: 10px 16px;
|
|
|
border-bottom: 1px solid var(--student-panel-border);
|
|
|
background: var(--student-accent-soft);
|
|
|
}
|
|
|
|
|
|
.positioning-version-card header span {
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h2);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.positioning-version-card header small {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
}
|
|
|
|
|
|
.statement-preview {
|
|
|
margin: 0;
|
|
|
padding: 15px 16px;
|
|
|
color: var(--student-text-secondary);
|
|
|
background: #ffffff;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.statement-preview strong {
|
|
|
color: var(--student-text);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.positioning-fields {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
gap: 12px;
|
|
|
padding: 16px;
|
|
|
}
|
|
|
|
|
|
.positioning-fields label {
|
|
|
display: grid;
|
|
|
gap: 6px;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
.positioning-fields label span {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.positioning-fields input,
|
|
|
.positioning-fields textarea {
|
|
|
width: 100%;
|
|
|
padding: 10px 11px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
box-sizing: border-box;
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.positioning-fields input:focus,
|
|
|
.positioning-fields textarea:focus {
|
|
|
outline: 2px solid rgba(59, 130, 246, 0.15);
|
|
|
outline-offset: -2px;
|
|
|
}
|
|
|
|
|
|
.positioning-fields textarea {
|
|
|
min-height: 66px;
|
|
|
resize: vertical;
|
|
|
}
|
|
|
|
|
|
.positioning-fields__wide {
|
|
|
grid-column: 1 / -1;
|
|
|
}
|
|
|
|
|
|
/* ===== MRD 区 ===== */
|
|
|
.mrd-section {
|
|
|
margin-top: 24px;
|
|
|
}
|
|
|
|
|
|
.mrd-positioning-section {
|
|
|
margin-top: 25px;
|
|
|
padding: 16px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
box-shadow: 0 1px 3px rgba(37, 99, 235, 0.06);
|
|
|
}
|
|
|
|
|
|
.mrd-positioning-section textarea {
|
|
|
min-height: 120px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 8px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ===== 消息提示 ===== */
|
|
|
.core-pain-validation-message {
|
|
|
margin: 12px 0 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.core-pain-validation-message.success {
|
|
|
color: var(--student-success);
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 底部导航:工具栏式
|
|
|
* ============================================================ */
|
|
|
.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: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
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: 18px;
|
|
|
}
|
|
|
|
|
|
&: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-primary {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid var(--student-accent);
|
|
|
border-radius: 8px;
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.2), 0 2px 8px rgba(37, 99, 235, 0.15);
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
&: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-color: 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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-save {
|
|
|
border-color: #e5e7eb;
|
|
|
color: #4b5563;
|
|
|
background: #f9fafb;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: #d1d5db;
|
|
|
background: #f3f4f6;
|
|
|
transform: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.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-family: var(--student-font-data);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.05em;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
@media (max-width: 1100px) {
|
|
|
.step-nav-right {
|
|
|
margin-left: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
.step-navigation {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
justify-content: center;
|
|
|
}
|
|
|
|
|
|
.btn-nav,
|
|
|
.btn-primary {
|
|
|
width: 100%;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 响应式 ===== */
|
|
|
@media (max-width: 840px) {
|
|
|
.positioning-fields {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.positioning-fields__wide {
|
|
|
grid-column: auto;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
.task-header {
|
|
|
flex-direction: column;
|
|
|
align-items: flex-start;
|
|
|
}
|
|
|
|
|
|
.workbench-head,
|
|
|
.workbench-actions {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
.workbench-actions,
|
|
|
.outline-action {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
/* ===== 步骤内小节标题(蓝底序号,与 diagnosis.vue 统一) ===== */
|
|
|
.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">
|
|
|
/* ============================================================
|
|
|
* 步骤导航:分段连接胶囊(教育蓝主题)
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.product-positioning-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-connector {
|
|
|
display: none !important;
|
|
|
}
|
|
|
|
|
|
.step-tab {
|
|
|
|
|
|
|
|
|
min-width: 0 !important;
|
|
|
display: flex !important;
|
|
|
flex-direction: row !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 8px !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;
|
|
|
letter-spacing: 0 !important;
|
|
|
white-space: nowrap !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-marker {
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
.step-copy {
|
|
|
display: flex !important;
|
|
|
flex-direction: column !important;
|
|
|
gap: 0 !important;
|
|
|
min-width: 0 !important;
|
|
|
}
|
|
|
|
|
|
.step-name {
|
|
|
overflow: hidden !important;
|
|
|
text-overflow: ellipsis !important;
|
|
|
white-space: nowrap !important;
|
|
|
line-height: 1.3 !important;
|
|
|
color: inherit !important;
|
|
|
}
|
|
|
|
|
|
.step-action {
|
|
|
display: none !important;
|
|
|
}
|
|
|
|
|
|
&:hover:not(.active):not(.complete) {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
color: #2563eb !important;
|
|
|
z-index: 1 !important;
|
|
|
|
|
|
.step-marker {
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.complete:not(.active) {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #374151 !important;
|
|
|
|
|
|
.step-marker {
|
|
|
background: rgba(43, 164, 113, 0.12) !important;
|
|
|
color: #16915f !important;
|
|
|
}
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #2ba471 !important;
|
|
|
background: #f0fdf6 !important;
|
|
|
z-index: 1 !important;
|
|
|
|
|
|
.step-marker {
|
|
|
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-marker {
|
|
|
background: #ffffff !important;
|
|
|
color: #2563eb !important;
|
|
|
}
|
|
|
|
|
|
&:hover {
|
|
|
background: #1d4ed8 !important;
|
|
|
border-color: #1d4ed8 !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 窄屏:横向滑动 ===== */
|
|
|
@media (max-width: 900px) {
|
|
|
#app .studentLayout section.app-main:has(.product-positioning-page) .step-indicator {
|
|
|
justify-content: flex-start !important;
|
|
|
overflow-x: auto !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-positioning-page) .step-indicator .step-tab {
|
|
|
flex: 0 0 140px !important;
|
|
|
max-width: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 顶部工具栏:TrainingMaterialButton 资料按钮 → 教育蓝
|
|
|
* ============================================================ */
|
|
|
.product-positioning-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.outline-action>)
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.product-positioning-page) .workbench-actions>button.outline-action {
|
|
|
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(.product-positioning-page) .workbench-actions>button.outline-action: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>
|