|
|
<template>
|
|
|
<div class="feature-conversion-page student-training-shell">
|
|
|
<main class="feature-conversion-core">
|
|
|
<section class="task-header">
|
|
|
<div class="task-head-main">
|
|
|
<div class="task-badge">市场洞察与需求分析</div>
|
|
|
<h1 class="task-title">{{ taskTitle }}</h1>
|
|
|
<p class="task-brief">
|
|
|
<span class="task-brief-text">{{ taskBackground }}</span>
|
|
|
<button type="button" class="task-brief-toggle" @click="briefOpen = !briefOpen">
|
|
|
{{ briefOpen ? "收起" : "展开目标与要求" }}
|
|
|
</button>
|
|
|
<span v-if="briefOpen" class="task-brief-more">
|
|
|
<b>实训目标</b>
|
|
|
<ul><li v-for="goal in taskGoals" :key="goal">{{ goal }}</li></ul>
|
|
|
<b>实训要求</b>
|
|
|
<p>{{ taskRequirement }}</p>
|
|
|
</span>
|
|
|
</p>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<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"
|
|
|
:training-data-name="taskConfig?.trainingDataName"
|
|
|
:training-data-url="taskConfig?.trainingDataUrl"
|
|
|
:guide-manual-name="taskConfig?.guideManualName"
|
|
|
:guide-manual-url="taskConfig?.guideManualUrl"
|
|
|
:ai-manual-name="taskConfig?.aiManualName"
|
|
|
:ai-manual-url="taskConfig?.aiManualUrl"
|
|
|
/>
|
|
|
<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">基于步骤二的 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">
|
|
|
<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 { getTrainingTaskByKey } from "@/api/trainingTask";
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap";
|
|
|
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 =
|
|
|
"需求转化不是照搬用户原话,而是把模糊痛点翻译成可设计、可开发、可验收的功能。过程包括需求清洗、场景还原、功能定义、验收标准和优先级排序。";
|
|
|
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 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: "",
|
|
|
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 createPriorityRows(sourceRows = createScoreRows()) {
|
|
|
return sourceRows.map((sourceRow, index) => ({
|
|
|
uid: index + 1,
|
|
|
name: sourceRow.name || "",
|
|
|
priority: "",
|
|
|
reason: "",
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
function createPriorityDecision() {
|
|
|
return {
|
|
|
p0: "",
|
|
|
p1: "",
|
|
|
p2: "",
|
|
|
p3: "",
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function createForm() {
|
|
|
const rows = createRows();
|
|
|
return {
|
|
|
activeStep: 1,
|
|
|
productName: "",
|
|
|
rows,
|
|
|
scoreRows: createScoreRows(rows),
|
|
|
priorityRows: createPriorityRows(createScoreRows(rows)),
|
|
|
priorityDecision: createPriorityDecision(),
|
|
|
};
|
|
|
}
|
|
|
|
|
|
const form = ref(createForm());
|
|
|
|
|
|
const taskTitle = computed(() => taskConfig.value?.taskName || "需求转化为功能");
|
|
|
const briefOpen = ref(false);
|
|
|
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(() => parseTrainingArray(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(() =>
|
|
|
form.value.priorityRows.some((row) => String(row.priority || "").trim() || String(row.reason || "").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, 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;
|
|
|
}
|
|
|
|
|
|
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),
|
|
|
priorityRows: normalizePriorityRows(parsed.priorityRows, 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 normalizePriorityRows(rows = [], sourceRows = form.value.scoreRows) {
|
|
|
const savedRows = Array.isArray(rows) ? rows : [];
|
|
|
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(),
|
|
|
...(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,
|
|
|
priorityRows: form.value.priorityRows,
|
|
|
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;
|
|
|
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;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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 {
|
|
|
--student-accent: #2563eb;
|
|
|
--student-accent-deep: #1d4ed8;
|
|
|
--student-accent-light: #3b82f6;
|
|
|
--student-accent-soft: #eff6ff;
|
|
|
--student-danger: #ef4444;
|
|
|
--student-success: #2ba471;
|
|
|
--student-layer-1: #1d4ed8;
|
|
|
--student-layer-2: #3b82f6;
|
|
|
--student-layer-3: #93c5fd;
|
|
|
--student-panel-border: #e5e7eb;
|
|
|
--student-card-bg: #ffffff;
|
|
|
--student-panel-bg: #fafbfc;
|
|
|
--student-text: #1f2937;
|
|
|
--student-text-secondary: #6b7280;
|
|
|
--el-color-primary: #2563eb;
|
|
|
--el-color-primary-light-3: #60a5fa;
|
|
|
--el-color-primary-light-5: #93c5fd;
|
|
|
--el-color-primary-light-7: #bfdbfe;
|
|
|
--el-color-primary-light-8: #dbeafe;
|
|
|
--el-color-primary-light-9: #eff6ff;
|
|
|
--el-color-primary-dark-2: #1d4ed8;
|
|
|
--el-input-hover-border-color: #3b82f6;
|
|
|
--el-input-focus-border-color: #2563eb;
|
|
|
--el-input-focus-ring-color: rgba(59, 130, 246, 0.15);
|
|
|
--el-textarea-hover-border-color: #3b82f6;
|
|
|
--el-textarea-focus-border-color: #2563eb;
|
|
|
--student-fs-display: 17px;
|
|
|
--student-fs-h1: 15px;
|
|
|
--student-fs-h2: 14px;
|
|
|
--student-fs-h3: 13px;
|
|
|
--student-fs-body: 13px;
|
|
|
--student-fs-body-sm: 12px;
|
|
|
--student-fs-caption: 11px;
|
|
|
--student-fs-button: 13px;
|
|
|
--student-fs-button-lg: 13px;
|
|
|
--student-fs-data: 14px;
|
|
|
|
|
|
position: relative;
|
|
|
z-index: 2;
|
|
|
display: grid;
|
|
|
grid-template-columns: minmax(720px, 1fr) 340px;
|
|
|
gap: 24px;
|
|
|
width: 100%;
|
|
|
max-width: 100%;
|
|
|
min-height: calc(100vh - 106px);
|
|
|
margin: 0;
|
|
|
padding: 24px 24px 32px;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
/* 对齐 foundation 的 .training-core:只设 gap,无边框/背景/阴影 */
|
|
|
.feature-conversion-core {
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
align-content: start;
|
|
|
min-width: 0;
|
|
|
border: 0;
|
|
|
background: transparent;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
/* 任务头:独立卡片 + 左侧教育蓝竖条 */
|
|
|
.task-header {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 16px;
|
|
|
margin: 0;
|
|
|
padding: 18px 20px 18px 24px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: var(--student-card-bg);
|
|
|
overflow: hidden;
|
|
|
|
|
|
&::before {
|
|
|
content: "";
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
bottom: 0;
|
|
|
left: 0;
|
|
|
width: 4px;
|
|
|
background: var(--student-accent);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.task-head-main {
|
|
|
display: grid;
|
|
|
justify-items: start;
|
|
|
gap: 8px;
|
|
|
min-width: 0;
|
|
|
flex: 1 1 auto;
|
|
|
}
|
|
|
|
|
|
.task-badge {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: 7px;
|
|
|
padding: 4px 12px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 999px;
|
|
|
color: var(--student-accent-deep);
|
|
|
background: var(--student-accent-soft);
|
|
|
font-size: 12px;
|
|
|
font-weight: 700;
|
|
|
text-transform: none;
|
|
|
letter-spacing: 0;
|
|
|
}
|
|
|
|
|
|
.task-title {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-display);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
.task-brief {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: 12px;
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.task-brief-text {
|
|
|
display: -webkit-box;
|
|
|
overflow: hidden;
|
|
|
-webkit-box-orient: vertical;
|
|
|
-webkit-line-clamp: 1;
|
|
|
}
|
|
|
|
|
|
.task-brief-toggle {
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
color: var(--student-accent-deep);
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover { text-decoration: underline; }
|
|
|
}
|
|
|
|
|
|
.task-brief-more {
|
|
|
display: grid;
|
|
|
gap: 6px;
|
|
|
margin-top: 8px;
|
|
|
padding-top: 8px;
|
|
|
border-top: 1px solid var(--student-panel-border);
|
|
|
|
|
|
b { color: var(--student-text); font-size: 12px; }
|
|
|
ul { display: grid; gap: 4px; margin: 0; padding-left: 18px; }
|
|
|
p { margin: 0; }
|
|
|
}
|
|
|
|
|
|
/* task-card / step-card 层:透明容器 */
|
|
|
.task-card,
|
|
|
.step-card {
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
/* ===== 顶部任务工具栏 ===== */
|
|
|
.workbench-head {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 16px;
|
|
|
}
|
|
|
|
|
|
.workbench-head--actions-only {
|
|
|
justify-content: flex-end;
|
|
|
margin-bottom: 18px;
|
|
|
padding-bottom: 0;
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.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-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.45);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
font-size: 13px;
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
text-shadow: none;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.outline-action:hover,
|
|
|
.outline-action:focus-visible {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
/* ===== step-title-row + section-eyebrow ===== */
|
|
|
.step-title-row {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: flex-start;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.step-title-row h3 {
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
.section-eyebrow {
|
|
|
margin: 0 0 6px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-weight: 700;
|
|
|
font-size: 11px;
|
|
|
letter-spacing: 0.12em;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
/* ===== 引导区:research-guide 同款 ===== */
|
|
|
.conversion-intro,
|
|
|
.rice-intro,
|
|
|
.decision-intro {
|
|
|
padding: 12px 14px;
|
|
|
border-left: 3px solid var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.conversion-intro {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 16px;
|
|
|
|
|
|
> p {
|
|
|
max-width: 840px;
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.rice-intro p {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.rice-intro p + p {
|
|
|
margin-top: 5px;
|
|
|
}
|
|
|
|
|
|
.decision-intro {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
}
|
|
|
|
|
|
.conversion-row-actions {
|
|
|
display: flex;
|
|
|
flex: 0 0 auto;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
/* ===== 表格 ===== */
|
|
|
.conversion-table-wrap {
|
|
|
overflow-x: auto;
|
|
|
}
|
|
|
|
|
|
.conversion-table,
|
|
|
.priority-score-table,
|
|
|
.priority-decision-table {
|
|
|
width: 100%;
|
|
|
border-collapse: collapse;
|
|
|
table-layout: fixed;
|
|
|
}
|
|
|
|
|
|
.conversion-table { min-width: 1120px; }
|
|
|
.priority-score-table { min-width: 1100px; }
|
|
|
.priority-decision-table { min-width: 860px; }
|
|
|
|
|
|
.conversion-table th,
|
|
|
.conversion-table td,
|
|
|
.priority-score-table th,
|
|
|
.priority-score-table td,
|
|
|
.priority-decision-table th,
|
|
|
.priority-decision-table td {
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
}
|
|
|
|
|
|
.conversion-table th,
|
|
|
.priority-score-table th,
|
|
|
.priority-decision-table th {
|
|
|
padding: 10px 12px;
|
|
|
color: var(--student-text);
|
|
|
background: #fafbfc;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 800;
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
.conversion-table td,
|
|
|
.priority-score-table td,
|
|
|
.priority-decision-table td {
|
|
|
padding: 10px;
|
|
|
background: transparent;
|
|
|
vertical-align: top;
|
|
|
color: var(--student-text);
|
|
|
}
|
|
|
|
|
|
.conversion-table th:first-child,
|
|
|
.conversion-table td:first-child {
|
|
|
width: 72px;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.conversion-table textarea {
|
|
|
display: block;
|
|
|
width: 100%;
|
|
|
height: 78px;
|
|
|
padding: 9px 10px;
|
|
|
border: 0;
|
|
|
outline: none;
|
|
|
resize: vertical;
|
|
|
color: var(--student-text);
|
|
|
background: transparent;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.55;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
.conversion-table textarea::placeholder {
|
|
|
color: var(--student-text-secondary);
|
|
|
}
|
|
|
|
|
|
.conversion-table textarea:focus,
|
|
|
.priority-score-table textarea:focus,
|
|
|
.priority-decision-table textarea:focus {
|
|
|
outline: 2px solid rgba(37, 99, 235, 0.15);
|
|
|
outline-offset: -2px;
|
|
|
}
|
|
|
|
|
|
/* ===== priority-score-table ===== */
|
|
|
.priority-score-table th,
|
|
|
.priority-score-table td {
|
|
|
text-align: center;
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
|
|
|
.priority-score-table td {
|
|
|
height: 66px;
|
|
|
padding: 8px;
|
|
|
}
|
|
|
|
|
|
.priority-score-table .auto-score-cell {
|
|
|
color: var(--student-text-secondary);
|
|
|
background: transparent;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.priority-score-table .auto-score-cell--name {
|
|
|
min-width: 180px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.55;
|
|
|
word-break: break-word;
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
.priority-score-table select,
|
|
|
.priority-score-table input {
|
|
|
width: 100%;
|
|
|
min-height: 36px;
|
|
|
padding: 0 9px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 8px;
|
|
|
outline: none;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
.priority-score-table select:focus,
|
|
|
.priority-score-table input:focus {
|
|
|
outline: 2px solid rgba(37, 99, 235, 0.15);
|
|
|
outline-offset: -2px;
|
|
|
border-color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
.priority-score-table .metric-input {
|
|
|
position: relative;
|
|
|
|
|
|
input {
|
|
|
padding-right: 24px;
|
|
|
}
|
|
|
|
|
|
span {
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
right: 9px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
transform: translateY(-50%);
|
|
|
pointer-events: none;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.priority-score-table .rice-score-cell {
|
|
|
color: var(--student-accent);
|
|
|
font-size: var(--student-fs-data);
|
|
|
font-weight: 700;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
/* ===== priority-decision-table ===== */
|
|
|
.priority-decision-table th,
|
|
|
.priority-decision-table td {
|
|
|
text-align: center;
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
|
|
|
.priority-decision-table td {
|
|
|
height: 76px;
|
|
|
padding: 10px;
|
|
|
color: var(--student-text);
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
.priority-decision-table th:nth-child(1),
|
|
|
.priority-decision-table td:nth-child(1) {
|
|
|
width: 22%;
|
|
|
}
|
|
|
|
|
|
.priority-decision-table th:nth-child(2),
|
|
|
.priority-decision-table td:nth-child(2) {
|
|
|
width: 18%;
|
|
|
}
|
|
|
|
|
|
.priority-decision-table .auto-score-cell--name {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.55;
|
|
|
word-break: break-word;
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
.priority-decision-table select {
|
|
|
width: min(100%, 130px);
|
|
|
min-height: 36px;
|
|
|
padding: 0 9px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 8px;
|
|
|
outline: none;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
font: inherit;
|
|
|
}
|
|
|
|
|
|
.priority-decision-table select:focus {
|
|
|
outline: 2px solid rgba(37, 99, 235, 0.15);
|
|
|
outline-offset: -2px;
|
|
|
border-color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
.priority-decision-table textarea {
|
|
|
display: block;
|
|
|
width: 100%;
|
|
|
height: 72px;
|
|
|
min-height: 46px;
|
|
|
padding: 10px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
outline: none;
|
|
|
color: var(--student-text);
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.55;
|
|
|
resize: vertical;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
.priority-decision-table textarea:focus {
|
|
|
outline: 2px solid rgba(37, 99, 235, 0.15);
|
|
|
outline-offset: -2px;
|
|
|
border-color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
/* ===== 辅助块 ===== */
|
|
|
.conversion-helper {
|
|
|
margin-top: 14px;
|
|
|
padding: 12px 14px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-text-secondary);
|
|
|
background: var(--student-accent-soft);
|
|
|
|
|
|
h5 {
|
|
|
margin: 0 0 6px;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
p + p { margin-top: 4px; }
|
|
|
}
|
|
|
|
|
|
.priority-legend {
|
|
|
display: grid;
|
|
|
gap: 6px;
|
|
|
margin-top: 14px;
|
|
|
padding: 12px 14px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 8px;
|
|
|
background: var(--student-accent-soft);
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
strong {
|
|
|
display: inline-block;
|
|
|
min-width: 29px;
|
|
|
margin-right: 8px;
|
|
|
color: var(--student-accent);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 消息提示 ===== */
|
|
|
.conversion-validation-message {
|
|
|
margin: 10px 0 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.conversion-limit-tip,
|
|
|
.rice-formula {
|
|
|
margin: 8px 0 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
/* ===== completion-content ===== */
|
|
|
.completion-content p {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
/* ===== 底部步骤导航按钮 ===== */
|
|
|
.step-navigation {
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
gap: 14px;
|
|
|
margin-top: 20px;
|
|
|
}
|
|
|
|
|
|
.btn-nav {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 8px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.45);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
text-shadow: none;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, transform 0.16s ease, box-shadow 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.btn-nav:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-accent);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
|
|
|
.btn-nav.btn-primary {
|
|
|
border-color: var(--student-accent);
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font-weight: 700;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28);
|
|
|
}
|
|
|
|
|
|
.btn-nav.btn-primary:hover:not(:disabled) {
|
|
|
background: var(--student-accent-deep);
|
|
|
border-color: var(--student-accent-deep);
|
|
|
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32);
|
|
|
}
|
|
|
|
|
|
.btn-nav.btn-save {
|
|
|
border-color: #e5e7eb;
|
|
|
color: var(--student-accent-light);
|
|
|
background: var(--student-accent-soft);
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.btn-nav.btn-save:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: #e9ecef;
|
|
|
color: var(--student-accent);
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.btn-nav:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
transform: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
/* training-actions 容器:保留,el-button 由全局样式接管 */
|
|
|
.training-actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: center;
|
|
|
gap: 14px;
|
|
|
margin-top: 20px;
|
|
|
}
|
|
|
|
|
|
/* ===== 响应式 ===== */
|
|
|
@media (max-width: 900px) {
|
|
|
.workbench-head,
|
|
|
.workbench-actions {
|
|
|
align-items: stretch;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.outline-action,
|
|
|
.step-navigation,
|
|
|
.btn-nav {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.step-navigation {
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.conversion-intro,
|
|
|
.conversion-row-actions {
|
|
|
align-items: stretch;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.conversion-row-actions .el-button {
|
|
|
width: 100%;
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<style lang="scss">
|
|
|
/* ============================================================
|
|
|
* 步骤导航:分段连接胶囊(教育蓝极简)
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .step-indicator {
|
|
|
display: flex !important;
|
|
|
flex-wrap: nowrap !important;
|
|
|
align-items: stretch !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 0 !important;
|
|
|
width: 100% !important;
|
|
|
margin: 0 0 20px !important;
|
|
|
padding: 0 !important;
|
|
|
border: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: transparent !important;
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
.step-connector { display: none !important; }
|
|
|
|
|
|
.step-tab {
|
|
|
flex: 1 1 0 !important;
|
|
|
max-width: 240px !important;
|
|
|
display: flex !important;
|
|
|
flex-direction: row !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 8px !important;
|
|
|
min-width: 0 !important;
|
|
|
height: auto !important;
|
|
|
min-height: 40px !important;
|
|
|
margin: 0 !important;
|
|
|
padding: 8px 14px !important;
|
|
|
border: 1px solid #e5e7eb !important;
|
|
|
border-right: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1.3 !important;
|
|
|
white-space: normal !important;
|
|
|
text-align: center !important;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease !important;
|
|
|
|
|
|
&:first-child {
|
|
|
border-radius: 8px 0 0 8px !important;
|
|
|
}
|
|
|
&:last-child {
|
|
|
border-radius: 0 8px 8px 0 !important;
|
|
|
border-right: 1px solid #e5e7eb !important;
|
|
|
}
|
|
|
|
|
|
.step-marker {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
min-width: 22px !important;
|
|
|
height: 22px !important;
|
|
|
padding: 0 5px !important;
|
|
|
border-radius: 6px !important;
|
|
|
background: #f3f4f6 !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-family: var(--student-font-data), ui-monospace, "SF Mono", Consolas, monospace !important;
|
|
|
font-size: 12px !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: all 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
.step-copy {
|
|
|
display: flex !important;
|
|
|
flex-direction: column !important;
|
|
|
gap: 0 !important;
|
|
|
min-width: 0 !important;
|
|
|
}
|
|
|
|
|
|
.step-name {
|
|
|
overflow: hidden !important;
|
|
|
text-overflow: ellipsis !important;
|
|
|
white-space: normal !important;
|
|
|
line-height: 1.3 !important;
|
|
|
color: inherit !important;
|
|
|
}
|
|
|
|
|
|
.step-action { display: none !important; }
|
|
|
|
|
|
&:hover:not(.active):not(.complete) {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
color: #2563eb !important;
|
|
|
z-index: 1 !important;
|
|
|
.step-marker {
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.complete:not(.active) {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #374151 !important;
|
|
|
.step-marker {
|
|
|
background: rgba(43, 164, 113, 0.12) !important;
|
|
|
color: #16915f !important;
|
|
|
}
|
|
|
&:hover {
|
|
|
border-color: #2ba471 !important;
|
|
|
background: #f0fdf6 !important;
|
|
|
z-index: 1 !important;
|
|
|
.step-marker {
|
|
|
background: #2ba471 !important;
|
|
|
color: #ffffff !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.active {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
font-weight: 700 !important;
|
|
|
z-index: 2 !important;
|
|
|
.step-marker {
|
|
|
background: #ffffff !important;
|
|
|
color: #2563eb !important;
|
|
|
}
|
|
|
&:hover {
|
|
|
background: #1d4ed8 !important;
|
|
|
border-color: #1d4ed8 !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 窄屏:横向滑动 */
|
|
|
@media (max-width: 900px) {
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .step-indicator {
|
|
|
justify-content: flex-start !important;
|
|
|
overflow-x: auto !important;
|
|
|
}
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .step-indicator .step-tab {
|
|
|
flex: 0 0 140px !important;
|
|
|
max-width: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 共用组件换肤:TrainingMaterialButton 资料按钮 → 教育蓝
|
|
|
* ============================================================ */
|
|
|
.feature-conversion-page .workbench-actions .material-button.el-button {
|
|
|
height: 36px !important;
|
|
|
min-height: 36px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.45) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
|
|
|
&.is-plain {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #9ca3af !important;
|
|
|
&:hover {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #9ca3af !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 顶部导出按钮(原生 <button.outline-action>)
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .workbench-actions > button.outline-action {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 6px !important;
|
|
|
height: 36px !important;
|
|
|
min-height: 36px !important;
|
|
|
padding: 0 14px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.45) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .workbench-actions > button.outline-action:hover {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
transform: none !important;
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 底部步骤按钮(40px 标准):task-actions > button
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .task-actions {
|
|
|
gap: 14px !important;
|
|
|
margin-top: 20px !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .task-actions > button {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 8px !important;
|
|
|
height: 40px !important;
|
|
|
min-height: 40px !important;
|
|
|
padding: 0 18px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.45) !important;
|
|
|
border-radius: 10px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .task-actions > button:hover:not(:disabled) {
|
|
|
transform: translateY(-1px) !important;
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .task-actions > button.btn-submit {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
background: #2563eb !important;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .task-actions > button.btn-submit:hover:not(:disabled) {
|
|
|
border-color: #1d4ed8 !important;
|
|
|
color: #ffffff !important;
|
|
|
background: #1d4ed8 !important;
|
|
|
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32) !important;
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 顶部任务工具栏(TrainingTaskToolbar 组件)
|
|
|
* ============================================================ */
|
|
|
.feature-conversion-page .task-toolbar-button.el-button {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 6px !important;
|
|
|
min-height: 34px !important;
|
|
|
height: 34px !important;
|
|
|
padding: 0 12px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.45) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
|
|
|
&:hover:not(.is-disabled) {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
transform: translateY(-1px) !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.feature-conversion-page .task-toolbar-button--submit {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.35), 0 3px 10px rgba(37, 99, 235, 0.22) !important;
|
|
|
&:hover:not(.is-disabled) {
|
|
|
background: #1d4ed8 !important;
|
|
|
box-shadow: 0 2px 4px rgba(37, 99, 235, 0.4), 0 5px 14px rgba(29, 78, 216, 0.28) !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 底部提交/清空按钮:training-actions
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .training-actions {
|
|
|
gap: 14px !important;
|
|
|
margin-top: 20px !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .training-actions .el-button.training-action {
|
|
|
min-width: 140px !important;
|
|
|
min-height: 36px !important;
|
|
|
height: 36px !important;
|
|
|
padding: 0 14px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.45) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
letter-spacing: 0 !important;
|
|
|
text-shadow: none !important;
|
|
|
box-shadow: none !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .training-actions .el-button.training-action:hover:not(.is-disabled) {
|
|
|
transform: translateY(-1px) !important;
|
|
|
border-color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
color: #2563eb !important;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .training-actions .el-button.training-action--submit {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
background: #2563eb !important;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .training-actions .el-button.training-action--submit:hover:not(.is-disabled) {
|
|
|
border-color: #1d4ed8 !important;
|
|
|
background: #1d4ed8 !important;
|
|
|
color: #ffffff !important;
|
|
|
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .training-actions .el-button.training-action--reset {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #374151 !important;
|
|
|
background: #f3f4f6 !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.feature-conversion-page) .training-actions .el-button.training-action--reset:hover:not(.is-disabled) {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
}
|
|
|
</style>
|