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

1521 lines
43 KiB
Vue

<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">
<div>
<p class="section-eyebrow">Training Task</p>
<h3>任务实训</h3>
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
</div>
<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="需求转化为功能步骤">
<button
v-for="(step, index) in flowSteps"
:key="`${step}-${index}`"
type="button"
class="step-tab"
:class="{ active: form.activeStep === index + 1 }"
:aria-current="form.activeStep === index + 1 ? 'step' : undefined"
@click="setCurrentStep(index + 1)"
>
{{ step }}
</button>
</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: 1180px) {
.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;
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 {
display: flex;
gap: 6px;
min-height: 58px;
margin: 0;
padding: 6px;
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 999px;
background: rgba(0, 20, 35, 0.65);
}
.step-tab {
flex: 1 0 max-content;
min-height: 46px;
padding: 0 18px;
border: 1px solid transparent;
border-radius: 999px;
color: #9ab3d0;
background: transparent;
font: inherit;
font-size: 14px;
font-weight: 800;
cursor: pointer;
transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
}
.step-tab:hover,
.step-tab:focus-visible {
color: #e9fbff;
border-color: rgba(99, 217, 255, 0.55);
outline: none;
}
.step-tab.active {
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
}
.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: 1180px) {
.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 {
border-radius: 26px;
flex-direction: column;
}
.step-tab {
flex: none;
}
.step-navigation {
flex-direction: column;
}
}
</style>