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/new-product-survey.vue

2158 lines
63 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="new-product-value-page student-training-shell">
<main class="training-core">
<section class="task-header">
<div class="task-head-main">
<span class="task-badge">
<el-icon><Operation /></el-icon>
核心实训 · 产品开发基础
</span>
<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>
<TrainingTaskToolbar class="new-product-survey-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
</section>
<section class="task-card">
<nav class="step-indicator" aria-label="新产品调查与分析步骤">
<button
v-for="step in steps"
:key="step.no"
type="button"
:class="['step-tab', { active: currentStep === step.no, complete: currentStep > step.no }]"
:aria-label="`切换至第 ${step.no} 步:${step.name}`"
:aria-current="currentStep === step.no ? 'step' : undefined"
:title="step.name"
@click="currentStep = step.no"
>
<span class="step-bar" aria-hidden="true"></span>
<span class="step-no" aria-hidden="true">{{ String(step.no).padStart(2, "0") }}</span>
<span class="step-name">{{ step.name }}</span>
</button>
</nav>
<section v-show="currentStep === 1" class="step-content active-step">
<div class="step-card">
<div class="step-title-row">
<div>
<p class="section-eyebrow">Step 01</p>
<h3>{{ getStepName(1) }}</h3>
</div>
<span class="step-chip">卖点分类练习</span>
</div>
<p class="step-desc">查看案例产品详情,仔细阅读主图及详情页信息,对下列产品卖点进行三层次分类。</p>
<TrainingStepOneChoiceQuestions
:questions="taskConfig?.stepOneQuestions"
v-model="stepOneChoiceAnswers"
/>
<div class="training-table-wrap classification-table-wrap">
<table class="training-data-table classification-table">
<thead>
<tr>
<th>序号</th>
<th>卖点</th>
<th>
三层次分类
<span class="layer-legend" aria-hidden="true">
<i style="--dot: var(--student-layer-1)"></i>
<i style="--dot: var(--student-layer-2)"></i>
<i style="--dot: var(--student-layer-3)"></i>
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(point, index) in STEP_ONE_SELLING_POINTS" :key="point.id" :class="[feedbackClass(point.id), layerClass(classifications[point.id])]">
<td class="classification-number">{{ index + 1 }}</td>
<td class="classification-selling-point">{{ point.text }}</td>
<td class="classification-select-cell">
<el-select v-model="classifications[point.id]" placeholder="请选择分类" :disabled="checking" @change="clearFeedback(point.id)">
<el-option v-for="category in LAYER_OPTIONS" :key="category" :label="category" :value="category" />
</el-select>
<p v-if="checkResult?.itemsById?.[point.id]" class="classification-feedback">
{{ checkResult.itemsById[point.id].message }}
</p>
</td>
</tr>
</tbody>
</table>
</div>
<p v-if="checkResult" class="classification-summary" :class="{ 'is-success': checkResult.allCorrect }">
本次判题:{{ checkResult.correctCount }} / {{ checkResult.totalCount }} 项分类正确
</p>
</div>
</section>
<section v-show="currentStep === 2" class="step-content active-step">
<div class="step-card step-two-survey">
<div class="step-title-row">
<div>
<p class="section-eyebrow">Step 02</p>
<h3>{{ getStepName(2) }}</h3>
</div>
<span class="step-chip">实地调研记录</span>
</div>
<p class="step-desc">请以真实消费者身份进入电商平台,通过搜索、浏览和对比,采集 1 个 AI 产品的真实数据,并完成产品三层次分析。</p>
<div class="survey-form">
<section class="survey-field-group">
<p class="survey-field-title"><b>01</b> 选择调研平台</p>
<el-radio-group v-model="step2Form.platform" class="survey-options">
<el-radio v-for="option in STEP_TWO_PLATFORM_OPTIONS" :key="option.value" :label="option.value" class="survey-option">
<strong>{{ option.label }}</strong><span>{{ option.description }}</span>
</el-radio>
</el-radio-group>
</section>
<section class="survey-field-group">
<p class="survey-field-title"><b>02</b> 确定调研场景</p>
<el-radio-group v-model="step2Form.scene" class="survey-options">
<el-radio v-for="option in STEP_TWO_SCENE_OPTIONS" :key="option.value" :label="option.value" class="survey-option">
<strong>{{ option.label }}</strong><span>{{ option.description }}</span>
</el-radio>
</el-radio-group>
<input v-if="step2Form.scene === '其他场景'" v-model.trim="step2Form.customScene" class="input-single custom-scene-input" placeholder="请填写其他调研场景" maxlength="60" />
</section>
<section class="survey-field-group">
<p class="survey-field-title"><b>03</b> 设计搜索关键词</p>
<p class="survey-field-hint">遵循“核心词 + 属性词 + 场景词”。例如:“AI智能音箱 语音控制 家居”“AI办公鼠标 语音转写 会议”。</p>
<input v-model.trim="step2Form.keyword" class="input-single" placeholder="请输入搜索关键词" maxlength="120" />
</section>
<section class="survey-field-group">
<p class="survey-field-title"><b>04</b> 记录产品信息并上传图片</p>
<p class="survey-field-hint">请在搜索结果中按“销量”排序,从排名前 10 的产品中选择 1 款产品;上传主图及详情页截图,最多 5 张。</p>
<label class="product-name-field">
<span>产品名称</span>
<input v-model.trim="step2Form.productName" class="input-single" placeholder="请输入产品名称" maxlength="120" />
</label>
<label class="image-upload-trigger" :class="{ disabled: uploadingImages || step2Form.images.length >= MAX_STEP_TWO_IMAGES }">
<input type="file" accept="image/*" multiple :disabled="uploadingImages || step2Form.images.length >= MAX_STEP_TWO_IMAGES" @change="uploadStepTwoImages" />
<span>{{ uploadingImages ? '图片上传中…' : `上传产品图片(${step2Form.images.length}/${MAX_STEP_TWO_IMAGES})` }}</span>
</label>
<div v-if="step2Form.images.length" class="product-image-list">
<figure v-for="(image, index) in step2Form.images" :key="`${image.url}-${index}`" class="product-image-card">
<button type="button" class="product-image-preview" @click="previewStepTwoImage(image.url)">
<img :src="resolveUploadedImageUrl(image.url)" :alt="`${step2Form.productName || '产品'}图片 ${index + 1}`" />
</button>
<figcaption :title="image.name">{{ image.name }}</figcaption>
<button type="button" class="remove-image-button" :aria-label="`删除图片 ${index + 1}`" @click="removeStepTwoImage(index)">删除</button>
</figure>
</div>
</section>
</div>
</div>
</section>
<section v-show="currentStep === 3" class="step-content active-step">
<div class="step-card">
<div class="step-title-row">
<div>
<p class="section-eyebrow">Step 03</p>
<h3>{{ getStepName(3) }}</h3>
</div>
<span class="step-chip">三层次对照分析</span>
</div>
<p class="step-desc">
基于上一步采集的真实数据,运用“三层次理论”进行深度分析,参考示例填写下表;每层至少填写 2 条要点并按换行分条。
</p>
<div class="training-table-wrap">
<table class="training-data-table layer-table">
<thead>
<tr>
<th>分析维度</th>
<th>实例:AI学习机</th>
<th>{{ step2Form.productName || '所选产品' }}分析</th>
</tr>
</thead>
<tbody>
<tr v-for="row in step3Rows" :key="row.dimension">
<td class="dimension-cell"><strong>{{ row.dimension }}</strong><span>{{ row.definition }}</span></td>
<td class="example-cell">
<ol class="reference-points">
<li v-for="point in row.referencePoints" :key="point">{{ point }}</li>
</ol>
</td>
<td class="analysis-input-cell">
<textarea v-model="row.analysis" class="cell-textarea" rows="7" placeholder="每行填写一条分析要点" />
<p class="analysis-count" :class="{ 'is-ready': countStepThreePoints(row.analysis) >= 2 }">已填写 {{ countStepThreePoints(row.analysis) }} / 至少 2 条</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<section v-show="currentStep === 4" class="step-content active-step">
<div class="step-card">
<div class="step-title-row">
<div>
<p class="section-eyebrow">Step 04</p>
<h3>{{ getStepName(4) }}</h3>
</div>
<span class="step-chip">价值判断</span>
</div>
<p class="step-desc">结合上面实训,针对AI技术在产品中的应用价值进行分析,回答以下问题。</p>
<label class="field-block is-layer layer-core">
<span><b>(1)核心层:</b>AI技术是否实现产品功能突破?是否形成差异化卖点?</span>
<textarea v-model="step4Form.coreValue" class="input-field step4-input" rows="7" placeholder="请填写核心层AI应用价值分析" />
</label>
<label class="field-block is-layer layer-tangible">
<span><b>(2)有形层:</b>AI技术是否影响产品形态?如产品外形包装、电商主图设计、参数配置、价格带定位。</span>
<textarea v-model="step4Form.tangibleValue" class="input-field step4-input" rows="7" placeholder="请填写有形层AI应用价值分析" />
</label>
<label class="field-block is-layer layer-extended">
<span><b>(3)延伸层:</b>AI技术如何通过服务升级、内容更新提升用户粘性,推动付费服务?</span>
<textarea v-model="step4Form.extendedValue" class="input-field step4-input" rows="7" placeholder="请填写延伸层AI应用价值分析" />
</label>
</div>
</section>
<div class="step-navigation">
<button v-if="currentStep > 1" type="button" class="btn-nav btn-ghost" @click="prevStep">
<el-icon><ArrowLeft /></el-icon>
上一步
</button>
<button type="button" class="btn-nav btn-save" :disabled="saving || checking || uploadingImages" @click="saveCurrentProgress">
<el-icon><CircleCheck /></el-icon>
保存当前进度
</button>
<button v-if="currentStep < steps.length" type="button" class="btn-nav btn-primary" :disabled="saving || checking || uploadingImages" @click="nextStep">
{{ checking ? "正在判题" : "下一步" }}
<el-icon><ArrowRight /></el-icon>
</button>
<span class="step-progress">Step {{ String(currentStep).padStart(2, "0") }} / {{ String(steps.length).padStart(2, "0") }}</span>
</div>
</section>
</main>
<el-dialog v-model="imagePreviewOpen" title="产品图片预览" width="min(760px, 92vw)" append-to-body>
<img v-if="imagePreviewUrl" :src="imagePreviewUrl" alt="产品图片大图" class="product-image-full" />
</el-dialog>
<TrainingAiSidebar
:study-tip="currentStudyTip"
reference-title="产品三层次分析参考"
reference-text="· 核心层:用户购买的根本利益与真实痛点<br />· 有形层:功能、外观、品牌、包装、参数等可感知内容<br />· 延伸层:售后、内容服务、会员权益、生态兼容、数据安全等附加价值"
assist-text="建议先把电商页面中的主图、参数、详情页、评价和售后承诺分别归入三层次,再比较哪些层次能形成差异化。"
:progress-items="progressItems"
/>
</div>
</template>
<script setup>
import {
ArrowLeft,
ArrowRight,
CircleCheck,
DataAnalysis,
Operation,
Opportunity,
RefreshLeft,
Search,
TrendCharts,
} from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { checkNewProductSurveyStepFour, checkNewProductSurveyStepOne, checkNewProductSurveyStepThree, checkNewProductSurveyStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import useUserStore from "@/store/modules/user";
import { isExternal } from "@/utils/validate";
import { extractTrainingStepNames } from "@/views/training/taskKeyMap";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import TrainingStepOneChoiceQuestions from "@/views/components/TrainingStepOneChoiceQuestions.vue";
const TASK_KEY = "new-product-survey";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const taskConfig = ref(null);
const saving = ref(false);
const checking = ref(false);
const uploadingImages = ref(false);
const imagePreviewOpen = ref(false);
const imagePreviewUrl = ref("");
const draftReady = ref(false);
const currentStep = ref(1);
const briefOpen = ref(false);
const defaultSteps = [
{ no: 1, name: "产品卖点的三层次分类", icon: Search },
{ no: 2, name: "选定调查平台与产品", icon: DataAnalysis },
{ no: 3, name: "AI产品三层次分析", icon: TrendCharts },
{ no: 4, name: "AI应用价值分析", icon: Operation },
];
const defaultGoals = [
"理解市场驱动、技术推动等不同类型产品开发的核心逻辑,培养市场意识,能分析新技术在互联网产品中的应用场景。",
"掌握运用电商平台开展市场调查的方法,能结合产品三层次理论分析互联网新产品。",
"能通过分析,理解企业如何通过不同产品层次创造差异化竞争优势。",
];
const taskTitle = computed(() => taskConfig.value?.taskName || "新产品调查与分析");
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals));
const taskBackground = computed(
() =>
taskConfig.value?.background ||
"2024年,生成式AI技术爆发式发展,催生大量消费端新产品,如AI智能音箱、AI学习机、AI玩具等。DeepSeek的出现降低了国内企业使用AI技术开发产品的成本,企业希望借助电商平台数据,为未来产品开发提供思路。"
);
const taskRequirement = computed(
() =>
taskConfig.value?.requirements ||
"请借助电商平台完成新产品调查,围绕产品三层次提取核心层、有形层、延伸层信息,并结合AI技术价值形成分析结论。"
);
const steps = computed(() => {
const configured = extractTrainingStepNames(taskConfig.value?.steps, defaultSteps.map((item) => item.name)).slice(0, 4);
return defaultSteps.map((step, index) => ({
...step,
name: configured[index] || step.name,
}));
});
const LAYER_OPTIONS = ["核心层", "有形层", "延伸层"];
/* 纯 UI 辅助:按已选层次为表格行上色,不影响判题数据 */
const LAYER_CLASS_MAP = { "核心层": "is-layer-core", "有形层": "is-layer-tangible", "延伸层": "is-layer-extended" };
function layerClass(value) {
return LAYER_CLASS_MAP[value] || "";
}
const MAX_STEP_TWO_IMAGES = 5;
const STEP_TWO_PLATFORM_OPTIONS = [
{ value: "京东", label: "京东", description: "3C/智能硬件类目数据全、评价真实" },
{ value: "天猫", label: "天猫", description: "品牌 AI 产品丰富,详情页设计成熟" },
{ value: "抖音电商", label: "抖音电商", description: "爆款 AI 玩具/家居新品多,价格带清晰" },
];
const STEP_TWO_SCENE_OPTIONS = [
{ value: "AI智能家居", label: "AI智能家居", description: "如 AI 音箱、AI 台灯、AI 门锁" },
{ value: "AI智能办公", label: "AI智能办公", description: "如 AI 鼠标、AI 录音笔、AI 翻译机" },
{ value: "AI儿童玩具", label: "AI儿童玩具", description: "如 AI 早教机器人、AI 对话玩偶" },
{ value: "AI学习设备", label: "AI学习设备", description: "如 AI 学习机、AI 点读笔" },
{ value: "AI美妆辅助", label: "AI美妆辅助", description: "如 AI 皮肤检测仪、AI 试妆镜" },
{ value: "其他场景", label: "其他场景", description: "填写你发现的其他高流量 AI 应用场景" },
];
const STEP_ONE_SELLING_POINTS = [
{ id: "selling-point-1", text: "生日礼物女孩子毕业儿童3到6岁10小女童" },
{ id: "selling-point-2", text: "AI智能机器人毛绒玩具" },
{ id: "selling-point-3", text: "豆包+DeepSeek六大混合模型,多语种AI对话" },
{ id: "selling-point-4", text: "覆盖百科、算术、语文、诗词、AI翻译等,早教能力强,还具备情绪感知安抚功能" },
{ id: "selling-point-5", text: "安全亲肤,零甲醛零荧光剂,母婴级不掉毛" },
{ id: "selling-point-6", text: "柔软手感,舒心无忧,内赠USB充电线" },
{ id: "selling-point-7", text: "嘴巴会动,翅膀煽动,脖子扭动,触摸感应触发,说话跳舞同步" },
{ id: "selling-point-8", text: "可自由定制角色/专属音色,打造宝宝专属小伙伴,适配哄睡、早教、陪伴多场景" },
{ id: "selling-point-9", text: "快递免运费,预计7小时内发货" },
{ id: "selling-point-10", text: "大促价保、假一赔四、极速退款、7天无理由退换" },
{ id: "selling-point-11", text: "店铺优惠后169元起" },
];
const createStep2Form = () => ({ platform: "", scene: "", customScene: "", keyword: "", productName: "", images: [] });
const STEP_THREE_REFERENCE_ROWS = [
{ dimension: "核心层", definition: "AI技术解决的核心需求/功能", referencePoints: ["大模型作业批改", "自适应学习路径规划"] },
{ dimension: "有形层", definition: "外观/材质/参数/价格策略", referencePoints: ["护眼屏+铝合金机身", "搭载NPU芯片", "全平台统一定价2999元,某网红直播间专属价2699元"] },
{ dimension: "延伸层", definition: "软件升级/专属服务/社群运营", referencePoints: ["终身免费题库更新", "VIP学科规划师1对1", "家长互助社群及资料共享微信群"] },
];
const createStep3Rows = () => STEP_THREE_REFERENCE_ROWS.map((row) => ({ ...row, analysis: "" }));
const createStep4Form = () => ({
coreValue: "",
tangibleValue: "",
extendedValue: "",
});
const classifications = ref({});
const stepOneChoiceAnswers = ref({});
const checkResult = ref(null);
const step2Form = ref(createStep2Form());
const step3Rows = ref(createStep3Rows());
const step4Form = ref(createStep4Form());
const getLocalDraftKey = () => {
const userInfo = userStore.userInfo || {};
const userKey = userInfo.userId || userInfo.user_id || userInfo.username || userInfo.userName || "anonymous";
return `${TASK_KEY}:draft:${userKey}`;
};
const currentStudyTip = computed(() => {
const tips = {
1: "当前步骤建议:先从案例或电商详情页中拆出核心层、有形层、延伸层,不要把功能和用户利益混在一起。",
2: "当前步骤建议:选择一个主流电商平台与真实 AI 产品,用“核心词 + 属性词 + 场景词”搜索并保留主图、详情页截图。",
3: "当前步骤建议:将上一步选定产品的卖点按核心层、有形层、延伸层拆分,每层至少记录两条可核对的分析要点。",
4: "当前步骤建议:把前面三步的发现收束为产品开发启发,说明哪些价值值得继续强化。",
};
return tips[currentStep.value];
});
const progressItems = computed(() =>
steps.value.map((step) => ({
text: `${currentStep.value > step.no ? "已完成" : currentStep.value === step.no ? "进行中" : "待完成"}:${step.name}`,
status: currentStep.value > step.no ? "done" : currentStep.value === step.no ? "doing" : "",
}))
);
onMounted(async () => {
loadDraft();
await loadTaskConfig();
await loadSavedAnswer();
draftReady.value = true;
});
watch(
[classifications, checkResult, step2Form, step3Rows, step4Form, currentStep],
() => persistDraft(),
{ deep: true }
);
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 getStepName(no) {
return steps.value.find((item) => item.no === no)?.name || defaultSteps[no - 1]?.name || "";
}
async function loadTaskConfig() {
try {
const res = await getTrainingTaskByKey(TASK_KEY);
taskConfig.value = res?.data || null;
} catch (error) {
taskConfig.value = null;
}
}
async function loadSavedAnswer() {
try {
const res = await getStudentTrainingAnswer(TASK_KEY);
const answer = res?.data;
if (answer && applySavedAnswer(answer)) {
clearDraft();
}
} catch (error) {
// 后端不可用时,页面仍允许学生直接填写。
}
}
function safeParse(value) {
if (!value) return null;
if (typeof value === "object") return value;
try {
return JSON.parse(value);
} catch (error) {
return null;
}
}
function applySavedAnswer(value) {
const parsed = safeParse(value);
if (!parsed || typeof parsed !== "object") return false;
if (parsed.step1Answer || parsed.step2Answer || parsed.step3Answer || parsed.step4Answer) {
let loaded = false;
const step1Parsed = safeParse(parsed.step1Answer);
if (step1Parsed?.step1Form || step1Parsed?.step2Form || step1Parsed?.step2Rows || step1Parsed?.step3Rows || step1Parsed?.step4Form) {
loaded = applySavedAnswer(step1Parsed) || loaded;
} else {
loaded = applyStep1Answer(step1Parsed || parsed.step1Answer) || loaded;
}
loaded = applyStep2Answer(parsed.step2Answer) || loaded;
loaded = applyStep3Answer(parsed.step3Answer) || loaded;
loaded = applyStep4Answer(parsed.step4Answer) || loaded;
currentStep.value = Math.min(Math.max(Number(parsed.currentStep || currentStep.value || 1), 1), 4);
return loaded;
}
let loaded = false;
loaded = applyStep1Answer(parsed.step1Form || parsed) || loaded;
loaded = applyStep2Answer(parsed.step2Form || parsed.step2Answer || parsed.step2Rows || parsed.rows2 || parsed.platformRows) || loaded;
loaded = applyStep3Answer(parsed.step3Rows || parsed.rows3 || parsed.layerRows) || loaded;
loaded = applyStep4Answer(parsed.step4Form || parsed.aiValue || parsed.valueAnalysis) || loaded;
currentStep.value = Math.min(Math.max(Number(parsed.currentStep || 1), 1), 4);
return loaded;
}
function applyStep1Answer(value) {
const parsed = safeParse(value);
if (!parsed || typeof parsed !== "object") return false;
const source = parsed.classifications || parsed.step1Form || parsed;
const restored = STEP_ONE_SELLING_POINTS.reduce((result, point) => {
if (LAYER_OPTIONS.includes(source?.[point.id])) {
result[point.id] = source[point.id];
}
return result;
}, {});
classifications.value = restored;
stepOneChoiceAnswers.value = parsed.choiceAnswers && typeof parsed.choiceAnswers === "object" ? parsed.choiceAnswers : {};
checkResult.value = normalizeCheckResult(parsed.checkResult);
return Object.keys(restored).length > 0;
}
function normalizeCheckResult(value) {
if (!value || typeof value !== "object" || !Array.isArray(value.items)) return null;
const itemsById = value.items.reduce((result, item) => {
if (item?.id) result[item.id] = item;
return result;
}, {});
return {
items: value.items,
itemsById,
correctCount: Number(value.correctCount || 0),
totalCount: Number(value.totalCount || value.items.length),
allCorrect: Boolean(value.allCorrect),
};
}
function feedbackClass(id) {
const item = checkResult.value?.itemsById?.[id];
if (!item) return "";
return item.correct ? "classification-row--correct" : "classification-row--incorrect";
}
function clearFeedback(id) {
if (checkResult.value?.itemsById?.[id]) {
checkResult.value = null;
}
}
function hasCompletedClassifications() {
return STEP_ONE_SELLING_POINTS.every((point) => LAYER_OPTIONS.includes(classifications.value[point.id]));
}
function buildCheckItems() {
return STEP_ONE_SELLING_POINTS.map((point) => ({ id: point.id, category: classifications.value[point.id] }));
}
async function uploadStepTwoImages(event) {
const input = event.target;
const files = Array.from(input?.files || []);
input.value = "";
if (!files.length) return;
if (files.some((file) => !file.type?.startsWith("image/"))) {
proxy?.$modal?.msgWarning("只能上传图片文件");
return;
}
if (step2Form.value.images.length + files.length > MAX_STEP_TWO_IMAGES) {
proxy?.$modal?.msgWarning(`产品图片最多上传 ${MAX_STEP_TWO_IMAGES} 张`);
return;
}
uploadingImages.value = true;
try {
for (const file of files) {
const data = new FormData();
data.append("file", file);
const res = await uploadStudentTrainingFile(data);
const url = res?.url || res?.fileName || res?.data?.url || res?.data?.fileName || "";
if (!url) throw new Error("图片上传失败,请重试");
step2Form.value.images.push({ name: res?.originalFilename || file.name, url });
}
proxy?.$modal?.msgSuccess("产品图片上传成功");
} catch (error) {
proxy?.$modal?.msgError(error?.message || "图片上传失败,请稍后重试");
} finally {
uploadingImages.value = false;
}
}
function removeStepTwoImage(index) {
step2Form.value.images.splice(index, 1);
}
function resolveUploadedImageUrl(url) {
const source = String(url || "").trim();
if (!source || source.startsWith("data:") || source.startsWith("blob:")) return source;
if (!isExternal(source)) return source.startsWith("/") ? source : `/${source}`;
try {
const parsed = new URL(source);
if (["localhost", "127.0.0.1", "::1"].includes(parsed.hostname)) {
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
}
} catch {
return source;
}
return source;
}
function previewStepTwoImage(url) {
imagePreviewUrl.value = resolveUploadedImageUrl(url);
imagePreviewOpen.value = true;
}
function applyStep2Answer(value) {
const parsed = safeParse(value);
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") return false;
const source = parsed.step2Form || parsed;
const fields = ["platform", "scene", "customScene", "keyword", "productName"];
const images = Array.isArray(source.images)
? source.images.filter((image) => image?.url).slice(0, MAX_STEP_TWO_IMAGES).map((image) => ({ name: image.name || "产品图片", url: image.url }))
: [];
const hasValue = fields.some((field) => String(source[field] || "").trim()) || images.length;
if (!hasValue) return false;
step2Form.value = { ...createStep2Form(), ...pickFields(source, fields), images };
return true;
}
function applyStep3Answer(value) {
const parsed = safeParse(value);
const rows = Array.isArray(parsed) ? parsed : parsed?.step3Rows || parsed?.rows || parsed?.layerRows;
if (!Array.isArray(rows) || !rows.length) return false;
const savedByDimension = rows.reduce((result, row) => {
if (row?.dimension) result[row.dimension] = row;
return result;
}, {});
step3Rows.value = createStep3Rows().map((row, index) => {
const saved = savedByDimension[row.dimension] || rows[index] || {};
const points = Array.isArray(saved.points) ? saved.points : [];
const legacyValue = [saved.analysis, saved.product1, saved.product2, saved.product3].find((item) => String(item || "").trim());
return {
...row,
analysis: points.length ? points.map((item) => String(item || "").trim()).filter(Boolean).join("\n") : String(legacyValue || ""),
};
});
return true;
}
function applyStep4Answer(value) {
const parsed = safeParse(value);
if (!parsed || typeof parsed !== "object") return false;
const source = parsed.step4Form || parsed;
const fields = Object.keys(createStep4Form());
const hasValue = fields.some((key) => source[key] !== undefined && source[key] !== null && source[key] !== "");
if (!hasValue) return false;
step4Form.value = { ...createStep4Form(), ...pickFields(source, fields) };
return true;
}
function pickFields(source, fields) {
return fields.reduce((target, key) => {
if (source[key] !== undefined && source[key] !== null) {
target[key] = source[key];
}
return target;
}, {});
}
function normalizeRows(rows, fallback, matchKey = "") {
if (!Array.isArray(rows) || !rows.length) return fallback;
if (matchKey) {
const rowMap = rows.reduce((map, row) => {
if (row?.[matchKey]) {
map[row[matchKey]] = row;
}
return map;
}, {});
return fallback.map((row, index) => ({
...row,
...(rowMap[row[matchKey]] || rows[index] || {}),
}));
}
return fallback.map((row, index) => ({
...row,
...(rows[index] || {}),
}));
}
function splitStepThreePoints(value) {
return String(value || "")
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean);
}
function countStepThreePoints(value) {
return splitStepThreePoints(value).length;
}
function buildStepThreeValidationPayload() {
return {
rows: step3Rows.value.map((row) => ({
dimension: row.dimension,
points: splitStepThreePoints(row.analysis),
})),
};
}
function buildAnswerPayload(status) {
const saveAction = status === "SUBMITTED" ? "SUBMIT" : status === "RESET" ? "RESET" : "SAVE";
return {
saveAction,
currentStep: currentStep.value,
step1Answer: JSON.stringify({ classifications: classifications.value, checkResult: checkResult.value, choiceAnswers: stepOneChoiceAnswers.value }),
step2Answer: JSON.stringify(step2Form.value),
step3Answer: JSON.stringify(buildStepThreeValidationPayload()),
step4Answer: JSON.stringify(step4Form.value),
};
}
async function saveAnswer(status = "IN_PROGRESS", silent = false) {
saving.value = true;
try {
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
if (!silent) {
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
}
} finally {
saving.value = false;
}
}
function saveCurrentProgress() {
return saveAnswer("IN_PROGRESS");
}
async function submitTask() {
checking.value = true;
try {
await checkNewProductSurveyStepFour(step4Form.value);
await saveAnswer("SUBMITTED");
} catch (error) {
proxy?.$modal?.msgWarning(error?.message || "请完成三层次AI应用价值分析");
} finally {
checking.value = false;
}
}
async function nextStep() {
if (currentStep.value === 2) {
checking.value = true;
try {
await checkNewProductSurveyStepTwo(step2Form.value);
await saveAnswer("IN_PROGRESS", true);
proxy?.$modal?.msgSuccess("第二步信息已校验并保存");
advanceStep();
} catch (error) {
proxy?.$modal?.msgWarning(error?.message || "请完善第二步调研信息后再进入下一步");
} finally {
checking.value = false;
}
return;
}
if (currentStep.value === 3) {
checking.value = true;
try {
await checkNewProductSurveyStepThree(buildStepThreeValidationPayload());
await saveAnswer("IN_PROGRESS", true);
proxy?.$modal?.msgSuccess("第三步分析已校验并保存");
advanceStep();
} catch (error) {
proxy?.$modal?.msgWarning(error?.message || "请确保每个层次至少填写 2 条分析要点");
} finally {
checking.value = false;
}
return;
}
if (currentStep.value !== 1) {
advanceStep();
return;
}
if (!hasCompletedClassifications()) {
proxy?.$modal?.msgWarning("请完成全部卖点分类后再进入下一步");
return;
}
checking.value = true;
try {
const res = await checkNewProductSurveyStepOne(buildCheckItems());
checkResult.value = normalizeCheckResult(res?.data);
await saveAnswer("IN_PROGRESS", true);
if (checkResult.value?.allCorrect) {
proxy?.$modal?.msgSuccess("分类全部正确,已进入下一步");
advanceStep();
} else {
proxy?.$modal?.msgWarning("存在分类错误,请根据提示修改后重新判题");
}
} catch (error) {
proxy?.$modal?.msgError(error?.message || "判题失败,请稍后重试");
} finally {
checking.value = false;
}
}
function advanceStep() {
if (currentStep.value < steps.value.length) {
currentStep.value += 1;
}
}
function prevStep() {
if (currentStep.value > 1) {
currentStep.value -= 1;
}
}
function resetExample() {
classifications.value = {};
stepOneChoiceAnswers.value = {};
checkResult.value = null;
step2Form.value = createStep2Form();
imagePreviewUrl.value = "";
imagePreviewOpen.value = false;
step3Rows.value = createStep3Rows();
step4Form.value = createStep4Form();
currentStep.value = 1;
clearDraft();
}
function buildOutcome() {
return {
taskKey: TASK_KEY,
taskName: taskTitle.value,
taskId: taskConfig.value?.id || "",
currentStep: currentStep.value,
steps: steps.value.map((item) => item.name),
classifications: classifications.value,
checkResult: checkResult.value,
choiceAnswers: stepOneChoiceAnswers.value,
step2Form: step2Form.value,
step3Rows: step3Rows.value,
step4Form: step4Form.value,
};
}
function persistDraft() {
if (!draftReady.value) return;
try {
localStorage.setItem(getLocalDraftKey(), JSON.stringify(buildOutcome()));
} catch (error) {
// 本地草稿只是兜底,后端保存仍是主来源。
}
}
function loadDraft() {
try {
const raw = localStorage.getItem(getLocalDraftKey());
if (raw) {
applySavedAnswer(raw);
}
} catch (error) {
clearDraft();
}
}
function clearDraft() {
try {
localStorage.removeItem(getLocalDraftKey());
} catch (error) {
// 忽略浏览器存储异常。
}
}
function exportOutcome() {
const content = buildOutcome();
const blob = new Blob([JSON.stringify(content, null, 2)], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "新产品调查与分析-实训成果.json";
link.click();
URL.revokeObjectURL(url);
}
</script>
<style lang="scss" scoped>
/*
* 页面级令牌(墨黑极简主题):纯白画布 + 墨黑主操作 + 中性灰层次。
* 覆盖全局主色/文字/边框令牌,使所有 var(--student-*) 规则自动重着色。
*/
.new-product-value-page {
/* 主色:墨黑 */
--student-accent: #1f2937;
--student-accent-deep: #111827;
--student-accent-light: #4b5563;
--student-accent-soft: #f3f4f6;
/* 点缀色:暖橙(小面积使用) */
--student-terracotta: #e8953c;
/* 语义色 */
--student-danger: #d9554c;
--student-success: #4a9d6e;
/* 三层次标识色:石墨 / 青灰 / 灰紫 */
--student-layer-1: #374151;
--student-layer-2: #43a294;
--student-layer-3: #8b6fc0;
/* 结构色:中性灰白系 */
--student-panel-border: #e5e7eb;
--student-card-bg: #ffffff;
--student-panel-bg: #fafbfc;
--student-text: #1f2937;
--student-text-secondary: #6b7280;
/* 紧凑字号梯度:在全局 --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 统一提供 */
.training-core {
display: grid;
gap: 10px;
align-content: start;
}
/* 任务头:徽章 + 标题 + 单行简介 + 右侧工具栏(合并原 task-header 与 workbench-head 双标题) */
.task-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin: 0;
padding: 16px;
border: 1px solid #e7e9ed;
border-radius: 16px;
background: #ffffff;
}
.task-head-main {
display: grid;
justify-items: start;
gap: 8px;
min-width: 0;
}
.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;
}
.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;
}
}
.task-card {
border: 0;
background: transparent;
}
.step-title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
/* Step 编号小标签:弱化处理,作为标题上方的灰色引导文字 */
.section-eyebrow {
display: inline-flex;
align-items: center;
margin: 0;
padding: 0;
color: var(--student-text-secondary);
font-size: 11px;
font-weight: 800;
line-height: 1.3;
letter-spacing: 0.12em;
}
/*
* 次按钮基类(.outline-action / .btn-nav / .btn-ghost 共用语言):
* 白底 + 墨黑描边 + 墨黑文字;圆角矩形而非全胶囊。
*/
.outline-action,
.btn-nav {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 40px;
padding: 0 18px;
border: 1px solid rgba(31, 41, 55, 0.4);
border-radius: 10px;
color: var(--student-accent);
background: #ffffff;
font-family: inherit;
font-weight: 700;
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(31, 41, 55, 0.12);
}
&:disabled {
cursor: not-allowed;
opacity: 0.45;
}
}
.step-content {
animation: fade 0.2s ease;
}
@keyframes fade {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.step-card {
margin-bottom: 0;
border: 1px solid var(--student-panel-border);
border-radius: 16px;
background: var(--student-card-bg);
}
.step-title-row {
margin-bottom: 10px;
h3 {
margin: 2px 0 0;
color: var(--student-text);
}
}
.step-chip {
flex: 0 0 auto;
padding: 6px 12px;
border: 1px solid #d6dae0;
border-radius: 999px;
color: #1f2937;
background: #f3f4f6;
font-size: 12px;
font-weight: 800;
}
.step-desc {
margin: 0 0 14px;
color: #374151;
font-size: 13px;
line-height: 1.6;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
&.three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.field-block {
display: grid;
gap: 6px;
margin-bottom: 12px;
color: #374151;
font-size: 13px;
font-weight: 800;
> span {
line-height: 1.5;
}
}
.input-field,
.input-single,
.cell-input,
.cell-textarea {
width: 100%;
border: 1px solid #d6dae0;
border-radius: 14px;
color: #1f2937;
background: #ffffff;
font-family: inherit;
font-size: 13px;
outline: none;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
&::placeholder {
color: #9ca3af;
}
&:focus {
border-color: #4b5563;
box-shadow: 0 0 0 3px rgba(31, 41, 55, 0.12);
}
}
.input-field {
min-height: 110px;
padding: 12px 14px;
resize: vertical;
line-height: 1.65;
}
.step4-input {
min-height: 168px;
}
.input-single {
min-height: 42px;
padding: 0 14px;
}
.price-options {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.price-option {
min-height: 42px;
border: 1px solid #d6dae0;
border-radius: 14px;
color: #6b7280;
background: #ffffff;
font-family: inherit;
font-weight: 800;
cursor: pointer;
&.active {
color: #ffffff;
border-color: var(--student-accent);
background: var(--student-accent);
box-shadow: 0 2px 8px rgba(31, 41, 55, 0.25);
}
}
.survey-form {
display: grid;
gap: 12px;
}
.survey-field-group {
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 14px;
background: linear-gradient(135deg, #f3f4f6, #fafbfc);
}
.survey-field-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 10px;
color: #1f2937;
font-size: 14px;
font-weight: 900;
b {
display: inline-grid;
width: 20px;
height: 20px;
place-items: center;
border-radius: 50%;
color: #ffffff;
background: #1f2937;
font-size: 11px;
}
}
.survey-field-hint {
margin: -2px 0 10px 28px;
color: #6b7280;
font-size: 12px;
line-height: 1.55;
}
.survey-options {
display: grid;
gap: 9px;
}
.step-two-survey {
.step-title-row {
h3 {
font-size: 15px;
}
}
.survey-options {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
:deep(.survey-option.el-radio) {
padding: 8px 10px;
.el-radio__label {
gap: 2px;
padding-left: 7px;
}
span {
font-size: 11px;
line-height: 1.35;
}
}
}
:deep(.survey-option.el-radio) {
display: grid;
grid-template-columns: auto 1fr;
align-items: start;
height: auto;
margin-right: 0;
padding: 10px 12px;
border: 1px solid #e5e7eb;
border-radius: 12px;
color: #374151;
background: #fafbfc;
transition: border-color 0.18s ease, background 0.18s ease;
.el-radio__label {
display: grid;
gap: 4px;
padding-left: 9px;
white-space: normal;
}
strong {
color: #1f2937;
font-weight: 800;
}
span {
color: #6b7280;
font-size: 12px;
line-height: 1.55;
}
&.is-checked {
border-color: #4b5563;
background: #f3f4f6;
}
}
/* 单选圆形标记:选中时墨黑填充,覆盖 Element Plus 默认蓝色 */
:deep(.survey-option.el-radio .el-radio__input.is-checked .el-radio__inner) {
border-color: #1f2937;
background: #1f2937;
}
:deep(.survey-option.el-radio .el-radio__input.is-checked .el-radio__inner::after) {
background: #ffffff;
}
:deep(.survey-option.el-radio .el-radio__input .el-radio__inner) {
border-color: #9ca3af;
}
/*
* 全页单选统一墨黑(Step 1 选择题带边框单选等):
* 圆形标记墨黑填充 + 带边框单选选中时墨黑描边/文字。
*/
.new-product-value-page :deep(.el-radio__input.is-checked .el-radio__inner) {
border-color: #1f2937;
background: #1f2937;
}
.new-product-value-page :deep(.el-radio__input.is-checked .el-radio__inner::after) {
background: #ffffff;
}
.new-product-value-page :deep(.el-radio.is-checked) {
border-color: #1f2937;
color: #1f2937;
}
.new-product-value-page :deep(.el-radio.is-checked .el-radio__label) {
color: #1f2937;
}
.custom-scene-input {
margin-top: 10px;
}
.product-name-field {
display: grid;
grid-template-columns: 76px minmax(0, 1fr);
align-items: center;
gap: 10px;
margin-bottom: 10px;
color: #374151;
font-size: 12px;
font-weight: 800;
}
.image-upload-trigger {
display: inline-flex;
min-height: 42px;
align-items: center;
justify-content: center;
padding: 0 18px;
border: 1px dashed #9ca3af;
border-radius: 12px;
color: #374151;
background: #fafbfc;
font-weight: 700;
cursor: pointer;
transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
input {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
}
&:hover:not(.disabled) {
transform: translateY(-1px);
border-color: #1f2937;
background: #f3f4f6;
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
.product-image-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
gap: 12px;
margin-top: 14px;
}
.product-image-card {
display: grid;
gap: 7px;
min-width: 0;
margin: 0;
padding: 8px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #fafbfc;
figcaption {
overflow: hidden;
color: #6b7280;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.product-image-preview {
aspect-ratio: 1.2;
overflow: hidden;
padding: 0;
border: 0;
border-radius: 8px;
background: #fafbfc;
cursor: zoom-in;
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.2s ease;
}
&:hover img {
transform: scale(1.05);
}
}
.remove-image-button {
justify-self: start;
padding: 0;
border: 0;
color: var(--student-danger);
background: transparent;
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: color 0.16s ease;
&:hover {
color: #b0443d;
text-decoration: underline;
}
}
.product-image-full {
display: block;
width: 100%;
max-height: 72vh;
object-fit: contain;
}
.training-table-wrap {
overflow-x: auto;
border: 1px solid #e5e7eb;
border-radius: 22px;
}
.training-data-table {
width: 100%;
min-width: 920px;
border-collapse: collapse;
th,
td {
border: 1px solid #e5e7eb;
padding: 12px;
vertical-align: top;
}
th {
color: #1f2937;
font-size: 13px;
font-weight: 900;
text-align: center;
}
td {
color: #374151;
}
}
.cell-input {
min-height: 38px;
padding: 0 10px;
}
.cell-textarea {
min-height: 118px;
padding: 10px;
resize: vertical;
line-height: 1.6;
}
.dimension-cell {
min-width: 170px;
color: #1f2937;
text-align: left;
strong,
span {
display: block;
}
strong {
margin-bottom: 7px;
font-weight: 900;
}
span {
color: #6b7280;
font-size: 12px;
font-weight: 700;
line-height: 1.55;
}
}
.requirement-cell,
.example-cell {
min-width: 170px;
color: #374151;
line-height: 1.65;
}
.example-cell {
color: #c08a2e;
}
.reference-points {
display: grid;
gap: 8px;
margin: 0;
padding-left: 20px;
}
.analysis-input-cell {
min-width: 280px;
}
.analysis-count {
margin: 8px 2px 0;
color: #d9554c;
font-size: 12px;
font-weight: 700;
&.is-ready {
color: #4a9d6e;
}
}
.example-hint {
display: flex;
gap: 8px;
align-items: flex-start;
margin-top: 12px;
padding: 12px 14px;
border: 1px solid #ffe3a3;
border-radius: 16px;
color: #c08a2e;
background: rgba(192, 138, 46, 0.1);
font-size: 13px;
line-height: 1.6;
}
.step-navigation {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 14px;
margin-top: 22px;
}
/*
* 主次层级:主按钮实底墨黑 + 中性柔影(扁平不渐变);
* 次按钮白底墨黑描边;统一 10px 圆角矩形。
*/
.btn-primary {
min-height: 40px;
padding: 0 22px;
border: 1px solid var(--student-accent);
border-radius: 10px;
color: #ffffff;
background: var(--student-accent);
font-family: inherit;
font-weight: 700;
box-shadow: 0 3px 10px rgba(31, 41, 55, 0.28);
cursor: pointer;
transition: transform 0.16s ease, background 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
border-color: var(--student-accent-deep);
background: var(--student-accent-deep);
box-shadow: 0 5px 14px rgba(17, 24, 39, 0.32);
}
.btn-primary:active:not(:disabled) {
transform: translateY(0);
}
.btn-ghost {
min-height: 40px;
padding: 0 18px;
border: 1px solid rgba(31, 41, 55, 0.4);
border-radius: 10px;
color: var(--student-accent);
background: #ffffff;
font-family: inherit;
font-weight: 700;
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease;
}
.btn-ghost:hover:not(:disabled) {
border-color: var(--student-accent);
background: var(--student-accent-soft);
}
/*
* 三级按钮(保存草稿):浅灰实底 + 深灰字,无重描边,
* 视觉权重低于"上一步"描边按钮,形成 实底黑 > 描边白 > 浅灰底 的层级。
*/
.btn-save {
min-height: 40px;
padding: 0 18px;
border: 1px solid #e5e7eb;
border-radius: 10px;
color: #374151;
background: #f3f4f6;
font-family: inherit;
font-weight: 700;
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease;
}
.btn-save:hover:not(:disabled) {
border-color: #d1d5db;
background: #e9ecef;
}
/* 底部三按钮统一禁用态:不改变形状,只降低不透明度 */
.btn-primary:disabled,
.btn-ghost:disabled,
.btn-save:disabled {
cursor: not-allowed;
opacity: 0.5;
transform: none;
box-shadow: none;
}
.step-progress {
position: absolute;
right: 14px;
color: #6b7280;
font-family: var(--student-font-data);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.5px;
}
/*
* 三层次分类表格(Step 01):白底卡片化行式表格。
* 去掉全边框大色块,改为细分隔线 + 序号徽章 + 层次语义色条。
*/
.classification-table-wrap {
margin-top: 14px;
border: 1px solid var(--student-panel-border);
border-radius: 14px;
background: #ffffff;
box-shadow: 0 1px 2px rgba(17, 24, 39, 0.04);
}
.classification-table {
th,
td {
border: none;
border-bottom: 1px solid #eceef1;
}
th {
padding: 11px 14px;
color: var(--student-text-secondary);
background: #ffffff;
font-size: 12px;
font-weight: 800;
letter-spacing: 2px;
text-align: left;
white-space: nowrap;
}
td {
padding: 9px 14px;
color: #374151;
background: #ffffff;
font-size: 12px;
}
tbody tr:last-child td {
border-bottom: none;
}
th:first-child,
td:first-child {
width: 64px;
text-align: center;
}
th:last-child,
td:last-child {
width: 230px;
}
}
/* 表头层次图例:三个语义色小色条 */
.layer-legend {
display: inline-flex;
gap: 4px;
margin-left: 8px;
vertical-align: 2px;
i {
width: 12px;
height: 4px;
border-radius: 2px;
background: var(--dot);
}
}
/* 序号徽章:浅灰圆角方块 + 等宽数字 */
.classification-number {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
border-radius: 6px;
color: var(--student-accent-deep);
background: #f3f4f6;
font-family: var(--student-font-data);
font-size: 12px;
font-weight: 800;
}
.classification-selling-point {
color: #28303c;
font-weight: 600;
line-height: 1.65;
}
.classification-select-cell :deep(.el-select) {
width: 100%;
}
/* 选中值文字强制深色不透明(覆盖禁用态浅色与全局浅色文字污染) */
.classification-select-cell {
:deep(.el-select__selected-item),
:deep(.el-select__placeholder),
:deep(.el-input__inner) {
color: #1f2937 !important;
-webkit-text-fill-color: #1f2937 !important;
font-weight: 600;
}
/* 判题请求中(禁用态):用清晰可见的中度灰蓝,而非近乎透明白 */
:deep(.el-select.is-disabled .el-select__selected-item),
:deep(.el-input.is-disabled .el-input__inner) {
color: #565e6b !important;
-webkit-text-fill-color: #565e6b !important;
}
:deep(.el-select__caret) {
color: #6b7280;
}
}
/* 数据行统一纯白:不随层次选择/判题结果变色,对错仅以文字反馈提示 */
.classification-table tbody td {
background: #ffffff !important;
box-shadow: none !important;
}
.classification-feedback,
.classification-summary {
margin: 8px 0 0;
color: #d9554c;
font-size: 12px;
line-height: 1.45;
}
.classification-row--correct .classification-feedback,
.classification-summary.is-success {
color: var(--student-success);
}
.classification-summary.is-success {
background: rgba(74, 157, 110, 0.08);
}
/* 判题汇总横幅 */
.classification-summary {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 8px 12px;
border-radius: 10px;
color: var(--student-danger);
background: rgba(217, 85, 76, 0.08);
font-size: 12px;
font-weight: 700;
}
/* 三层次彩色标识条:Step 03 维度列 / Step 04 分层卡片共用同一组语义色 */
.dimension-cell::before {
content: "";
display: block;
width: 26px;
height: 4px;
margin-bottom: 7px;
border-radius: 2px;
background: var(--student-accent);
}
.layer-table tr:nth-child(1) .dimension-cell::before {
background: var(--student-layer-1);
}
.layer-table tr:nth-child(2) .dimension-cell::before {
background: var(--student-layer-2);
}
.layer-table tr:nth-child(3) .dimension-cell::before {
background: var(--student-layer-3);
}
.field-block.is-layer {
position: relative;
margin-bottom: 12px;
padding: 12px 14px 12px 20px;
border: 1px solid var(--student-panel-border);
border-radius: 10px;
background: var(--student-card-bg);
overflow: hidden;
&::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 5px;
background: var(--bar, var(--student-accent));
}
}
.field-block.layer-core {
--bar: var(--student-layer-1);
}
.field-block.layer-tangible {
--bar: var(--student-layer-2);
}
.field-block.layer-extended {
--bar: var(--student-layer-3);
}
@media (max-width: 1100px) {
.step-progress {
position: static;
width: 100%;
text-align: center;
}
}
@media (max-width: 900px) {
.task-header {
flex-direction: column;
}
.step-navigation {
flex-direction: column;
align-items: stretch;
}
.btn-nav {
width: 100%;
}
.classification-table {
min-width: 720px;
}
}
@media (max-width: 720px) {
.step-two-survey {
.survey-options {
grid-template-columns: 1fr;
}
}
}
.new-product-survey-toolbar {
flex: 0 0 auto;
justify-content: flex-end;
max-width: 100%;
}
@media (max-width: 900px) {
.new-product-survey-toolbar {
width: 100%;
justify-content: flex-start;
}
}
</style>
<!--
非 scoped 全局微调(墨黑极简主题,仅作用于本页):
1) app-main 画布改为纯白(scoped 无法触及祖先节点);
2) AI 助学侧栏组件整体中性灰白换肤,避免与页面风格割裂。
所有选择器均以 .new-product-value-page 为根,不影响其他页面。
-->
<style lang="scss">
/* ============================================================
* 步骤导航重设计:按钮切换风(分段按钮 + 墨黑激活)
* id 锚点 + !important 覆盖全局浅蓝胶囊与杂志风旧规则。
* ============================================================ */
#app .studentLayout section.app-main:has(.new-product-value-page) .step-indicator {
display: flex !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: center !important;
gap: 10px !important;
width: 100% !important;
margin: 4px 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: 220px !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: 42px !important;
margin: 0 !important;
padding: 8px 12px !important;
border: 1px solid #e5e7eb !important;
border-radius: 8px !important;
background: #ffffff !important;
box-shadow: none !important;
color: #6b7280 !important;
font-size: 13px !important;
font-weight: 600 !important;
line-height: 1.3 !important;
letter-spacing: 0 !important;
white-space: normal !important;
text-align: center !important;
cursor: pointer;
transition: background-color 0.16s ease, color 0.16s ease, border-color 0.16s ease !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: ui-monospace, "SF Mono", Consolas, monospace !important;
font-size: 12px !important;
font-weight: 700 !important;
line-height: 1 !important;
letter-spacing: 0 !important;
transition: background-color 0.16s ease, color 0.16s ease !important;
&::after { display: none !important; }
}
.step-name { color: inherit !important; }
/* 未到达项 hover:黑边浅底,编号徽章转墨黑 */
&:hover:not(.active):not(.complete) {
border-color: #1f2937 !important;
background: #f9fafb !important;
color: #1f2937 !important;
.step-no { background: #1f2937 !important; color: #ffffff !important; }
}
/* 已完成:绿色编号徽章 */
&.complete:not(.active) {
border-color: #dce9e2 !important;
color: #374151 !important;
.step-no { background: rgba(74, 157, 110, 0.12) !important; color: #3f8a5f !important; }
&:hover {
border-color: #4a9d6e !important;
background: #f9fafb !important;
.step-no { background: #4a9d6e !important; color: #ffffff !important; }
}
}
/* 当前步骤:墨黑实底白字,白色编号徽章黑字 */
&.active {
border-color: #1f2937 !important;
background: #1f2937 !important;
color: #ffffff !important;
font-weight: 700 !important;
.step-no { background: #ffffff !important; color: #1f2937 !important; }
&:hover {
background: #111827 !important;
border-color: #111827 !important;
}
}
}
}
/* 窄屏:横向滑动,不接受全局 1100px 纵向堆叠 */
@media (max-width: 900px) {
#app .studentLayout section.app-main:has(.new-product-value-page) .step-indicator {
justify-content: flex-start !important;
gap: 8px !important;
overflow-x: auto !important;
}
#app .studentLayout section.app-main:has(.new-product-value-page) .step-indicator .step-tab {
flex: 0 0 148px !important;
max-width: none !important;
}
}
/* ===== AI 侧栏样式已收敛进 TrainingAiSidebar 组件(墨黑极简),此处不再覆盖 ===== */
/*
* 顶部任务工具栏四按钮统一设计(下载案例资料 / 提交任务 / 重新实训 / 导出实训成果):
* 统一 36px 高、8px 圆角;主操作墨黑实底,次操作白底灰描边。
* 强制 text-shadow:none 消除文字重影。
*/
.new-product-value-page .training-task-toolbar {
gap: 8px;
}
/* 次按钮基类:白底 + 中性灰描边 + 深灰字 */
.new-product-value-page .task-toolbar-button.el-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 36px;
height: 36px;
padding: 0 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
color: #374151;
background: #ffffff;
box-shadow: none;
text-shadow: none;
font-size: 13px;
font-weight: 600;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease;
}
.new-product-value-page .task-toolbar-button.el-button .el-icon {
font-size: 14px;
color: inherit;
}
.new-product-value-page .task-toolbar-button.el-button:hover,
.new-product-value-page .task-toolbar-button.el-button:focus {
border-color: #1f2937;
color: #1f2937;
background: #f9fafb;
box-shadow: 0 2px 6px rgba(17, 24, 39, 0.1);
}
/* 主按钮(提交任务):墨黑实底白字 */
.new-product-value-page .task-toolbar-button--submit.el-button {
border-color: #1f2937;
color: #ffffff;
background: #1f2937;
box-shadow: 0 2px 8px rgba(17, 24, 39, 0.22);
}
.new-product-value-page .task-toolbar-button--submit.el-button:hover,
.new-product-value-page .task-toolbar-button--submit.el-button:focus {
border-color: #111827;
color: #ffffff;
background: #111827;
box-shadow: 0 4px 12px rgba(17, 24, 39, 0.28);
}
/* 导出按钮 loading 态保持白底灰描边不变 */
.new-product-value-page .task-toolbar-button.el-button.is-loading {
border-color: #d1d5db;
color: #374151;
background: #ffffff;
}
</style>