diff --git a/src/api/studentTrainingAnswer.js b/src/api/studentTrainingAnswer.js index a7bb5bd..3b40677 100644 --- a/src/api/studentTrainingAnswer.js +++ b/src/api/studentTrainingAnswer.js @@ -1737,3 +1737,19 @@ export function checkConsumerScenarioStepFour(payload) { if (isStudentDemo()) { const valid = rows.length === consumerScenarioJourneyOrder.length && rows.every((row) => consumerScenarioJourneyOrder.includes(row?.stageId) && String(row?.painPoint || "").trim() && String(row?.optimizationSuggestion || "").trim()) && new Set(rows.map((row) => row.stageId)).size === consumerScenarioJourneyOrder.length; return Promise.resolve({ code: 200, data: { valid, message: valid ? "五个阶段的优化分析填写完整。" : "请完整填写五个阶段的痛点和优化建议。" } }); } return request({ url: "/api/student-training-answers/consumer-scenario/step-4/validate", method: "post", data: { rows }, headers: { repeatSubmit: false } }); } + +export function checkFeatureConversionStepOne(payload) { + const rows = Array.isArray(payload?.rows) ? payload.rows : []; + if (isStudentDemo()) { const valid = rows.length >= 8 && rows.length <= 12 && rows.every((row) => ["voice", "insight", "definition", "description", "acceptance"].every((key) => String(row?.[key] || "").trim())); return Promise.resolve({ code: 200, data: { valid, message: valid ? "需求转化表填写完整。" : "请完整填写 8 至 12 行需求转化信息。" } }); } + return request({ url: "/api/student-training-answers/feature-conversion/step-1/validate", method: "post", data: { rows }, headers: { repeatSubmit: false } }); +} +export function checkFeatureConversionStepTwo(payload) { + const sourceDefinitions = Array.isArray(payload?.sourceDefinitions) ? payload.sourceDefinitions : []; const rows = Array.isArray(payload?.rows) ? payload.rows : []; + if (isStudentDemo()) { const valid = rows.length >= 8 && rows.length <= 12 && sourceDefinitions.length === rows.length && rows.every((row) => ["reach", "impact", "confidence", "effort"].every((key) => Number.isFinite(Number(row?.[key])))); return Promise.resolve({ code: 200, data: { valid, message: valid ? "RICE 评分填写完整。" : "请完整填写 RICE 评分。" } }); } + return request({ url: "/api/student-training-answers/feature-conversion/step-2/validate", method: "post", data: { sourceDefinitions, rows }, headers: { repeatSubmit: false } }); +} +export function checkFeatureConversionStepThree(payload) { + const scoreRows = Array.isArray(payload?.scoreRows) ? payload.scoreRows : []; const rows = Array.isArray(payload?.rows) ? payload.rows : []; + if (isStudentDemo()) { const valid = scoreRows.length >= 8 && scoreRows.length <= 12 && rows.length === scoreRows.length && rows.every((row, index) => row?.name === scoreRows[index]?.name && ["P0", "P1", "P2", "P3"].includes(row?.priority) && String(row?.reason || "").trim()); return Promise.resolve({ code: 200, data: { valid, message: valid ? "功能优先级填写完整。" : "请完整填写功能优先级与理由。" } }); } + return request({ url: "/api/student-training-answers/feature-conversion/step-3/validate", method: "post", data: { scoreRows, rows }, headers: { repeatSubmit: false } }); +} diff --git a/src/views/demand/feature-conversion.vue b/src/views/demand/feature-conversion.vue index be98f90..5013d93 100644 --- a/src/views/demand/feature-conversion.vue +++ b/src/views/demand/feature-conversion.vue @@ -50,49 +50,13 @@

{{ activeStepName }}

- - - -

需求转化:从“用户语言”到“工程语言”

-

用户往往只描述现象或愿望,研发团队需要将其转化为可执行、可验收的技术指标。

- -
-
需求转化过程示例:
-

1. 场景还原与痛点提炼

-

不要只听用户说“我想要个能陪孩子的机器人”,而要深入场景。

-

用户原话:“孩子在家总看电视,我希望机器人能陪他玩,还能教我孩子学英语。”

-

场景洞察:家长(30-40 岁)双职工,下班晚,担心孩子视力及教育缺失;孩子(3-6 岁)注意力短,需要强互动。

-

转化动作:将“陪玩”转化为多模态交互能力(语音 + 视觉 + 运动);将“学英语”转化为内容生态接入与个性化推荐算法。

-

2. 标准化需求定义(PRD 要素)

-

将洞察转化为开发团队能看懂的文档,必须包含验收标准。

-

需求名称:主动式英语启蒙互动。

-

功能描述:当摄像头识别到孩子注视屏幕超过 10 分钟,或检测到孩子无聊发呆时,机器人主动发起英语对话或游戏邀请。

-

验收标准:

-

识别响应时间 < 1 秒;

-

对话自然度(MOS 分)> 4.0;

-

内容准确率由教育专家评估 > 90%。

-
- -

原始需求基础材料:

-
-
-
家长需求反馈:
-
    -
  1. {{ item }}
  2. -
-
-
-
孩子需求反馈:
-
    -
  1. {{ item }}
  2. -
-
+
+

用户反馈通常混杂重复描述、情绪化表达、无关信息。请阅读案例资料中原始用户反馈,逐条进行场景洞察拆解,分析用户在什么时间、什么情境下产生该需求,背后的深层动机是什么,然后将用户语言转化为可执行的产品功能定义,填写以下需求转化表。

+
+ 增加一行 + 删除一行 +
- -

请根据本题需求,填写以下需求转化表:

@@ -108,15 +72,22 @@ - - - - - + + + + +
{{ index + 1 }}
+

至少 {{ MIN_CONVERSION_ROWS }} 行,最多 {{ MAX_CONVERSION_ROWS }} 行

+
+
可参考以下方法:
+

1. 自行阅读案例信息,将用户原声转化为需求,再通过需求分析转化为产品功能。

+

2. 使用通用 AI 大模型辅助完成场景洞察与功能转化,参考提示词见案例文档。

+
+

{{ conversionValidationMessage }}

@@ -126,74 +97,47 @@

{{ activeStepName }}

-

- 对于智能硬件,建议采用RICE 模型(Reach 覆盖范围、Impact 影响程度、Confidence 置信度、Effort 投入成本)进行量化打分,但必须加一个前置过滤器:合规与安全。并使用KANO模型辅助决策。 -

-

- 必备属性(Must-be):如“跌倒检测”、“语音唤醒”。做不到用户会极度不满,做到了是理所应当。优先保障,不计成本投入稳定性。 -

-

- 期望属性(Performance):如“续航时间”、“识别准确率”。做得越好用户越满意。在 BOM 成本允许范围内最大化。 -

-

- 魅力属性(Delighters):如“情绪识别主动安慰”。没有用户无所谓,有了是惊喜。在核心功能稳定后,作为差异化卖点迭代。 -

-

智能硬件需求优先级评估打分表模板:

- -
- - - - - - - - - - - - - - - - - -
字段说明填写示例来源/依据
{{ row.field }}{{ row.source }}
+
+

将步骤一的功能纳入 RICE 评估模型,进行量化打分。RICE 模型包含覆盖用户数、影响程度、置信度、开发成本四个维度。

+

请根据上一步骤需求转化后的功能定义,针对具体需求进行量化打分,填写以下打分表。

- -

请根据上一步骤需求转化后的功能定义,针对具体需求进行量化打分,填写以下打分表:

- - + - + - - - - - - - - - - - + + + + + + + +
需求ID需求名称需求来源功能名称 KANO属性 Reach(覆盖范围) Impact(影响程度) Confidence(置信度)Effort(投入成本)Effort(人/月) RICE得分合规性检查项
{{ row.requirementId }}{{ row.name || "请先在步骤一填写功能定义" }} + + +
%
+
{{ calculateRiceScore(row) }}
+

计算公式:(Reach × Impact × Confidence) / Effort,结果保留两位小数。需求 ID 与功能名称由步骤一自动同步。

+

{{ scoreValidationMessage }}

@@ -203,29 +147,34 @@

{{ activeStepName }}

-

- 假设项目首期 BOM 成本控制在 300 元以内,研发周期 4 个月。根据上一步骤完成的需求打分表,填写以下内容: -

- - - - - - - - +

基于步骤二的 RICE 评分结果,输出 P0-P3 四级功能清单,并说明每一等级的划分理由。

+
+ + + + + + + + + + + + + + + +
功能名称优先级理由
{{ row.name || "请先完成步骤二 RICE 评分" }} + +
+
+
+

{{ level.value }}{{ level.description }}

+
+

{{ priorityValidationMessage }}

@@ -281,9 +230,18 @@ 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"; +import { checkFeatureConversionStepOne, checkFeatureConversionStepTwo, checkFeatureConversionStepThree, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; const TASK_KEY = "feature-conversion"; +const MIN_CONVERSION_ROWS = 8; +const MAX_CONVERSION_ROWS = 12; +const kanoTypes = ["基本型", "期望型", "兴奋型", "无差异型", "反向型"]; +const priorityLevels = [ + { value: "P0", description: "(首期必做,生存线):不做会导致产品无法上市或差评退货的基本功能。" }, + { value: "P1", description: "(首期核心,竞争力):在成本允许下最大化期望属性,是产品区别于竞品的核心竞争力。" }, + { value: "P2", description: "(二期迭代,差异化):能显著提升用户体验,但成本高或属于不允许的兴奋型功能。" }, + { value: "P3", description: "(暂缓/砍掉):伪需求或偏离核心场景的功能建议。" }, +]; const { proxy } = getCurrentInstance(); const defaultRequirement = @@ -350,9 +308,15 @@ const childFeedback = [ const taskConfig = ref(null); const saving = ref(false); - -function createRows() { - return Array.from({ length: 5 }, (_, index) => ({ +const checkingConversion = ref(false); +const checkingScore = ref(false); +const checkingPriority = ref(false); +const conversionValidationMessage = ref(""); +const scoreValidationMessage = ref(""); +const priorityValidationMessage = ref(""); + +function createRows(count = MIN_CONVERSION_ROWS) { + return Array.from({ length: count }, (_, index) => ({ id: index + 1, voice: "", insight: "", @@ -362,19 +326,25 @@ function createRows() { })); } -function createScoreRows() { - return Array.from({ length: 5 }, (_, index) => ({ +function createScoreRows(sourceRows = createRows()) { + return sourceRows.map((sourceRow, index) => ({ uid: index + 1, - requirementId: "", - name: "", - source: "", + requirementId: formatRequirementId(index), + name: sourceRow.definition || "", kano: "", reach: "", impact: "", confidence: "", effort: "", - rice: "", - compliance: "", + })); +} + +function createPriorityRows(sourceRows = createScoreRows()) { + return sourceRows.map((sourceRow, index) => ({ + uid: index + 1, + name: sourceRow.name || "", + priority: "", + reason: "", })); } @@ -388,11 +358,13 @@ function createPriorityDecision() { } function createForm() { + const rows = createRows(); return { activeStep: 1, productName: "", - rows: createRows(), - scoreRows: createScoreRows(), + rows, + scoreRows: createScoreRows(rows), + priorityRows: createPriorityRows(createScoreRows(rows)), priorityDecision: createPriorityDecision(), }; } @@ -408,7 +380,6 @@ 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" : "" }, @@ -423,14 +394,14 @@ const hasTableContent = computed(() => const hasScoreContent = computed(() => form.value.scoreRows.some((row) => - ["requirementId", "name", "source", "kano", "reach", "impact", "confidence", "effort", "rice", "compliance"].some((key) => + ["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()) + form.value.priorityRows.some((row) => String(row.priority || "").trim() || String(row.reason || "").trim()) ); onMounted(async () => { @@ -482,31 +453,37 @@ function normalizeActiveStep(value) { return Math.min(Math.max(parsed, 1), flowSteps.value.length); } -function setCurrentStep(step) { - form.value.activeStep = normalizeActiveStep(step); +async function setCurrentStep(step) { + const targetStep = normalizeActiveStep(step); + if (targetStep > form.value.activeStep && [1, 2, 3].includes(form.value.activeStep) && !(await persist("IN_PROGRESS"))) return; + if (form.value.activeStep === 1 && targetStep > 1) syncScoreRows(); + if (targetStep > 2 && form.value.activeStep <= 2) syncPriorityRows(); + form.value.activeStep = targetStep; } function isSectionVisible(section) { return !hasConfiguredSteps.value || form.value.activeStep === section; } -function goPreviousStep() { - setCurrentStep(form.value.activeStep - 1); +async function goPreviousStep() { + await setCurrentStep(form.value.activeStep - 1); } -function goNextStep() { - 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: normalizeRows(parsed.rows), - scoreRows: normalizeScoreRows(parsed.scoreRows), + rows, + scoreRows: normalizeScoreRows(parsed.scoreRows, rows), + priorityRows: normalizePriorityRows(parsed.priorityRows, normalizeScoreRows(parsed.scoreRows, rows)), priorityDecision: normalizePriorityDecision(parsed.priorityDecision), activeStep: normalizeActiveStep(parsed.activeStep), }; @@ -517,22 +494,125 @@ function applySavedAnswer(value) { function normalizeRows(rows = []) { const savedRows = Array.isArray(rows) ? rows : []; - return createRows().map((row, index) => ({ + 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] || {}), - id: row.id, + uid: row.uid, + requirementId: row.requirementId, + name: row.name, })); } -function normalizeScoreRows(rows = []) { +function syncScoreRows() { + form.value.scoreRows = normalizeScoreRows(form.value.scoreRows, form.value.rows); +} + +function normalizePriorityRows(rows = [], sourceRows = form.value.scoreRows) { const savedRows = Array.isArray(rows) ? rows : []; - return createScoreRows().map((row, index) => ({ + return createPriorityRows(sourceRows).map((row, index) => ({ ...row, ...(savedRows[index] || {}), uid: row.uid, + name: row.name, })); } +function syncPriorityRows() { + form.value.priorityRows = normalizePriorityRows(form.value.priorityRows, form.value.scoreRows); +} + +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; + } +} + +async function ensurePriorityValid() { + if (checkingPriority.value) return false; + checkingPriority.value = true; + try { + const response = await checkFeatureConversionStepThree({ + scoreRows: form.value.scoreRows.map((row) => ({ ...row, riceScore: calculateRiceScore(row) })), + rows: form.value.priorityRows, + }); + const result = response?.data || {}; + priorityValidationMessage.value = result.message || (result.valid ? "功能优先级清单填写完整。" : "请完成每项功能的优先级和划分理由。"); + return result.valid === true; + } catch (error) { + priorityValidationMessage.value = error?.message || "功能优先级清单校验失败,请稍后重试。"; + return false; + } finally { + checkingPriority.value = false; + } +} + function normalizePriorityDecision(value = {}) { return { ...createPriorityDecision(), @@ -550,6 +630,7 @@ function buildOutcome() { activeStep: form.value.activeStep, rows: form.value.rows, scoreRows: form.value.scoreRows, + priorityRows: form.value.priorityRows, priorityDecision: form.value.priorityDecision, updatedAt: new Date().toISOString(), }; @@ -566,10 +647,14 @@ function buildAnswerPayload(status) { 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; + if (status !== "RESET" && form.value.activeStep === 3 && !(await ensurePriorityValid())) return false; saving.value = true; try { await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status)); proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功"); + return true; } finally { saving.value = false; } @@ -789,6 +874,35 @@ function exportOutcome() { 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; } @@ -878,6 +992,40 @@ function exportOutcome() { 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%; } @@ -1587,6 +1735,204 @@ function exportOutcome() { 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); + } +} + +.priority-decision-table { + width: 100%; + min-width: 860px; + border-collapse: collapse; + table-layout: fixed; + + th, + td { + border: 1px solid rgba(77, 167, 220, 0.2); + text-align: center; + vertical-align: middle; + } + + th { + padding: 12px; + color: #f4fcff; + background: linear-gradient(100deg, #0787bd, #075276); + font-size: 14px; + font-weight: 800; + } + + td { + height: 76px; + padding: 10px; + color: #d7f1ff; + background: rgba(1, 14, 26, 0.48); + } + + th:nth-child(1), + td:nth-child(1) { + width: 22%; + } + + th:nth-child(2), + td:nth-child(2) { + width: 18%; + } + + select { + width: min(100%, 130px); + 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; + } + + textarea { + display: block; + width: 100%; + height: 72px; + min-height: 46px; + padding: 10px; + border: 1px solid rgba(45, 95, 126, 0.95); + border-radius: 10px; + outline: none; + color: #eef5ff; + background: rgba(4, 24, 38, 0.72); + font: inherit; + font-size: 13px; + line-height: 1.55; + resize: vertical; + box-sizing: border-box; + + &:focus { + border-color: rgba(92, 224, 255, 0.7); + box-shadow: 0 0 0 3px rgba(0, 174, 255, 0.13); + } + } +} + +.priority-legend { + display: grid; + gap: 6px; + margin-top: 18px; + padding: 16px 18px; + border: 1px solid rgba(89, 184, 226, 0.22); + border-radius: 14px; + background: rgba(5, 37, 55, 0.44); + + p { + margin: 0; + color: #bfd9e5; + font-size: 13px; + line-height: 1.65; + } + + strong { + display: inline-block; + min-width: 29px; + margin-right: 8px; + color: #68e3ff; + } +} + +.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; @@ -1645,6 +1991,16 @@ function exportOutcome() { .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;