You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/foundation/product-development-factors...

1157 lines
40 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="product-factors student-training-shell">
<main class="factors-core">
<section class="task-header">
<div class="task-head-main">
<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-action-strip">
<TrainingTaskToolbar
:task-key="TASK_KEY"
: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"
:on-export="exportOutcome"
/>
</section>
<section class="factors-workbench">
<nav v-if="trainingSteps.length" class="step-indicator" aria-label="实训步骤">
<button
v-for="(step, index) in trainingSteps"
:key="`${step}-${index}`"
type="button"
:class="['step-tab', { active: currentStep === index, complete: currentStep > index }]"
:aria-label="`切换至第 ${index + 1} 步:${step}`"
:aria-current="currentStep === index ? 'step' : undefined"
:title="step"
@click="currentStep = index"
>
<span class="step-bar" aria-hidden="true"></span>
<span class="step-no" aria-hidden="true">{{ String(index + 1).padStart(2, '0') }}</span>
<span class="step-name">{{ step }}</span>
</button>
</nav>
<section class="factor-step-card">
<div v-if="trainingSteps.length" class="step-title-row">
<div>
<p class="section-eyebrow">第 {{ String(currentStep + 1).padStart(2, '0') }} 步</p>
<h3>{{ trainingSteps[currentStep] }}</h3>
</div>
</div>
<template v-if="currentStep === 0">
<p class="factor-intro">用户角度分析,填写下表:</p>
<div class="training-table-wrap">
<table class="factor-table">
<thead>
<tr>
<th class="dimension-col">维度</th>
<th class="criteria-col">评估项</th>
<th>成功产品</th>
<th>失败产品</th>
</tr>
</thead>
<tbody>
<tr v-for="row in stepOneRows" :key="row.dimension">
<td class="dimension-cell">{{ row.dimension }}</td>
<td class="criteria-cell">{{ row.criteria }}</td>
<td>
<textarea
v-model="row.successAnalysis"
class="table-textarea"
placeholder="填写成功产品在该维度的分析"
/>
</td>
<td>
<textarea
v-model="row.failedAnalysis"
class="table-textarea"
placeholder="填写失败产品在该维度的分析"
/>
</td>
</tr>
</tbody>
</table>
</div>
<div class="task-actions">
<button type="button" class="btn-nav btn-submit" :disabled="saving" @click="saveAndGoToStepTwo">
保存并进入第 2 步
<el-icon><ArrowRight /></el-icon>
</button>
<button type="button" class="btn-nav" :disabled="saving" @click="resetTraining">
<el-icon><RefreshLeft /></el-icon>
清空填写
</button>
</div>
</template>
<template v-else-if="currentStep === 1">
<p class="factor-intro">技术与合规角度分析,填写下表:</p>
<div class="training-table-wrap"><table class="factor-table"><thead><tr><th class="dimension-col">维度</th><th class="criteria-col">评估项</th><th>成功产品</th><th>失败产品</th></tr></thead><tbody>
<tr v-for="row in stepTwoRows" :key="row.dimension"><td class="dimension-cell">{{ row.dimension }}</td><td class="criteria-cell">{{ row.criteria }}</td><td><textarea v-model="row.successAnalysis" class="table-textarea" placeholder="填写成功产品在该维度的分析" /></td><td><textarea v-model="row.failedAnalysis" class="table-textarea" placeholder="填写失败产品在该维度的分析" /></td></tr>
</tbody></table></div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving" @click="saveAndGoToStepThree">保存并进入第 3 步<el-icon><ArrowRight /></el-icon></button><button type="button" class="btn-nav" :disabled="saving" @click="resetTraining"><el-icon><RefreshLeft /></el-icon>清空填写</button></div>
</template>
<template v-else-if="currentStep === 2">
<p class="factor-intro">商业化及市场反馈角度分析,填写下表:</p>
<div class="training-table-wrap"><table class="factor-table"><thead><tr><th class="dimension-col">维度</th><th class="criteria-col">评估项</th><th>成功产品</th><th>失败产品</th></tr></thead><tbody>
<tr v-for="row in stepThreeRows" :key="row.dimension"><td class="dimension-cell">{{ row.dimension }}</td><td class="criteria-cell">{{ row.criteria }}</td><td><textarea v-model="row.successAnalysis" class="table-textarea" placeholder="填写成功产品在该维度的分析" /></td><td><textarea v-model="row.failedAnalysis" class="table-textarea" placeholder="填写失败产品在该维度的分析" /></td></tr>
</tbody></table></div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving" @click="saveAndGoToStepFour">保存并进入第 4 步<el-icon><ArrowRight /></el-icon></button><button type="button" class="btn-nav" :disabled="saving" @click="resetTraining"><el-icon><RefreshLeft /></el-icon>清空填写</button></div>
</template>
<template v-else-if="currentStep === 3">
<p class="factor-intro">结合案例信息及前述步骤,分析案例资料中产品一成功的关键因素是什么,产品二失败的关键因素是什么。</p>
<label class="factor-intro">产品成功的关键因素:</label><textarea v-model="stepFourForm.successFactors" class="table-textarea summary-textarea" />
<label class="factor-intro">产品失败的关键因素:</label><textarea v-model="stepFourForm.failedFactors" class="table-textarea summary-textarea" />
</template>
<div v-else class="step-placeholder">
<p>第 {{ currentStep + 1 }} 步内容待配置</p>
<span>已保存的分析可从上方步骤栏返回查看。</span>
</div>
</section>
</section>
</main>
<TrainingAiSidebar
study-tip="当前任务请从用户角度对成功与失败产品完成三项对比分析。"
reference-title="关键因素分析参考"
reference-text="· 用户需求真实性:是否高频、真实、可付费<br />· 产品体验闭环:是否易用、稳定、反馈清晰<br />· 生态与兼容:是否支持主流平台与生态扩展"
assist-text="请围绕每项评估要求,分别说明成功产品与失败产品的表现。"
:progress-items="progressItems"
/>
</div>
</template>
<script setup>
import { ArrowRight, CircleCheck, Operation, RefreshLeft } from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { checkProductDevelopmentFactorsStepOne, checkProductDevelopmentFactorsStepTwo, checkProductDevelopmentFactorsStepThree, checkProductDevelopmentFactorsStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import useUserStore from "@/store/modules/user";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const { proxy } = getCurrentInstance();
const TASK_KEY = "product-development-factors";
const userStore = useUserStore();
const taskConfig = ref(null);
const saving = ref(false);
const draftReady = ref(false);
const currentStep = ref(0);
const briefOpen = ref(false);
const getLocalDraftKey = () => {
const userInfo = userStore.userInfo || {};
const userKey = userInfo.userId || userInfo.user_id || userInfo.username || userInfo.userName || "anonymous";
return `${TASK_KEY}:draft:${userKey}`;
};
const defaultTask = {
title: "产品开发关键因素",
background:
"智能产品开发不能只看技术亮点,还需要验证用户是否愿意为功能付费,产品是否稳定易用,供应链是否可控,商业模式是否清晰,并持续结合市场反馈迭代。",
goals: [
"理解产品开发成功与失败的核心判断维度。",
"能够结合真实产品案例,分析关键成功因素和失败原因。",
"形成面向后续产品规划、测试验证和上线运营的判断依据。",
],
requirement: "请选择上一实训任务某款产品作为成功产品,分析关键成功因素,再分析一款失败产品原因。",
};
const taskTitle = computed(() => taskConfig.value?.taskName || defaultTask.title);
const taskBackground = computed(() => taskConfig.value?.background || defaultTask.background);
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultTask.goals));
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultTask.requirement);
const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, []).slice(0, 4));
const STEP_ONE_FACTOR_ROWS = [
{
dimension: "用户需求真实性",
criteria: "是否解决真实、高频、可付费的痛点,用户愿为该功能支付溢价,非“技术炫技”或“伪需求”",
},
{
dimension: "产品体验闭环",
criteria: "操作是否无需说明书,“零学习成本”?是否操作简单、反馈明确、稳定性高",
},
{
dimension: "生态与兼容",
criteria:
"是否能接入主流平台?是否支持iOS/Android、HomeKit、米家、Alexa等生态?是否支持第三方开发者扩展,提供API或SDK,鼓励生态共建?",
},
];
const STEP_TWO_FACTOR_ROWS = [
{ dimension: "技术应用", criteria: "核心技术是否已成熟、可量产?避免使用实验室级原型(如未验证的 AI 模型)" },
{ dimension: "合规与认证", criteria: "是否通过目标市场强制认证?目标市场要求的产品资质、检测报告等中国:CCC;美国:FCC;欧盟:CE;出口需符合 RoHS、REACH。是否规避专利与知识产权风险?" },
];
const STEP_THREE_FACTOR_ROWS = [
{ dimension: "供应链与成本控制", criteria: "产品成本是否可控?是否支持规模化?是否具备备选供应链?关键元器件(如芯片、传感器)有≥2家可替换供应商" },
{ dimension: "商业模式", criteria: "盈利模式是否清晰?硬件销售毛利率≥50%,或有订阅服务(如云存储、AI 功能包)。是否构建“数据 - 反馈 - 迭代”闭环?产品使用数据可回传,驱动功能优化与新版本开发" },
{ dimension: "市场反馈", criteria: "是否处于需求上升期?功能、设计、服务、品牌是否有差异化壁垒?用户是否高留存、高互动、低退货?" },
];
const createStepOneRows = () => STEP_ONE_FACTOR_ROWS.map((row) => ({
...row,
successAnalysis: "",
failedAnalysis: "",
}));
const createStepTwoRows = () => STEP_TWO_FACTOR_ROWS.map((row) => ({ ...row, successAnalysis: "", failedAnalysis: "" }));
const createStepThreeRows = () => STEP_THREE_FACTOR_ROWS.map((row) => ({ ...row, successAnalysis: "", failedAnalysis: "" }));
const stepOneRows = ref(createStepOneRows());
const stepTwoRows = ref(createStepTwoRows());
const stepThreeRows = ref(createStepThreeRows());
const stepFourForm = ref({ successFactors: "", failedFactors: "" });
const progressItems = [
{ text: "进行中:成功/失败产品对比", status: "doing" },
{ text: "进行中:三项关键因素分析", status: "doing" },
{ text: "待完成:导出实训成果", status: "" },
];
onMounted(async () => {
await loadTaskConfig();
loadDraft();
await loadSavedAnswer();
draftReady.value = true;
});
watch(
() => JSON.stringify(buildDraft()),
() => persistDraft()
);
async function loadTaskConfig() {
try {
const res = await getTrainingTaskByKey(TASK_KEY);
taskConfig.value = res?.data || null;
currentStep.value = 0;
} catch (error) {
taskConfig.value = null;
currentStep.value = 0;
}
}
async function loadSavedAnswer() {
try {
const res = await getStudentTrainingAnswer(TASK_KEY);
const answer = res?.data;
if (answer?.step1Answer) {
stepOneRows.value = restoreRows(answer.step1Answer, createStepOneRows);
clearDraft();
}
if (answer?.step2Answer) stepTwoRows.value = restoreRows(answer.step2Answer, createStepTwoRows);
if (answer?.step3Answer) stepThreeRows.value = restoreRows(answer.step3Answer, createStepThreeRows);
if (answer?.step4Answer) stepFourForm.value = { ...stepFourForm.value, ...parseSavedAnswer(answer.step4Answer) };
restoreCurrentStep(answer?.currentStep);
} catch (error) {
// Keep local draft when the backend answer cannot be loaded.
}
}
function parseArray(value, fallback) {
if (Array.isArray(value)) {
return value.length ? value : fallback;
}
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(/[;;|]/)
.map((item) => item.trim())
.filter(Boolean);
return list.length ? list : fallback;
}
}
function parseSavedAnswer(value) {
if (!value) return null;
try {
return JSON.parse(value);
} catch (error) {
return null;
}
}
function restoreRows(value, createRows) {
const parsed = parseSavedAnswer(value);
if (!parsed || typeof parsed !== "object") {
return createRows();
}
if (Array.isArray(parsed.factors)) {
const savedByDimension = parsed.factors.reduce((map, row) => {
if (row?.dimension) {
map[row.dimension] = row;
}
return map;
}, {});
return createRows().map((row, index) => ({
...row,
successAnalysis: savedByDimension[row.dimension]?.successAnalysis || parsed.factors[index]?.successAnalysis || "",
failedAnalysis: savedByDimension[row.dimension]?.failedAnalysis || parsed.factors[index]?.failedAnalysis || "",
}));
}
return createRows();
}
function restoreCurrentStep(savedStep) {
const stepNumber = Number(savedStep);
const targetIndex = stepNumber - 1;
if (Number.isInteger(stepNumber) && targetIndex >= 0 && targetIndex < trainingSteps.value.length) {
currentStep.value = targetIndex;
}
}
function persistDraft() {
if (!draftReady.value) return;
try {
localStorage.setItem(getLocalDraftKey(), JSON.stringify(buildDraft()));
} catch (error) {
// Local draft is only a convenience; backend save remains the source of truth.
}
}
function loadDraft() {
try {
const raw = localStorage.getItem(getLocalDraftKey());
if (raw) {
const draft = parseSavedAnswer(raw);
if (draft?.stepOne) stepOneRows.value = restoreRows(draft.stepOne, createStepOneRows);
else stepOneRows.value = restoreRows(raw, createStepOneRows);
if (draft?.stepTwo) stepTwoRows.value = restoreRows(draft.stepTwo, createStepTwoRows);
if (draft?.stepThree) stepThreeRows.value = restoreRows(draft.stepThree, createStepThreeRows);
if (draft?.stepFour) stepFourForm.value = { ...stepFourForm.value, ...draft.stepFour };
}
} catch (error) {
clearDraft();
}
}
function clearDraft() {
try {
localStorage.removeItem(getLocalDraftKey());
} catch (error) {
// Ignore browser storage failures.
}
}
function buildStepOneOutcome() {
return {
title: taskTitle.value,
factors: stepOneRows.value.map((row) => ({
dimension: row.dimension,
criteria: row.criteria,
successAnalysis: row.successAnalysis,
failedAnalysis: row.failedAnalysis,
})),
};
}
function buildStepTwoOutcome() { return { title: taskTitle.value, factors: stepTwoRows.value.map((row) => ({ dimension: row.dimension, criteria: row.criteria, successAnalysis: row.successAnalysis, failedAnalysis: row.failedAnalysis })) }; }
function buildStepThreeOutcome() { return { title: taskTitle.value, factors: stepThreeRows.value.map((row) => ({ dimension: row.dimension, criteria: row.criteria, successAnalysis: row.successAnalysis, failedAnalysis: row.failedAnalysis })) }; }
function buildStepFourOutcome() { return { successFactors: stepFourForm.value.successFactors, failedFactors: stepFourForm.value.failedFactors }; }
function buildDraft() { return { stepOne: buildStepOneOutcome(), stepTwo: buildStepTwoOutcome(), stepThree: buildStepThreeOutcome(), stepFour: buildStepFourOutcome(), currentStep: currentStep.value + 1 }; }
function buildStepOneValidationPayload() {
return {
factors: stepOneRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({
dimension,
successAnalysis,
failedAnalysis,
})),
};
}
function buildStepOneAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) {
return {
step1Answer: JSON.stringify(buildStepOneOutcome()),
currentStep: persistedStep,
submitted: false,
saveAction,
};
}
function buildStepTwoValidationPayload() { return { factors: stepTwoRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({ dimension, successAnalysis, failedAnalysis })) }; }
function buildStepTwoAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) { return { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: persistedStep, submitted: false, saveAction }; }
function buildStepThreeValidationPayload() { return { factors: stepThreeRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({ dimension, successAnalysis, failedAnalysis })) }; }
function buildStepThreeAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) { return { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: persistedStep, submitted: false, saveAction }; }
function buildStepFourValidationPayload() { return buildStepFourOutcome(); }
function buildStepFourAnswerPayload(saveAction = "SUBMIT", persistedStep = 4) { return { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: persistedStep, submitted: true, saveAction }; }
async function saveAndGoToStepTwo() {
if (saving.value) return;
saving.value = true;
try {
await checkProductDevelopmentFactorsStepOne(buildStepOneValidationPayload());
await saveStudentTrainingAnswer(TASK_KEY, buildStepOneAnswerPayload("SAVE", 2));
clearDraft();
if (trainingSteps.value.length < 2) {
proxy?.$modal?.msgSuccess("已保存,当前任务尚未配置第 2 步");
return;
}
currentStep.value = 1;
proxy?.$modal?.msgSuccess("已保存,已进入第 2 步");
} catch (error) {
proxy?.$modal?.msgError?.(error?.message || "请完成三项成功产品和失败产品分析");
} finally {
saving.value = false;
}
}
async function saveAndGoToStepThree() {
if (saving.value) return;
saving.value = true;
try {
await checkProductDevelopmentFactorsStepTwo(buildStepTwoValidationPayload());
await saveStudentTrainingAnswer(TASK_KEY, buildStepTwoAnswerPayload("SAVE", 3));
clearDraft();
if (trainingSteps.value.length < 3) { proxy?.$modal?.msgSuccess("已保存,当前任务尚未配置第 3 步"); return; }
currentStep.value = 2;
proxy?.$modal?.msgSuccess("已保存,已进入第 3 步");
} catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成技术应用和合规与认证分析"); } finally { saving.value = false; }
}
async function saveAndGoToStepFour() {
if (saving.value) return;
saving.value = true;
try {
await checkProductDevelopmentFactorsStepThree(buildStepThreeValidationPayload());
await saveStudentTrainingAnswer(TASK_KEY, buildStepThreeAnswerPayload("SAVE", 4));
clearDraft();
if (trainingSteps.value.length < 4) { proxy?.$modal?.msgSuccess("已保存,当前任务尚未配置第 4 步"); return; }
currentStep.value = 3;
proxy?.$modal?.msgSuccess("已保存,已进入第 4 步");
} catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成供应链与成本控制、商业模式和市场反馈分析"); } finally { saving.value = false; }
}
async function submitTraining() { if (saving.value) return; saving.value = true; try { await checkProductDevelopmentFactorsStepFour(buildStepFourValidationPayload()); await saveStudentTrainingAnswer(TASK_KEY, buildStepFourAnswerPayload("SUBMIT", 4)); clearDraft(); proxy?.$modal?.msgSuccess("任务提交成功"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成关键因素分析"); } finally { saving.value = false; } }
async function resetTraining() {
stepOneRows.value = createStepOneRows();
stepTwoRows.value = createStepTwoRows();
stepThreeRows.value = createStepThreeRows();
stepFourForm.value = { successFactors: "", failedFactors: "" };
currentStep.value = 0;
clearDraft();
try {
saving.value = true;
await saveStudentTrainingAnswer(TASK_KEY, buildStepOneAnswerPayload("RESET", 1));
} catch (error) {
// Clearing the page should still work locally even when backend reset fails.
} finally {
saving.value = false;
}
proxy?.$modal?.msgSuccess("已清空填写内容");
}
function exportOutcome() {
const outcome = buildDraft();
const blob = new Blob([JSON.stringify(outcome, null, 2)], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "产品开发关键因素-实训成果.json";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
proxy?.$modal?.msgSuccess("导出成功");
}
</script>
<style lang="scss" scoped>
/*
* 页面级令牌(教育蓝极简主题):纯白画布 + 教育蓝主操作 + 中性灰文字层次。
* 覆盖全局主色/文字/边框令牌,使所有 var(--student-*) 规则自动重着色。
*/
.product-factors {
/* 主色:教育蓝 */
--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;
/* Element Plus 主色变量(el-select / el-switch / 进度条等吃这个) */
--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-* 基础上整体下调一档(11→17px) */
--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; /* 数字/成绩 */
min-width: 0;
color: var(--student-text);
}
/* 主列布局:容器栅格与内边距由全局 .student-training-shell 统一提供 */
.factors-core {
display: grid;
gap: 10px;
align-content: start;
}
/* 任务头:白卡 + 左侧教育蓝竖条 + 徽章 + 标题 + 简介 */
.task-header {
position: relative;
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;
}
.task-title {
margin: 0;
color: var(--student-text);
font-size: 17px;
line-height: 1.35;
font-weight: 700;
}
.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;
b {
color: var(--student-text);
}
ul {
display: grid;
gap: 4px;
margin: 0;
padding-left: 18px;
}
p {
margin: 0;
}
}
/* 工作台:白底卡片 + 12px 圆角 */
.factors-workbench {
margin: 0;
border: 1px solid var(--student-panel-border);
border-radius: 12px;
background: var(--student-card-bg);
&::before {
content: none !important;
display: none !important;
}
}
/* ============================================================
* 操作工具栏横条:内联布局,去掉悬浮和边框
* ============================================================ */
.task-action-strip {
margin: 0;
padding: 10px 16px;
:deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
}
/* 步骤导航(极简文字下划线)的换肤见非 scoped 块 */
.step-name {
display: block;
overflow: hidden;
min-width: 0;
color: inherit;
text-overflow: ellipsis;
}
/* 步骤卡片:12px 圆角白卡 */
.factor-step-card {
margin: 0;
padding: 18px 20px;
border: 1px solid var(--student-panel-border);
border-radius: 12px;
background: var(--student-card-bg);
}
.step-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
h3 {
margin: 2px 0 0;
color: var(--student-text);
font-size: 15px;
font-weight: 700;
line-height: 1.35;
}
}
/* Step 编号小标签:教育蓝引导文字 */
.section-eyebrow {
margin: 0;
color: var(--student-accent);
font-size: var(--student-fs-caption);
font-weight: 800;
line-height: 1.3;
letter-spacing: 0.12em;
}
/* 表格:纯白 + 中性边框,无任何底色 */
.training-table-wrap {
overflow-x: auto;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #ffffff;
}
.factor-table {
width: 100%;
min-width: 960px;
border-collapse: collapse;
table-layout: fixed;
th {
padding: 9px 10px;
border-bottom: 1px solid #e5e7eb;
color: #374151;
background: #ffffff;
font-size: 12px;
font-weight: 700;
text-align: center;
}
td {
padding: 9px 10px;
border-top: 1px solid #f0f1f3;
color: #374151;
background: #ffffff;
vertical-align: top;
}
}
.dimension-col {
width: 190px;
}
.criteria-col {
width: 300px;
}
.dimension-cell {
color: #1f2937;
font-size: 12px;
font-weight: 700;
text-align: center;
}
.criteria-cell {
color: #6b7280;
font-size: 12px;
line-height: 1.55;
text-align: center;
}
/* 表格内文本域:中性描边,聚焦教育蓝 */
.table-textarea {
box-sizing: border-box;
width: 100%;
min-height: 96px;
padding: 9px 11px;
border: 1px solid #d1d5db;
border-radius: 8px;
outline: none;
color: #1f2937;
background: #ffffff;
font-family: inherit;
font-size: 12px;
line-height: 1.55;
resize: vertical;
transition: border-color 0.16s ease, box-shadow 0.16s ease;
&::placeholder {
color: #9ca3af;
}
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
}
.summary-textarea {
min-height: 84px;
}
.factor-intro {
margin: 0 0 12px;
color: #374151;
font-size: 13px;
font-weight: 600;
line-height: 1.6;
}
.step-placeholder {
display: grid;
place-items: center;
min-height: 220px;
padding: 24px;
border: 1px dashed #d1d5db;
border-radius: 12px;
color: #6b7280;
background: #ffffff;
text-align: center;
p {
margin: 0 0 6px;
color: #374151;
font-size: 14px;
font-weight: 700;
}
span {
font-size: 12px;
line-height: 1.6;
}
}
/* 操作按钮区:居中紧凑排列(与标准页 .step-navigation 同规格) */
.task-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
margin-top: 20px;
}
.outline-action,
.btn-nav {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: inherit;
cursor: pointer;
}
/*
* 次按钮(清空填写):白底 + 教育蓝半透明描边 + 教育蓝字;
* 与标准样例一致:40px 高、10px 圆角、700 字重、hover 轻微浮起。
*/
.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.35);
border-radius: 8px;
color: var(--student-accent);
background: #ffffff;
font-family: inherit;
font-size: 13px;
font-weight: 700;
line-height: 1;
text-shadow: none;
cursor: pointer;
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
&:hover:not(:disabled) {
transform: translateY(-1px);
border-color: var(--student-accent);
background: var(--student-accent-soft);
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12);
}
&:active:not(:disabled) {
transform: translateY(0);
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
transform: none;
box-shadow: none;
}
}
/* 主按钮(保存进入下一步 / 提交任务):教育蓝实底白字 + 蓝色柔影 */
.btn-submit {
border-color: var(--student-accent);
color: #ffffff;
background: var(--student-accent);
min-height: 36px;
height: 36px;
padding: 0 14px;
border-radius: 8px;
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28);
&:hover:not(:disabled) {
transform: translateY(-1px);
border-color: var(--student-accent-deep);
background: var(--student-accent-deep);
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32);
}
&:active:not(:disabled) {
transform: translateY(0);
}
}
@media (max-width: 900px) {
/* 窄屏全宽规则的强制覆盖见非 scoped 块(scoped 打不赢全局 !important) */
/* 步骤标题行纵向(全局未锁,scoped 可生效) */
.step-title-row {
flex-direction: column;
align-items: stretch;
}
}
</style>
<style lang="scss">
/* ===== AI 侧栏样式已收敛进 TrainingAiSidebar 组件(教育蓝极简),此处不再覆盖 ===== */
/* ============================================================
* 步骤导航:胶囊分段 + 连接(教育蓝主题)
* 与 new-product-survey.vue 完全一致,覆盖全局 !important 规则
* ============================================================ */
#app .studentLayout section.app-main:has(.product-factors) .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-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-bar { display: none !important; }
/* 编号小徽章 */
.step-no {
position: static !important;
flex: 0 0 auto !important;
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;
&::after { display: none !important; }
}
.step-name { color: inherit !important; }
/* 未到达项 hover:教育蓝边浅底,编号徽章转教育蓝 */
&:hover:not(.active):not(.complete) {
border-color: #2563eb !important;
background: #eff6ff !important;
color: #2563eb !important;
z-index: 1 !important;
.step-no { background: #2563eb !important; color: #ffffff !important; }
}
/* 已完成:绿色编号徽章 */
&.complete:not(.active) {
border-color: #e5e7eb !important;
color: #374151 !important;
.step-no { background: rgba(43, 164, 113, 0.12) !important; color: #16915f !important; }
&:hover {
border-color: #2ba471 !important;
background: #f0fdf6 !important;
z-index: 1 !important;
.step-no { 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-no { background: #ffffff !important; color: #2563eb !important; }
&:hover {
background: #1d4ed8 !important;
border-color: #1d4ed8 !important;
}
}
}
}
@media (max-width: 900px) {
#app .studentLayout section.app-main:has(.product-factors) .step-indicator {
justify-content: flex-start !important;
overflow-x: auto !important;
}
#app .studentLayout section.app-main:has(.product-factors) .step-indicator .step-tab {
flex: 0 0 140px !important;
max-width: none !important;
}
}
/* ============================================================
* 按钮风格强制同步:
* 全局 student-training.scss 用 !important 把 .task-actions 按钮锁成浅蓝大胶囊,
* scoped 规则无法取胜。这里用 id 锚点(特异性含 #app)+ !important 稳定覆盖。
* ============================================================ */
/* --- 底部步骤按钮:36px 标准按钮(位置 A) --- */
#app .studentLayout section.app-main:has(.product-factors) .task-actions {
gap: 14px !important;
margin-top: 20px !important;
}
#app .studentLayout section.app-main:has(.product-factors) .task-actions > button {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 8px !important;
min-width: 0 !important;
width: auto !important;
height: 34px !important;
min-height: 34px !important;
padding: 0 14px !important;
border: 1px solid rgba(37, 99, 235, 0.35) !important;
border-radius: 8px !important;
color: #2563eb !important;
background: #ffffff !important;
box-shadow: none !important;
text-shadow: none !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(.product-factors) .task-actions > button .el-icon {
display: inline-flex !important;
}
#app .studentLayout section.app-main:has(.product-factors) .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(.product-factors) .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(.product-factors) .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;
}
#app .studentLayout section.app-main:has(.product-factors) .task-actions > button:disabled {
cursor: not-allowed !important;
opacity: 0.5 !important;
transform: none !important;
box-shadow: none !important;
}
/* 窄屏:按钮纵向全宽(全局容器 min-width/布局均为 !important,须在此覆盖) */
@media (max-width: 900px) {
#app .studentLayout section.app-main:has(.product-factors) .task-actions {
flex-direction: column !important;
align-items: stretch !important;
}
#app .studentLayout section.app-main:has(.product-factors) .task-actions > button {
width: 100% !important;
}
/* task-action-strip 响应式 */
#app .studentLayout section.app-main:has(.product-factors) .task-action-strip {
position: static !important;
}
#app .studentLayout section.app-main:has(.product-factors) .task-action-strip :deep(.training-task-toolbar) {
justify-content: stretch !important;
}
#app .studentLayout section.app-main:has(.product-factors) .task-action-strip :deep(.training-task-toolbar > *) {
flex: 1 1 auto !important;
}
}
</style>