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

1658 lines
46 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="feature-conversion-page student-training-shell">
<main class="training-core">
<section class="task-header">
<div class="task-badge">市场洞察与需求分析</div>
<h1 class="task-title">{{ taskTitle }}</h1>
</section>
<TrainingTaskBrief
:background="taskBackground"
:goals="taskGoals"
:requirements="taskRequirement"
:show-material="false"
/>
<section class="task-card">
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
<el-icon><Download /></el-icon>
导出实训成果
</button>
</div>
</div>
<nav v-if="flowSteps.length" class="step-indicator" aria-label="需求转化为功能步骤">
<template v-for="(step, index) in flowSteps" :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 < flowSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section v-show="isSectionVisible(1)" class="step-card conversion-content">
<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>
<label class="field-block product-field">
<span>产品名称:</span>
<input v-model="form.productName" type="text" />
</label>
<h4>需求转化:从“用户语言”到“工程语言”</h4>
<p>用户往往只描述现象或愿望,研发团队需要将其转化为可执行、可验收的技术指标。</p>
<div class="example-block">
<h5>需求转化过程示例:</h5>
<p><strong>1. 场景还原与痛点提炼</strong></p>
<p>不要只听用户说“我想要个能陪孩子的机器人”,而要深入场景。</p>
<p class="indent">用户原话:“孩子在家总看电视,我希望机器人能陪他玩,还能教我孩子学英语。”</p>
<p class="indent">场景洞察:家长(30-40 岁)双职工,下班晚,担心孩子视力及教育缺失;孩子(3-6 岁)注意力短,需要强互动。</p>
<p class="indent">转化动作:将“陪玩”转化为多模态交互能力(语音 + 视觉 + 运动);将“学英语”转化为内容生态接入与个性化推荐算法。</p>
<p><strong>2. 标准化需求定义(PRD 要素)</strong></p>
<p>将洞察转化为开发团队能看懂的文档,必须包含验收标准。</p>
<p class="indent">需求名称:主动式英语启蒙互动。</p>
<p class="indent">功能描述:当摄像头识别到孩子注视屏幕超过 10 分钟,或检测到孩子无聊发呆时,机器人主动发起英语对话或游戏邀请。</p>
<p class="indent">验收标准:</p>
<p class="indent">识别响应时间 &lt; 1 秒;</p>
<p class="indent">对话自然度(MOS 分)&gt; 4.0;</p>
<p class="indent">内容准确率由教育专家评估 &gt; 90%。</p>
</div>
<p class="source-title">原始需求基础材料:</p>
<div class="feedback-grid">
<section>
<h5>家长需求反馈:</h5>
<ol>
<li v-for="item in parentFeedback" :key="item">{{ item }}</li>
</ol>
</section>
<section>
<h5>孩子需求反馈:</h5>
<ol>
<li v-for="item in childFeedback" :key="item">{{ item }}</li>
</ol>
</section>
</div>
<p class="table-tip">请根据本题需求,填写以下需求转化表:</p>
<div class="conversion-table-wrap">
<table class="conversion-table">
<thead>
<tr>
<th>序号</th>
<th>用户原声</th>
<th>场景洞察</th>
<th>转化后的功能定义</th>
<th>功能描述</th>
<th>技术指标/验收标准</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in form.rows" :key="row.id">
<td>{{ index + 1 }}</td>
<td><textarea v-model="row.voice" placeholder="填写用户原话"></textarea></td>
<td><textarea v-model="row.insight" placeholder="填写场景与痛点洞察"></textarea></td>
<td><textarea v-model="row.definition" placeholder="填写转化后的功能定义"></textarea></td>
<td><textarea v-model="row.description" placeholder="填写功能描述"></textarea></td>
<td><textarea v-model="row.acceptance" placeholder="填写技术指标或验收标准"></textarea></td>
</tr>
</tbody>
</table>
</div>
</section>
<section v-show="isSectionVisible(2)" class="step-card conversion-content priority-content">
<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>
<p>
对于智能硬件,建议采用RICE 模型(Reach 覆盖范围、Impact 影响程度、Confidence 置信度、Effort 投入成本)进行量化打分,但必须加一个前置过滤器:合规与安全。并使用KANO模型辅助决策。
</p>
<p>
必备属性(Must-be):如“跌倒检测”、“语音唤醒”。做不到用户会极度不满,做到了是理所应当。优先保障,不计成本投入稳定性。
</p>
<p>
期望属性(Performance):如“续航时间”、“识别准确率”。做得越好用户越满意。在 BOM 成本允许范围内最大化。
</p>
<p>
魅力属性(Delighters):如“情绪识别主动安慰”。没有用户无所谓,有了是惊喜。在核心功能稳定后,作为差异化卖点迭代。
</p>
<p class="source-title">智能硬件需求优先级评估打分表模板:</p>
<div class="conversion-table-wrap">
<table class="priority-template-table">
<thead>
<tr>
<th>字段</th>
<th>说明</th>
<th>填写示例</th>
<th>来源/依据</th>
</tr>
</thead>
<tbody>
<tr v-for="row in priorityTemplateRows" :key="row.field">
<td>{{ row.field }}</td>
<td v-html="row.description"></td>
<td v-html="row.example"></td>
<td>{{ row.source }}</td>
</tr>
</tbody>
</table>
</div>
<p class="table-tip">请根据上一步骤需求转化后的功能定义,针对具体需求进行量化打分,填写以下打分表:</p>
<div class="conversion-table-wrap">
<table class="priority-score-table">
<thead>
<tr>
<th>需求ID</th>
<th>需求名称</th>
<th>需求来源</th>
<th>KANO属性</th>
<th>Reach(覆盖范围)</th>
<th>Impact(影响程度)</th>
<th>Confidence(置信度)</th>
<th>Effort(投入成本)</th>
<th>RICE得分</th>
<th>合规性检查项</th>
</tr>
</thead>
<tbody>
<tr v-for="row in form.scoreRows" :key="row.uid">
<td><textarea v-model="row.requirementId" aria-label="需求ID"></textarea></td>
<td><textarea v-model="row.name" aria-label="需求名称"></textarea></td>
<td><textarea v-model="row.source" aria-label="需求来源"></textarea></td>
<td><textarea v-model="row.kano" aria-label="KANO属性"></textarea></td>
<td><textarea v-model="row.reach" aria-label="Reach覆盖范围"></textarea></td>
<td><textarea v-model="row.impact" aria-label="Impact影响程度"></textarea></td>
<td><textarea v-model="row.confidence" aria-label="Confidence置信度"></textarea></td>
<td><textarea v-model="row.effort" aria-label="Effort投入成本"></textarea></td>
<td><textarea v-model="row.rice" aria-label="RICE得分"></textarea></td>
<td><textarea v-model="row.compliance" aria-label="合规性检查项"></textarea></td>
</tr>
</tbody>
</table>
</div>
</section>
<section v-show="isSectionVisible(3)" class="step-card conversion-content decision-content">
<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>
<p class="decision-intro">
假设项目首期 BOM 成本控制在 300 元以内,研发周期 4 个月。根据上一步骤完成的需求打分表,填写以下内容:
</p>
<label class="decision-field">
<span>P0(首期必做 - 生存线):不做这些,产品无法上市或会被立即退货。</span>
<textarea v-model="form.priorityDecision.p0" aria-label="P0首期必做"></textarea>
</label>
<label class="decision-field">
<span>P1(首期核心 - 竞争力):在成本允许下,最大化“期望属性”,提升性价比感知。</span>
<textarea v-model="form.priorityDecision.p1" aria-label="P1首期核心"></textarea>
</label>
<label class="decision-field">
<span>P2(二期迭代 - 差异化):虽然可以提升用户体验,但是成本较高,或者增加合规风险的。</span>
<textarea v-model="form.priorityDecision.p2" aria-label="P2二期迭代"></textarea>
</label>
<label class="decision-field">
<span>P3(暂缓/砍掉):需求伪命题,偏离核心场景</span>
<textarea v-model="form.priorityDecision.p3" aria-label="P3暂缓砍掉"></textarea>
</label>
</section>
<section v-show="isSectionVisible(4)" class="step-card conversion-content completion-content">
<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>
<p>请检查需求转化、优先级评分和排序决策是否填写完整,再保存或提交本次实训成果。</p>
</section>
<div v-if="hasConfiguredSteps" class="step-navigation">
<button type="button" class="btn-nav" :disabled="form.activeStep === 1" @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>
<button type="button" class="btn-nav btn-primary" :disabled="form.activeStep === flowSteps.length" @click="goNextStep">
下一步
<el-icon><ArrowRight /></el-icon>
</button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">
清空填写
</el-button>
</div>
</section>
</main>
<TrainingAiSidebar
study-tip="当前任务建议:先把用户原声还原成具体场景,再提炼痛点,最后写成可开发、可验收的功能定义。"
reference-title="需求转化参考"
reference-text="· 用户语言:愿望、抱怨、现象<br />· 场景洞察:谁在什么情况下遇到什么阻碍<br />· 工程语言:功能名称、触发条件、处理逻辑、验收指标"
assist-text="可以让 AI 帮你把每条用户原声拆成“场景洞察 + 功能定义 + 验收标准”,再人工检查是否可执行。"
:progress-items="progressItems"
/>
</div>
</template>
<script setup>
import { computed, getCurrentInstance, onMounted, ref } from "vue";
import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
const TASK_KEY = "feature-conversion";
const { proxy } = getCurrentInstance();
const defaultRequirement =
"需求转化不是照搬用户原话,而是把模糊痛点翻译成可设计、可开发、可验收的功能。过程包括需求清洗、场景还原、功能定义、验收标准和优先级排序。";
const defaultBackground =
"假如你是某款智能产品的产品经理,在前期问卷调研和需求分析后,需要把收集到的用户需求转化为功能设计。你要成为需求的翻译者和决策者,而不是用户原话的传声筒。";
const defaultGoals = [
"学会梳理用户需求,剔除重复或低价值需求。",
"将用户语言转化为功能设计和工程语言。",
"能够对功能优先级进行排序。",
];
const priorityTemplateRows = [
{ field: "需求ID", description: "唯一编号,便于追踪", example: "REQ-2026-003", source: "内部系统生成" },
{ field: "需求名称", description: "清晰、无歧义的功能描述", example: "“儿童位置异常自动推送家长APP”", source: "用户访谈 / 客服反馈" },
{ field: "需求来源", description: "提出方(用户/销售/合规/研发)", example: "家长调研组 / 法务部", source: "-" },
{ field: "KANO属性", description: "分类:必备 / 期望 / 魅力", example: "必备属性", source: "基于KANO模型调研" },
{ field: "Reach(覆盖范围)", description: "本周期内受影响的用户数(估算)", example: "85,000(占目标用户群72%)", source: "市场数据 / DAU统计" },
{ field: "Impact(影响程度)", description: "单用户价值评分(1–5分)<br>5=显著提升留存/转化;1=无感知", example: "5(解决核心安全焦虑)", source: "专家评估 + 用户调研" },
{ field: "Confidence(置信度)", description: "对Reach/Impact预测的信心(0%–100%)", example: "90%(基于1,200份问卷)", source: "市场研究 / A/B测试" },
{ field: "Effort(投入成本)", description: "实现所需总人天(开发+测试+认证)", example: "45人天(含嵌入式+云端+认证)", source: "工程团队评估" },
{ field: "RICE得分", description: "计算公式:(Reach × Impact × Confidence)/ Effort<br>结果保留两位小数", example: "(85,000 × 5 × 0.9) / 45 = 8,500.00", source: "自动计算列" },
{
field: "合规性检查项",
description: "必须通过的强制项(任一不通过则直接降为P3或砍掉)",
example: "符合GB 46859-2025(儿童手表隐私权限)<br>□ 通过SRRC认证(无线模块)<br>□ 无未经加密的儿童位置上传<br>□ 内容池通过AI过滤(无不良信息)",
source: "法务/认证团队确认",
},
{ field: "BOM成本影响", description: "是否增加核心元器件(如GPS模组、安全芯片)", example: "是(+¥18/台)", source: "采购部评估" },
{ field: "DFM影响", description: "是否导致模具复杂度↑、良率↓", example: "否", source: "结构工程师确认" },
{ field: "供应链风险", description: "是否依赖长交期/单一供应商物料", example: "否(GPS模组有备选)", source: "供应链团队确认" },
{
field: "优先级等级",
description: "根据RICE得分与合规性综合判定:<br>P0:≥5,000 且合规全通过<br>P1:2,000–4,999 且合规通过<br>P2:<2,000 或部分合规待优化",
example: "P0",
source: "综合判定",
},
{ field: "建议动作", description: "是否纳入首期开发?是否需替代方案?", example: "纳入首期,同步启动SRRC认证预检", source: "——" },
];
const parentFeedback = [
"我家孩子总爱抢手机玩游戏,能不能让机器人把手机里的学习内容导进去,替代手机当学习工具?",
"孩子学拼音总是平翘舌不分,机器人能不能像老师那样纠正发音,还能出练习题测试?",
"机器人能不能记录孩子白天的日常,比如玩了什么游戏、学了什么儿歌,我直接就能看?",
"老人带孩子不会教英语,机器人能不能设定固定时间,比如每天晚上7点,主动跟孩子说英语、玩英语小游戏?",
"孩子害怕一个人睡,机器人能不能在床边播放轻柔的故事,还能感应孩子有没有踢被子,踢了就提醒盖被子?",
"孩子在家总爱乱扔玩具,机器人能不能当“小管家”,提醒孩子收拾玩具,还能跟孩子比赛谁收拾得快?",
"有时候加班晚回家,孩子会闹情绪,机器人能不能跟视频电话结合,我在手机上就能通过机器人跟孩子互动,还能远程控制机器人给孩子放动画片?",
"孩子总爱问“为什么”,我很多问题答不上来,机器人能不能像百科全书一样,不管天文地理都能给孩子讲明白,还要用孩子能听懂的话?",
"我家孩子有点内向,不爱跟别人说话,机器人能不能主动找孩子聊天,引导孩子表达自己的想法,比如“你今天最开心的事是什么呀?”",
"孩子写作业姿势不对,总是趴在桌子上,机器人能不能提醒孩子坐端正,要是不听还能告诉家长?",
"现在网上内容太杂,机器人能不能自动过滤掉暴力、恐怖的内容,只给孩子推送适合3-6岁看的内容?",
];
const childFeedback = [
"机器人能不能跟我一起玩躲猫猫,它来找我,我藏起来它能找到我!",
"我想要机器人会变魔术,比如能把我画的画变成动画片里的小人动起来!",
"机器人的声音能不能像佩奇一样,我还能让它变成奥特曼的声音跟我说话!",
"我要是不开心了,机器人能不能给我跳舞,还能唱我喜欢的《孤勇者》!",
"机器人能不能陪我搭积木,它要是看到我搭错了,就告诉我怎么搭才能搭得更高!",
"我想要机器人有好多好多表情,开心的时候笑,难过的时候哭,就像小朋友一样!",
"机器人能不能跟我一起养小宠物,比如养一只虚拟的小猫,我们一起喂它吃饭、给它洗澡!",
"我要是想爷爷奶奶了,机器人能不能直接帮我打电话给他们,不用我找妈妈拿手机!",
];
const taskConfig = ref(null);
const saving = ref(false);
function createRows() {
return Array.from({ length: 5 }, (_, index) => ({
id: index + 1,
voice: "",
insight: "",
definition: "",
description: "",
acceptance: "",
}));
}
function createScoreRows() {
return Array.from({ length: 5 }, (_, index) => ({
uid: index + 1,
requirementId: "",
name: "",
source: "",
kano: "",
reach: "",
impact: "",
confidence: "",
effort: "",
rice: "",
compliance: "",
}));
}
function createPriorityDecision() {
return {
p0: "",
p1: "",
p2: "",
p3: "",
};
}
function createForm() {
return {
activeStep: 1,
productName: "",
rows: createRows(),
scoreRows: createScoreRows(),
priorityDecision: createPriorityDecision(),
};
}
const form = ref(createForm());
const taskTitle = computed(() => taskConfig.value?.taskName || "需求转化为功能");
const taskBackground = computed(() => taskConfig.value?.background || defaultBackground);
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals));
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement);
const flowSteps = computed(() => parseArray(taskConfig.value?.steps));
const hasConfiguredSteps = computed(() => flowSteps.value.length > 0);
const activeStepName = computed(() => flowSteps.value[form.value.activeStep - 1] || "");
const progressItems = computed(() => [
{ text: form.value.productName ? "已填写:产品名称" : "待填写:产品名称", status: form.value.productName ? "done" : "" },
{ text: hasTableContent.value ? "进行中:需求转化表" : "待完成:需求转化表", status: hasTableContent.value ? "doing" : "" },
{ text: hasScoreContent.value ? "进行中:优先级打分表" : "待完成:优先级打分表", status: hasScoreContent.value ? "doing" : "" },
{ text: hasDecisionContent.value ? "进行中:优先级排序决策" : "待完成:优先级排序决策", status: hasDecisionContent.value ? "doing" : "" },
{ text: "待完成:提交实训成果", status: "" },
]);
const hasTableContent = computed(() =>
form.value.rows.some((row) =>
["voice", "insight", "definition", "description", "acceptance"].some((key) => String(row[key] || "").trim())
)
);
const hasScoreContent = computed(() =>
form.value.scoreRows.some((row) =>
["requirementId", "name", "source", "kano", "reach", "impact", "confidence", "effort", "rice", "compliance"].some((key) =>
String(row[key] || "").trim()
)
)
);
const hasDecisionContent = computed(() =>
["p0", "p1", "p2", "p3"].some((key) => String(form.value.priorityDecision?.[key] || "").trim())
);
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);
const answer = res?.data;
if (answer?.step1Answer) {
applySavedAnswer(answer.step1Answer);
}
} catch (error) {
// Keep local defaults when history cannot be loaded.
}
}
function parseArray(value, fallback = []) {
if (Array.isArray(value)) return value.filter(Boolean);
if (!value) return fallback;
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) && parsed.length ? parsed : fallback;
} catch (error) {
const list = String(value)
.split(/[;;||\n]/)
.map((item) => item.trim())
.filter(Boolean);
return list.length ? list : fallback;
}
}
function normalizeActiveStep(value) {
if (!flowSteps.value.length) return 1;
const parsed = Number(value) || 1;
return Math.min(Math.max(parsed, 1), flowSteps.value.length);
}
function setCurrentStep(step) {
form.value.activeStep = normalizeActiveStep(step);
}
function isSectionVisible(section) {
return !hasConfiguredSteps.value || form.value.activeStep === section;
}
function goPreviousStep() {
setCurrentStep(form.value.activeStep - 1);
}
function goNextStep() {
setCurrentStep(form.value.activeStep + 1);
}
function applySavedAnswer(value) {
try {
const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== "object") return;
form.value = {
...createForm(),
...parsed,
rows: normalizeRows(parsed.rows),
scoreRows: normalizeScoreRows(parsed.scoreRows),
priorityDecision: normalizePriorityDecision(parsed.priorityDecision),
activeStep: normalizeActiveStep(parsed.activeStep),
};
} catch (error) {
// Ignore malformed historical answers.
}
}
function normalizeRows(rows = []) {
const savedRows = Array.isArray(rows) ? rows : [];
return createRows().map((row, index) => ({
...row,
...(savedRows[index] || {}),
id: row.id,
}));
}
function normalizeScoreRows(rows = []) {
const savedRows = Array.isArray(rows) ? rows : [];
return createScoreRows().map((row, index) => ({
...row,
...(savedRows[index] || {}),
uid: row.uid,
}));
}
function normalizePriorityDecision(value = {}) {
return {
...createPriorityDecision(),
...(value && typeof value === "object" ? value : {}),
};
}
function buildOutcome() {
return {
taskKey: TASK_KEY,
taskId: taskConfig.value?.id || "",
taskName: taskTitle.value,
flowSteps: flowSteps.value,
productName: form.value.productName,
activeStep: form.value.activeStep,
rows: form.value.rows,
scoreRows: form.value.scoreRows,
priorityDecision: form.value.priorityDecision,
updatedAt: new Date().toISOString(),
};
}
function buildAnswerPayload(status) {
const saveAction = status === "SUBMITTED" ? "SUBMIT" : status === "RESET" ? "RESET" : "SAVE";
return {
saveAction,
currentStep: form.value.activeStep,
step1Answer: JSON.stringify(buildOutcome()),
};
}
async function persist(status = "IN_PROGRESS") {
if (saving.value) return;
saving.value = true;
try {
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
} finally {
saving.value = false;
}
}
function saveCurrentProgress() {
return persist("IN_PROGRESS");
}
function submitTask() {
return persist("SUBMITTED");
}
async function resetTraining() {
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);
}
</script>
<style scoped lang="scss">
.feature-conversion-page {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
gap: 22px;
min-height: calc(100vh - 76px);
padding: 24px;
color: #eaf7ff;
background:
radial-gradient(circle at 20% 4%, rgba(24, 151, 210, 0.18), transparent 34%),
linear-gradient(180deg, #05131f 0%, #07111b 100%);
}
.training-core {
min-width: 0;
padding: 28px;
border: 1px solid rgba(23, 145, 194, 0.56);
border-radius: 28px;
background: linear-gradient(180deg, rgba(4, 22, 34, 0.96), rgba(4, 15, 25, 0.98));
box-shadow: inset 0 1px 0 rgba(95, 232, 255, 0.16), 0 22px 44px rgba(0, 0, 0, 0.24);
}
.task-header {
margin-bottom: 18px;
}
.task-badge {
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 14px;
border: 1px solid rgba(67, 209, 255, 0.72);
border-radius: 999px;
color: #d9f7ff;
background: rgba(14, 98, 139, 0.48);
font-size: 13px;
font-weight: 800;
}
.task-title {
margin: 16px 0 0;
color: #f5fbff;
font-size: 30px;
line-height: 1.2;
letter-spacing: 0;
}
.goal-bg,
.task-card {
border: 1px solid rgba(32, 149, 200, 0.58);
border-radius: 26px;
background: rgba(4, 31, 46, 0.72);
}
.goal-bg {
padding: 22px;
color: #c9ddec;
font-size: 15px;
line-height: 1.8;
white-space: pre-line;
}
.goal-bg p,
.goal-bg ol {
margin: 8px 0 0;
}
.subtitle-icon {
display: inline-flex;
align-items: center;
gap: 8px;
color: #eafaff;
font-size: 18px;
font-weight: 900;
}
.subtitle-icon--spaced {
margin-top: 16px;
}
.task-card {
margin-top: 22px;
padding: 24px;
}
.workbench-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
.section-eyebrow {
margin: 0 0 6px;
color: #67e6ff;
font-size: 12px;
font-weight: 900;
text-transform: uppercase;
}
.workbench-head h3 {
margin: 0;
color: #f5fbff;
font-size: 22px;
}
.ghost-action {
border-color: #2c83aa;
color: #dff7ff;
background: rgba(13, 64, 88, 0.62);
}
.product-field {
display: grid;
grid-template-columns: 88px minmax(260px, 520px);
align-items: center;
gap: 12px;
margin-bottom: 34px;
color: #e7f7ff;
font-size: 16px;
font-weight: 900;
}
.product-field input {
height: 38px;
padding: 0 12px;
border: 1px solid rgba(137, 189, 213, 0.82);
border-radius: 2px;
color: #f5fbff;
background: rgba(6, 22, 34, 0.86);
font-size: 15px;
outline: none;
}
.flow-steps {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 22px;
margin-bottom: 44px;
}
.flow-pill {
min-width: 230px;
height: 58px;
border: 0;
border-radius: 6px;
color: #ffffff;
background: #1d9bd0;
font-size: 17px;
font-weight: 900;
cursor: pointer;
}
.flow-pill.active {
background: #20a4d9;
box-shadow: 0 0 0 2px rgba(113, 223, 255, 0.18);
}
.flow-arrow {
color: #d4dbe1;
font-size: 42px;
font-weight: 900;
letter-spacing: -8px;
transform: scaleX(0.72);
}
.conversion-content {
color: #d5e6f0;
font-size: 16px;
line-height: 1.75;
}
.conversion-content h4 {
margin: 0 0 14px;
color: #f5fbff;
font-size: 20px;
}
.conversion-content h5 {
margin: 16px 0 8px;
color: #eef9ff;
font-size: 16px;
}
.conversion-content p {
margin: 4px 0;
}
.indent {
padding-left: 24px;
}
.source-title,
.table-tip {
margin-top: 20px !important;
color: #f0f8ff;
font-weight: 900;
}
.feedback-grid {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
gap: 46px;
margin-top: 10px;
}
.feedback-grid section {
min-width: 0;
}
.feedback-grid ol {
margin: 8px 0 0;
padding-left: 20px;
}
.feedback-grid li {
margin-bottom: 6px;
}
.conversion-table-wrap {
overflow-x: auto;
margin-top: 10px;
border: 1px solid rgba(139, 187, 208, 0.5);
}
.conversion-table {
width: 100%;
min-width: 1120px;
border-collapse: collapse;
table-layout: fixed;
}
.conversion-table th,
.conversion-table td {
border: 1px solid rgba(139, 187, 208, 0.5);
}
.conversion-table th {
height: 46px;
color: #f2fbff;
background: rgba(14, 76, 107, 0.72);
font-size: 15px;
font-weight: 900;
}
.conversion-table th:first-child,
.conversion-table td:first-child {
width: 72px;
text-align: center;
}
.conversion-table td {
height: 78px;
padding: 0;
background: rgba(3, 18, 29, 0.38);
}
.conversion-table textarea {
display: block;
width: 100%;
height: 78px;
padding: 9px 10px;
border: 0;
outline: none;
resize: none;
color: #f4fbff;
background: transparent;
font-family: inherit;
font-size: 13px;
line-height: 1.55;
box-sizing: border-box;
}
.conversion-table textarea::placeholder {
color: #8fa4b1;
}
.priority-content {
max-width: 100%;
}
.priority-template-table,
.priority-score-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.priority-template-table {
min-width: 1180px;
}
.priority-score-table {
min-width: 1540px;
}
.priority-template-table th,
.priority-template-table td,
.priority-score-table th,
.priority-score-table td {
border: 1px solid rgba(139, 187, 208, 0.58);
}
.priority-template-table th,
.priority-score-table th {
height: 42px;
padding: 6px 10px;
color: #f2fbff;
background: rgba(14, 76, 107, 0.72);
font-size: 15px;
font-weight: 900;
text-align: center;
}
.priority-template-table td {
min-height: 38px;
padding: 8px 10px;
color: #d8e8f1;
background: rgba(3, 18, 29, 0.38);
font-size: 14px;
line-height: 1.5;
text-align: center;
word-break: break-word;
}
.priority-template-table td:nth-child(2),
.priority-template-table td:nth-child(3) {
text-align: left;
}
.priority-score-table th {
white-space: normal;
}
.priority-score-table td {
height: 72px;
padding: 0;
background: rgba(3, 18, 29, 0.38);
}
.priority-score-table textarea {
display: block;
width: 100%;
height: 72px;
padding: 9px 10px;
border: 0;
outline: none;
resize: none;
color: #f4fbff;
background: transparent;
font-family: inherit;
font-size: 13px;
line-height: 1.5;
box-sizing: border-box;
}
.decision-content {
max-width: 100%;
}
.decision-intro {
margin-bottom: 18px !important;
color: #d8e8f1;
font-size: 16px;
}
.decision-field {
display: block;
margin-top: 30px;
}
.decision-field span {
display: block;
margin-bottom: 10px;
color: #e7f7ff;
font-size: 17px;
font-weight: 900;
}
.decision-field textarea {
display: block;
width: 100%;
min-height: 210px;
padding: 14px 16px;
border: 1px solid rgba(139, 187, 208, 0.7);
border-radius: 2px;
outline: none;
resize: vertical;
color: #f4fbff;
background: rgba(3, 18, 29, 0.38);
font-family: inherit;
font-size: 15px;
line-height: 1.65;
box-sizing: border-box;
}
.decision-field textarea:focus {
border-color: rgba(90, 218, 255, 0.95);
box-shadow: 0 0 0 2px rgba(42, 173, 220, 0.18);
}
.training-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 52px;
margin-top: 36px;
}
.training-actions :deep(.training-action) {
min-width: 172px;
height: 64px;
padding: 0 28px;
border: 1px solid #2c83aa;
border-radius: 6px;
color: #dff7ff;
background: rgba(13, 64, 88, 0.62);
box-shadow: none;
font-size: 24px;
font-weight: 900;
line-height: 1;
letter-spacing: 0;
text-shadow: none;
}
.training-actions :deep(.training-action span) {
display: inline-flex;
align-items: center;
color: inherit;
font-size: inherit;
line-height: 1;
}
.training-actions :deep(.training-action--submit),
.training-actions :deep(.training-action--reset) {
border-color: #2c83aa;
color: #dff7ff;
background: rgba(13, 64, 88, 0.62);
}
.training-actions :deep(.training-action:hover:not(.is-disabled)) {
border-color: #46b5e7;
color: #ffffff;
background: rgba(18, 85, 116, 0.78);
}
@media (max-width: 1100px) {
.feature-conversion-page {
grid-template-columns: 1fr;
}
.feedback-grid {
grid-template-columns: 1fr;
gap: 18px;
}
}
@media (max-width: 760px) {
.feature-conversion-page {
padding: 14px;
}
.training-core {
padding: 18px;
}
.product-field {
grid-template-columns: 1fr;
}
.flow-steps {
gap: 12px;
}
.flow-pill,
.training-actions :deep(.training-action) {
width: 100%;
}
.flow-arrow {
display: none;
}
}
/* Student training standard: shared workbench hierarchy and controls. */
.feature-conversion-page {
grid-template-columns: minmax(720px, 1fr) 340px;
gap: 24px;
min-height: calc(100vh - 76px);
padding: 24px 24px 32px;
color: #eef5ff;
background: #06111d;
}
.training-core {
padding: 28px;
border: 1px solid rgba(0, 180, 255, 0.25);
border-radius: 36px;
background: rgba(8, 18, 28, 0.6);
box-shadow: inset 0 1px 0 rgba(169, 229, 255, 0.08), 0 22px 48px rgba(0, 0, 0, 0.22);
}
.task-header {
margin-bottom: 24px;
}
.task-badge {
height: auto;
min-height: 28px;
padding: 4px 14px;
border-color: rgba(99, 217, 255, 0.55);
color: #dff5ff;
background: rgba(17, 126, 178, 0.36);
font-size: 12px;
font-weight: 700;
line-height: 1.4;
}
.task-title {
margin-top: 12px;
color: #ffffff;
font-size: 30px;
font-weight: 900;
line-height: 1.3;
}
.task-card,
.step-card {
border: 1px solid rgba(82, 174, 226, 0.42);
border-radius: 26px;
background: rgba(0, 25, 42, 0.64);
}
.task-card {
margin-top: 28px;
padding: 18px 22px 22px;
}
.workbench-head {
align-items: flex-start;
gap: 18px;
margin-bottom: 22px;
padding-bottom: 18px;
border-bottom: 1px solid rgba(0, 180, 255, 0.2);
}
.section-eyebrow {
margin: 0;
color: #71dfff;
font-size: 12px;
font-weight: 800;
line-height: 1.4;
letter-spacing: 0.02em;
}
.workbench-head h3 {
margin: 4px 0 8px;
color: #ffffff;
font-size: 26px;
font-weight: 800;
line-height: 1.3;
}
.workbench-description {
max-width: 760px;
margin: 0;
color: #d7ecff;
font-size: 15px;
font-weight: 400;
line-height: 1.8;
}
.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;
min-width: 0;
min-height: 40px;
padding: 0 18px;
border: 1px solid rgba(99, 217, 255, 0.55);
border-radius: 999px;
color: #e9fbff;
background: rgba(17, 126, 178, 0.36);
font: inherit;
font-size: 14px;
font-weight: 800;
cursor: pointer;
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.outline-action:hover,
.outline-action:focus-visible {
border-color: rgba(116, 235, 255, 0.85);
background: rgba(18, 146, 204, 0.5);
transform: translateY(-1px);
outline: none;
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
gap: 0;
width: calc(100% + 24px);
min-height: 0;
margin: 0 -12px 32px;
padding: 18px 22px;
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
scrollbar-width: thin;
}
.step-tab {
display: flex;
flex: 1 0 145px;
align-items: center;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font: inherit;
font-size: 14px;
font-weight: 800;
cursor: pointer;
text-align: left;
transition: transform 0.2s ease, color 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.step-tab:hover {
transform: translateY(-2px);
color: #ffffff;
border-color: rgba(89, 223, 255, 0.82);
background: rgba(8, 92, 131, 0.52);
box-shadow: 0 7px 18px rgba(0, 0, 0, 0.24), inset 0 1px 0 rgba(152, 239, 255, 0.16);
}
.step-tab:hover .step-action {
color: #9bf4ff;
}
.step-tab:focus-visible {
outline: 2px solid rgba(92, 224, 255, 0.7);
outline-offset: 2px;
}
.step-tab.active {
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
}
.step-marker {
display: inline-flex;
flex: 0 0 38px;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border: 1px solid rgba(101, 176, 208, 0.6);
border-radius: 50%;
color: #a6c4d9;
background: rgba(3, 25, 40, 0.8);
box-shadow: inset 0 0 12px rgba(38, 174, 233, 0.08);
font-size: 12px;
font-weight: 900;
letter-spacing: 0.04em;
transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.step-copy {
display: grid;
min-width: 0;
gap: 4px;
margin-left: 10px;
}
.step-name {
display: -webkit-box;
overflow: hidden;
color: inherit;
font-size: 14px;
font-weight: 800;
line-height: 1.25;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.step-action {
color: #66b6d4;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.06em;
line-height: 1;
transition: color 0.2s ease;
}
.step-tab.active .step-marker {
border-color: #72e8ff;
color: #ffffff;
background: linear-gradient(135deg, #08b7e8, #1375d0);
box-shadow: 0 0 0 5px rgba(25, 179, 237, 0.14), 0 0 18px rgba(46, 208, 255, 0.58);
}
.step-tab.complete .step-marker {
border-color: rgba(84, 230, 221, 0.85);
color: #062b37;
background: #6ef0e1;
box-shadow: 0 0 12px rgba(79, 231, 218, 0.35);
}
.step-connector {
position: relative;
flex: 0 1 40px;
min-width: 18px;
height: 2px;
margin: 0 8px;
overflow: visible;
background: rgba(98, 169, 201, 0.34);
}
.step-connector::after {
position: absolute;
top: 50%;
right: -1px;
width: 6px;
height: 6px;
border-top: 1px solid #73b1ce;
border-right: 1px solid #73b1ce;
content: "";
transform: translateY(-50%) rotate(45deg);
}
.step-tab.complete + .step-connector {
background: linear-gradient(90deg, #6ef0e1, #1cb8e9);
box-shadow: 0 0 9px rgba(53, 218, 235, 0.46);
&::after {
border-color: #63e6f1;
}
}
.step-card {
margin-top: 32px;
padding: 18px 22px 22px;
}
.workbench-head + .step-card {
margin-top: 0;
}
.step-card + .step-card {
margin-top: 16px;
}
.step-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 14px;
}
.step-title-row h3 {
margin: 4px 0 0;
color: #ffffff;
font-size: 22px;
font-weight: 900;
line-height: 1.3;
}
.conversion-content {
color: #d7ecff;
font-size: 15px;
line-height: 1.8;
}
.conversion-content h4 {
margin: 0 0 14px;
color: #ffffff;
font-size: 22px;
font-weight: 900;
line-height: 1.3;
}
.conversion-content h5,
.source-title,
.table-tip {
color: #dff5ff;
}
.conversion-content p {
margin: 0 0 10px;
}
.product-field,
.decision-field {
display: block;
margin: 0 0 16px;
color: #dff5ff;
font-size: 14px;
font-weight: 800;
}
.product-field input,
.decision-field textarea,
.conversion-table textarea,
.priority-score-table textarea {
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 14px;
line-height: 1.65;
}
.product-field input {
display: block;
width: min(100%, 680px);
min-height: 42px;
margin-top: 8px;
padding: 0 14px;
box-sizing: border-box;
}
.product-field input:focus,
.decision-field textarea:focus,
.conversion-table textarea:focus,
.priority-score-table textarea:focus {
border-color: rgba(92, 224, 255, 0.7);
box-shadow: 0 0 0 3px rgba(0, 174, 255, 0.13);
outline: none;
}
.feedback-grid {
gap: 16px;
}
.feedback-grid section,
.example-block {
padding: 16px;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 20px;
background: rgba(1, 14, 26, 0.72);
}
.feedback-grid li {
margin-bottom: 8px;
color: #d7ecff;
font-size: 14px;
line-height: 1.72;
}
.conversion-table-wrap {
margin-top: 12px;
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 22px;
background: rgba(1, 14, 26, 0.72);
}
.conversion-table th,
.conversion-table td,
.priority-template-table th,
.priority-template-table td,
.priority-score-table th,
.priority-score-table td {
border-color: rgba(77, 167, 220, 0.2);
}
.conversion-table th,
.priority-template-table th,
.priority-score-table th {
height: auto;
min-height: 46px;
padding: 12px;
color: #eaffff;
background: rgba(8, 55, 82, 0.98);
font-size: 14px;
font-weight: 900;
}
.conversion-table td,
.priority-template-table td,
.priority-score-table td {
padding: 10px;
color: #d7f1ff;
background: rgba(1, 14, 26, 0.48);
vertical-align: top;
}
.conversion-table textarea,
.priority-score-table textarea {
height: 96px;
min-height: 38px;
padding: 10px;
resize: vertical;
}
.priority-template-table td {
color: #d7f1ff;
font-size: 14px;
line-height: 1.65;
}
.decision-field {
margin-top: 16px;
}
.decision-field span {
margin-bottom: 8px;
color: #dff5ff;
font-size: 14px;
font-weight: 800;
}
.decision-field textarea {
min-height: 110px;
padding: 12px 14px;
}
.completion-content p {
margin: 0;
}
.step-navigation {
display: flex;
justify-content: center;
gap: 14px;
margin-top: 22px;
}
.btn-nav {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 40px;
padding: 0 18px;
border: 1px solid rgba(99, 217, 255, 0.55);
border-radius: 999px;
color: #e9fbff;
background: rgba(17, 126, 178, 0.36);
font: inherit;
font-size: 14px;
font-weight: 800;
cursor: pointer;
}
.btn-nav.btn-primary,
.btn-nav.btn-save {
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.btn-nav:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.training-actions {
margin-top: 36px;
}
@media (max-width: 1100px) {
.feature-conversion-page {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.feature-conversion-page {
padding: 14px;
}
.training-core {
padding: 18px;
border-radius: 24px;
}
.workbench-head,
.workbench-actions {
align-items: stretch;
flex-direction: column;
}
.workbench-actions,
.outline-action,
.step-navigation,
.btn-nav,
.training-actions :deep(.training-action) {
width: 100%;
}
.step-indicator {
align-items: stretch;
flex-direction: column;
width: 100%;
margin-right: 0;
margin-left: 0;
overflow: visible;
border-radius: 20px;
}
.step-tab {
flex: 0 0 auto;
width: 100%;
}
.step-connector {
flex: 0 0 20px;
min-width: 0;
width: 2px;
height: 20px;
margin-left: 29px;
text-align: center;
writing-mode: vertical-rl;
}
.step-navigation {
flex-direction: column;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>