|
|
|
|
@ -50,49 +50,13 @@
|
|
|
|
|
<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">识别响应时间 < 1 秒;</p>
|
|
|
|
|
<p class="indent">对话自然度(MOS 分)> 4.0;</p>
|
|
|
|
|
<p class="indent">内容准确率由教育专家评估 > 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 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>
|
|
|
|
|
|
|
|
|
|
<p class="table-tip">请根据本题需求,填写以下需求转化表:</p>
|
|
|
|
|
<div class="conversion-table-wrap">
|
|
|
|
|
<table class="conversion-table">
|
|
|
|
|
<thead>
|
|
|
|
|
@ -108,15 +72,22 @@
|
|
|
|
|
<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>
|
|
|
|
|
<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">
|
|
|
|
|
@ -126,74 +97,47 @@
|
|
|
|
|
<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 class="rice-intro">
|
|
|
|
|
<p>将步骤一的功能纳入 RICE 评估模型,进行量化打分。RICE 模型包含覆盖用户数、影响程度、置信度、开发成本四个维度。</p>
|
|
|
|
|
<p>请根据上一步骤需求转化后的功能定义,针对具体需求进行量化打分,填写以下打分表。</p>
|
|
|
|
|
</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>功能名称</th>
|
|
|
|
|
<th>KANO属性</th>
|
|
|
|
|
<th>Reach(覆盖范围)</th>
|
|
|
|
|
<th>Impact(影响程度)</th>
|
|
|
|
|
<th>Confidence(置信度)</th>
|
|
|
|
|
<th>Effort(投入成本)</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>
|
|
|
|
|
<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">
|
|
|
|
|
@ -203,29 +147,34 @@
|
|
|
|
|
<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>
|
|
|
|
|
<p class="decision-intro">基于步骤二的 RICE 评分结果,输出 P0-P3 四级功能清单,并说明每一等级的划分理由。</p>
|
|
|
|
|
<div class="conversion-table-wrap">
|
|
|
|
|
<table class="priority-decision-table">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>功能名称</th>
|
|
|
|
|
<th>优先级</th>
|
|
|
|
|
<th>理由</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr v-for="row in form.priorityRows" :key="row.uid">
|
|
|
|
|
<td class="auto-score-cell auto-score-cell--name">{{ row.name || "请先完成步骤二 RICE 评分" }}</td>
|
|
|
|
|
<td>
|
|
|
|
|
<select v-model="row.priority" :aria-label="`${row.name || row.uid} 优先级`">
|
|
|
|
|
<option value="">请选择</option>
|
|
|
|
|
<option v-for="level in priorityLevels" :key="level.value" :value="level.value">{{ level.value }}</option>
|
|
|
|
|
</select>
|
|
|
|
|
</td>
|
|
|
|
|
<td><textarea v-model="row.reason" :aria-label="`${row.name || row.uid} 划分理由`" placeholder="结合 RICE 评分、KANO 属性和开发投入说明划分理由"></textarea></td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="priority-legend" aria-label="优先级说明">
|
|
|
|
|
<p v-for="level in priorityLevels" :key="level.value"><strong>{{ level.value }}</strong>{{ level.description }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<p v-if="priorityValidationMessage" class="conversion-validation-message" role="status">{{ priorityValidationMessage }}</p>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-show="isSectionVisible(4)" class="step-card conversion-content completion-content">
|
|
|
|
|
@ -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;
|
|
|
|
|
|