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

1856 lines
51 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 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>
<div class="conversion-intro">
<p>用户反馈通常混杂重复描述、情绪化表达、无关信息。请阅读案例资料中原始用户反馈,逐条进行场景洞察拆解,分析用户在什么时间、什么情境下产生该需求,背后的深层动机是什么,然后将用户语言转化为可执行的产品功能定义,填写以下需求转化表。</p>
<div class="conversion-row-actions">
<el-button type="primary" :disabled="form.rows.length >= MAX_CONVERSION_ROWS" @click="addConversionRow">增加一行</el-button>
<el-button type="primary" plain :disabled="form.rows.length <= MIN_CONVERSION_ROWS" @click="removeConversionRow">删除一行</el-button>
</div>
</div>
<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="index === 0 ? '示例:起夜需要温和照明,避免强光刺激' : '填写用户原声'"></textarea></td>
<td><textarea v-model="row.insight" :placeholder="index === 0 ? '示例:凌晨2-5点用户从深度睡眠中被尿意唤醒,处于半梦半醒状态,瞳孔对光线极其敏感' : '填写场景洞察'"></textarea></td>
<td><textarea v-model="row.definition" :placeholder="index === 0 ? '示例:起夜微光模式' : '填写转化后的功能定义'"></textarea></td>
<td><textarea v-model="row.description" :placeholder="index === 0 ? '示例:感应到用户起身时,自动开启亮度≤10%的暖黄地脚光,延迟3秒渐亮' : '填写功能描述'"></textarea></td>
<td><textarea v-model="row.acceptance" :placeholder="index === 0 ? '示例:亮度5-15流明可调,色温2700K,启动延时≤0.5秒,光通量渐升时间3±0.5秒' : '填写技术指标/验收标准'"></textarea></td>
</tr>
</tbody>
</table>
</div>
<p class="conversion-limit-tip">至少 {{ MIN_CONVERSION_ROWS }} 行,最多 {{ MAX_CONVERSION_ROWS }} 行</p>
<div class="conversion-helper">
<h5>可参考以下方法:</h5>
<p>1. 自行阅读案例信息,将用户原声转化为需求,再通过需求分析转化为产品功能。</p>
<p>2. 使用通用 AI 大模型辅助完成场景洞察与功能转化,参考提示词见案例文档。</p>
</div>
<p v-if="conversionValidationMessage" class="conversion-validation-message" role="status">{{ conversionValidationMessage }}</p>
</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>
<div class="rice-intro">
<p>将步骤一的功能纳入 RICE 评估模型,进行量化打分。RICE 模型包含覆盖用户数、影响程度、置信度、开发成本四个维度。</p>
<p>请根据上一步骤需求转化后的功能定义,针对具体需求进行量化打分,填写以下打分表。</p>
</div>
<div class="conversion-table-wrap">
<table class="priority-score-table">
<thead>
<tr>
<th>需求ID</th>
<th>需求名称</th>
<th>KANO属性</th>
<th>Reach(覆盖范围)</th>
<th>Impact(影响程度)</th>
<th>Confidence(置信度)</th>
<th>Effort(人/月)</th>
<th>RICE得分</th>
</tr>
</thead>
<tbody>
<tr v-for="row in form.scoreRows" :key="row.uid">
<td class="auto-score-cell">{{ row.requirementId }}</td>
<td class="auto-score-cell auto-score-cell--name">{{ row.name || "请先在步骤一填写功能定义" }}</td>
<td>
<select v-model="row.kano" :aria-label="`${row.requirementId} KANO属性`">
<option value="">请选择</option>
<option v-for="type in kanoTypes" :key="type" :value="type">{{ type }}</option>
</select>
</td>
<td><input v-model.number="row.reach" type="number" min="1" max="5" step="1" aria-label="Reach覆盖范围" /></td>
<td><input v-model.number="row.impact" type="number" min="1" max="5" step="1" aria-label="Impact影响程度" /></td>
<td>
<div class="metric-input"><input v-model.number="row.confidence" type="number" min="1" max="100" step="1" aria-label="Confidence置信度" /><span>%</span></div>
</td>
<td><input v-model.number="row.effort" type="number" min="0.1" step="0.1" aria-label="Effort人月" /></td>
<td class="rice-score-cell">{{ calculateRiceScore(row) }}</td>
</tr>
</tbody>
</table>
</div>
<p class="rice-formula">计算公式:(Reach × Impact × Confidence) / Effort,结果保留两位小数。需求 ID 与功能名称由步骤一自动同步。</p>
<p v-if="scoreValidationMessage" class="conversion-validation-message" role="status">{{ scoreValidationMessage }}</p>
</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 { checkFeatureConversionStepOne, checkFeatureConversionStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
const TASK_KEY = "feature-conversion";
const MIN_CONVERSION_ROWS = 8;
const MAX_CONVERSION_ROWS = 12;
const kanoTypes = ["基本型", "期望型", "兴奋型", "无差异型", "反向型"];
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);
const checkingConversion = ref(false);
const checkingScore = ref(false);
const conversionValidationMessage = ref("");
const scoreValidationMessage = ref("");
function createRows(count = MIN_CONVERSION_ROWS) {
return Array.from({ length: count }, (_, index) => ({
id: index + 1,
voice: "",
insight: "",
definition: "",
description: "",
acceptance: "",
}));
}
function createScoreRows(sourceRows = createRows()) {
return sourceRows.map((sourceRow, index) => ({
uid: index + 1,
requirementId: formatRequirementId(index),
name: sourceRow.definition || "",
kano: "",
reach: "",
impact: "",
confidence: "",
effort: "",
}));
}
function createPriorityDecision() {
return {
p0: "",
p1: "",
p2: "",
p3: "",
};
}
function createForm() {
const rows = createRows();
return {
activeStep: 1,
productName: "",
rows,
scoreRows: createScoreRows(rows),
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: 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) =>
["kano", "reach", "impact", "confidence", "effort"].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);
}
async function setCurrentStep(step) {
const targetStep = normalizeActiveStep(step);
if (targetStep > form.value.activeStep && [1, 2].includes(form.value.activeStep) && !(await persist("IN_PROGRESS"))) return;
if (form.value.activeStep === 1 && targetStep > 1) syncScoreRows();
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;
const rows = normalizeRows(parsed.rows);
form.value = {
...createForm(),
...parsed,
rows,
scoreRows: normalizeScoreRows(parsed.scoreRows, rows),
priorityDecision: normalizePriorityDecision(parsed.priorityDecision),
activeStep: normalizeActiveStep(parsed.activeStep),
};
} catch (error) {
// Ignore malformed historical answers.
}
}
function normalizeRows(rows = []) {
const savedRows = Array.isArray(rows) ? rows : [];
const normalizedCount = Math.min(Math.max(savedRows.length, MIN_CONVERSION_ROWS), MAX_CONVERSION_ROWS);
return createRows(normalizedCount).map((row, index) => ({ ...row, ...(savedRows[index] || {}), id: row.id }));
}
function addConversionRow() {
if (form.value.rows.length >= MAX_CONVERSION_ROWS) {
proxy?.$modal?.msgWarning(`最多只能填写 ${MAX_CONVERSION_ROWS} 行`);
return;
}
const nextId = Math.max(0, ...form.value.rows.map((row) => Number(row.id) || 0)) + 1;
form.value.rows.push({ id: nextId, voice: "", insight: "", definition: "", description: "", acceptance: "" });
syncScoreRows();
}
function removeConversionRow() {
if (form.value.rows.length <= MIN_CONVERSION_ROWS) {
proxy?.$modal?.msgWarning(`至少需要填写 ${MIN_CONVERSION_ROWS} 行`);
return;
}
form.value.rows.pop();
syncScoreRows();
}
async function ensureConversionValid() {
if (checkingConversion.value) return false;
checkingConversion.value = true;
try {
const response = await checkFeatureConversionStepOne({ rows: form.value.rows });
const result = response?.data || {};
conversionValidationMessage.value = result.message || (result.valid ? "需求转化表填写完整。" : `请完整填写 ${MIN_CONVERSION_ROWS}-${MAX_CONVERSION_ROWS} 行需求转化信息。`);
return result.valid === true;
} catch (error) {
conversionValidationMessage.value = error?.message || "需求转化表校验失败,请稍后重试。";
return false;
} finally {
checkingConversion.value = false;
}
}
function formatRequirementId(index) {
return `F${String(index + 1).padStart(2, "0")}`;
}
function normalizeScoreRows(rows = [], sourceRows = form.value.rows) {
const savedRows = Array.isArray(rows) ? rows : [];
return createScoreRows(sourceRows).map((row, index) => ({
...row,
...(savedRows[index] || {}),
uid: row.uid,
requirementId: row.requirementId,
name: row.name,
}));
}
function syncScoreRows() {
form.value.scoreRows = normalizeScoreRows(form.value.scoreRows, form.value.rows);
}
function calculateRiceScore(row) {
const reach = Number(row.reach);
const impact = Number(row.impact);
const confidence = Number(row.confidence);
const effort = Number(row.effort);
if (![reach, impact, confidence, effort].every(Number.isFinite) || effort <= 0) return "--";
return ((reach * impact * (confidence / 100)) / effort).toFixed(2);
}
async function ensureScoreValid() {
if (checkingScore.value) return false;
checkingScore.value = true;
try {
const response = await checkFeatureConversionStepTwo({
sourceDefinitions: form.value.rows.map((row) => row.definition),
rows: form.value.scoreRows.map((row) => ({ ...row, riceScore: calculateRiceScore(row) })),
});
const result = response?.data || {};
scoreValidationMessage.value = result.message || (result.valid ? "RICE 评分填写完整。" : "请完整填写 RICE 评分表。");
return result.valid === true;
} catch (error) {
scoreValidationMessage.value = error?.message || "RICE 评分校验失败,请稍后重试。";
return false;
} finally {
checkingScore.value = false;
}
}
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;
if (status !== "RESET" && form.value.activeStep === 1 && !(await ensureConversionValid())) return false;
if (status !== "RESET" && form.value.activeStep === 2 && !(await ensureScoreValid())) 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() {
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;
}
.conversion-intro {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 22px;
margin-bottom: 16px;
padding: 18px 20px;
border-left: 3px solid #51dfff;
border-radius: 0 14px 14px 0;
background: linear-gradient(90deg, rgba(7, 95, 137, 0.28), rgba(4, 31, 49, 0.18));
> p {
max-width: 840px;
color: #c6dee9;
font-size: 14px;
line-height: 1.85;
}
}
.conversion-row-actions {
display: flex;
flex: 0 0 auto;
gap: 10px;
.el-button {
min-width: 112px;
}
}
.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;
}
.conversion-limit-tip {
margin: 12px 0 0 !important;
color: #ff958a;
font-size: 13px;
font-weight: 800;
}
.conversion-helper {
margin-top: 26px;
padding: 16px 18px;
border: 1px solid rgba(68, 163, 208, 0.3);
border-radius: 14px;
color: #b7d1de;
background: rgba(3, 27, 43, 0.4);
h5 {
margin: 0 0 4px;
color: #e8f9ff;
font-size: 14px;
}
p {
font-size: 13px;
line-height: 1.8;
}
}
.conversion-validation-message {
margin: 14px 0 0 !important;
color: #ffbbb0;
font-size: 13px;
font-weight: 800;
}
.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;
}
.rice-intro {
margin-bottom: 16px;
padding: 18px 20px;
border-left: 3px solid #53dcff;
border-radius: 0 14px 14px 0;
background: linear-gradient(90deg, rgba(7, 95, 137, 0.28), rgba(4, 31, 49, 0.16));
p {
margin: 0;
color: #c7dfe9;
font-size: 14px;
line-height: 1.85;
}
p + p {
margin-top: 5px;
}
}
.priority-score-table {
min-width: 1100px;
th,
td {
text-align: center;
vertical-align: middle;
}
td {
height: 66px;
padding: 8px;
}
.auto-score-cell {
color: #bde8f6;
background: rgba(7, 62, 88, 0.5);
font-weight: 800;
}
.auto-score-cell--name {
min-width: 180px;
color: #d9f4ff;
font-weight: 700;
line-height: 1.55;
word-break: break-word;
}
select,
input {
width: 100%;
min-height: 36px;
padding: 0 9px;
border: 1px solid rgba(74, 157, 194, 0.64);
border-radius: 8px;
outline: none;
color: #eefbff;
background: rgba(6, 38, 58, 0.78);
font: inherit;
font-size: 13px;
box-sizing: border-box;
&:focus {
border-color: rgba(92, 224, 255, 0.76);
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.1);
}
}
.metric-input {
position: relative;
input {
padding-right: 24px;
}
span {
position: absolute;
top: 50%;
right: 9px;
color: #94bece;
font-size: 12px;
transform: translateY(-50%);
pointer-events: none;
}
}
.rice-score-cell {
color: #64e1ff;
font-size: 16px;
font-weight: 900;
background: rgba(9, 94, 137, 0.28);
}
}
.rice-formula {
margin: 12px 0 0 !important;
color: #ff9a8d;
font-size: 13px;
font-weight: 800;
}
@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;
}
.conversion-intro,
.conversion-row-actions {
align-items: stretch;
flex-direction: column;
}
.conversion-row-actions .el-button {
width: 100%;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>