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/product/components/ProductStandardTrainingPage...

1099 lines
34 KiB
Vue

<template>
<div class="product-standard-page student-training-shell">
<main class="training-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-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="hasConfiguredSteps" class="step-indicator" :aria-label="`${taskTitle}步骤`">
<template v-for="(step, index) in configuredSteps" :key="`${step}-${index}`">
<button type="button" :class="['step-tab', { active: activeStep === index + 1, complete: activeStep > index + 1 }]" :aria-current="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">{{ activeStep === index + 1 ? "当前步骤" : "点击切换" }} <span aria-hidden="true">→</span></span></span>
</button>
<span v-if="index < configuredSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section class="step-card">
<div class="step-title-row">
<div>
<p v-if="hasConfiguredSteps" class="section-eyebrow">Step {{ String(activeStep).padStart(2, "0") }}</p>
<h3>{{ activeStepName }}</h3>
</div>
</div>
<div class="step-content">
<slot v-if="hasCustomContent" name="step-content" :active-step="activeStep" :active-step-name="activeStepName" :has-configured-steps="hasConfiguredSteps" />
<template v-else>
<p v-if="page.keyPoints?.length && isFirstStep" class="step-desc">请围绕以下内容组织需求说明书,确保文档能够支撑设计、开发、测试和验收协作。</p>
<div v-if="page.keyPoints?.length && isFirstStep" class="key-point-grid">
<span v-for="item in page.keyPoints" :key="item">{{ item }}</span>
</div>
<label v-for="field in visibleFields" :key="field.key" class="field-block">
<span>{{ field.label }}</span>
<el-input v-model="form[field.key]" :type="field.type === 'textarea' ? 'textarea' : 'text'" :autosize="field.type === 'textarea' ? { minRows: 6, maxRows: 11 } : undefined" :placeholder="field.placeholder" />
</label>
<div v-if="showWorkTable" class="table-panel">
<div class="panel-title"><h4>{{ page.tableTitle || "实训工作表" }}</h4><p>请结合产品对象填写对应内容,形成可提交的实训成果。</p></div>
<div class="table-wrap">
<table>
<thead><tr><th>项目</th><th>说明</th><th>学生填写</th></tr></thead>
<tbody><tr v-for="row in workRows" :key="row.name"><th scope="row">{{ row.name }}</th><td>{{ row.description }}</td><td><el-input v-model="row.answer" type="textarea" :autosize="{ minRows: 3, maxRows: 6 }" placeholder="填写本项分析内容" /></td></tr></tbody>
</table>
</div>
</div>
<p v-if="!visibleFields.length && !showWorkTable" class="step-desc">请根据本步骤要求梳理前述内容,确认信息完整后继续下一步。</p>
</template>
</div>
</section>
<div v-if="hasConfiguredSteps" class="step-navigation">
<button type="button" class="btn-nav" :disabled="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="activeStep === configuredSteps.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" :loading="saving" @click="resetTask">清空填写</el-button>
</div>
</section>
</main>
<TrainingAiSidebar :study-tip="`当前任务建议:围绕“${taskTitle}”先明确产品对象,再按配置步骤补全实训内容。`" :progress-items="progressItems" />
</div>
</template>
<script setup>
import { computed, getCurrentInstance, onMounted, ref, useSlots } from "vue";
import { ArrowLeft, ArrowRight, CircleCheck, Document, 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 { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { getStudentTrainingPage } from "@/views/training/studentTrainingPages";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
const props = defineProps({ taskKey: { type: String, required: true }, title: { type: String, default: "" }, background: { type: String, default: "" }, requirement: { type: String, default: "" }, goals: { type: Array, default: () => [] }, badgeText: { type: String, default: "核心实训 · 产品规划与设计" } });
const emit = defineEmits(["save", "submit", "reset"]);
const slots = useSlots();
const { proxy } = getCurrentInstance();
const taskConfig = ref(null);
const briefOpen = ref(false);
const saving = ref(false);
const activeStep = ref(1);
const page = computed(() => getStudentTrainingPage(props.taskKey));
const form = ref(createForm());
const workRows = ref(createRows());
const hasCustomContent = computed(() => Boolean(slots["step-content"]));
const taskTitle = computed(() => taskConfig.value?.taskName || props.title || page.value.title);
const taskBackground = computed(() => taskConfig.value?.background || props.background || page.value.background);
const taskRequirement = computed(() => taskConfig.value?.requirements || props.requirement || page.value.task);
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives, props.goals.length ? props.goals : page.value.goals));
const configuredSteps = computed(() => parseTrainingArray(taskConfig.value?.steps));
const hasConfiguredSteps = computed(() => configuredSteps.value.length > 0);
const activeStepName = computed(() => configuredSteps.value[activeStep.value - 1] || page.value.title);
const fieldsPerStep = computed(() => hasConfiguredSteps.value ? Math.max(1, Math.ceil((page.value.fields?.length || 0) / configuredSteps.value.length)) : page.value.fields?.length || 0);
const visibleFields = computed(() => {
const fields = page.value.fields || [];
if (!hasConfiguredSteps.value) return fields;
const start = (activeStep.value - 1) * fieldsPerStep.value;
return fields.slice(start, start + fieldsPerStep.value);
});
const showWorkTable = computed(() => Boolean(workRows.value.length) && (!hasConfiguredSteps.value || activeStep.value === configuredSteps.value.length));
const isFirstStep = computed(() => !hasConfiguredSteps.value || activeStep.value === 1);
const progressItems = computed(() => configuredSteps.value.map((text, index) => ({ text, status: index + 1 < activeStep.value ? "done" : index + 1 === activeStep.value ? "doing" : "" })));
function createForm() { return Object.fromEntries((page.value.fields || []).map((field) => [field.key, ""])); }
function createRows() { return (page.value.tableRows || []).map(([name, description]) => ({ name, description, answer: "" })); }
function normalizeActiveStep(value) { if (!configuredSteps.value.length) return 1; return Math.min(Math.max(Number(value) || 1, 1), configuredSteps.value.length); }
function buildProgress() { return { activeStep: activeStep.value, form: form.value, workRows: workRows.value, updatedAt: new Date().toISOString() }; }
function applyProgress(value) { if (!value || typeof value !== "object") return; activeStep.value = normalizeActiveStep(value.activeStep); form.value = { ...createForm(), ...(value.form || {}) }; workRows.value = createRows().map((row, index) => ({ ...row, answer: value.workRows?.[index]?.answer || "" })); }
onMounted(async () => { await loadTaskConfig(); await loadSavedProgress(); });
async function loadTaskConfig() { try { const res = await getTrainingTaskByKey(props.taskKey); taskConfig.value = res?.data || null; activeStep.value = normalizeActiveStep(activeStep.value); } catch (error) { taskConfig.value = null; activeStep.value = 1; } }
async function loadSavedProgress() { try { const res = await getStudentTrainingAnswer(props.taskKey); const raw = res?.data?.step1Answer; if (raw) applyProgress(JSON.parse(raw)); } catch (error) { /* The training task remains available without previous progress. */ } }
async function persistProgress(saveAction = "SAVE", silent = false) { await saveStudentTrainingAnswer(props.taskKey, { saveAction, currentStep: activeStep.value, step1Answer: JSON.stringify(buildProgress()) }); if (!silent) proxy?.$modal?.msgSuccess(saveAction === "SUBMIT" ? "提交成功" : "保存成功"); }
function setCurrentStep(step) { activeStep.value = normalizeActiveStep(step); persistProgress("SAVE", true).catch(() => {}); }
function goPreviousStep() { setCurrentStep(activeStep.value - 1); }
function goNextStep() { setCurrentStep(activeStep.value + 1); }
async function saveCurrentProgress() { if (saving.value) return; saving.value = true; try { if (hasCustomContent.value) { emit("save"); await persistProgress("SAVE", true); } else { await persistProgress(); } } finally { saving.value = false; } }
async function submitTask() { if (saving.value) return; saving.value = true; try { if (hasCustomContent.value) { emit("submit"); await persistProgress("SAVE", true); } else { await persistProgress("SUBMIT"); } } finally { saving.value = false; } }
async function resetTask() { if (saving.value) return; saving.value = true; try { if (hasCustomContent.value) emit("reset"); form.value = createForm(); workRows.value = createRows(); activeStep.value = 1; await persistProgress("RESET", hasCustomContent.value); if (!hasCustomContent.value) proxy?.$modal?.msgSuccess("清空填写成功"); } finally { saving.value = false; } }
function exportOutcome() { const blob = new Blob([JSON.stringify(buildProgress(), 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">
/* ============================================================
* 教育蓝极简 · 产品标准实训页
* ============================================================ */
.product-standard-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;
/* 字号梯度:紧凑 */
--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;
display: grid;
grid-template-columns: minmax(720px, 1fr) 340px;
gap: 24px;
min-height: calc(100vh - 76px);
padding: 24px 24px 32px;
color: var(--student-text);
background: #ffffff;
}
/* ============================================================
* 训练核心容器
* ============================================================ */
.training-core {
display: grid;
gap: 12px;
align-content: start;
min-width: 0;
padding: 20px;
border: 1px solid var(--student-panel-border);
border-radius: 12px;
background: #ffffff;
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-title {
margin: 0;
color: var(--student-text);
font-size: var(--student-fs-display);
font-weight: 700;
line-height: 1.4;
letter-spacing: -0.01em;
}
.task-brief {
margin: 0;
color: var(--student-text-secondary);
font-size: var(--student-fs-body-sm);
line-height: 1.65;
}
.task-brief-text {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.task-brief-toggle {
margin-left: 6px;
padding: 0;
border: 0;
color: var(--student-accent);
background: transparent;
font: inherit;
font-weight: 600;
cursor: pointer;
&:hover {
color: var(--student-accent-deep);
text-decoration: underline;
}
}
.task-brief-more {
display: grid;
gap: 6px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #e5e7eb;
b {
color: var(--student-text);
font-size: var(--student-fs-body-sm);
}
ul {
display: grid;
gap: 3px;
margin: 0;
padding-left: 18px;
color: var(--student-text-secondary);
font-size: var(--student-fs-body-sm);
line-height: 1.6;
}
p {
margin: 0;
color: var(--student-text-secondary);
font-size: var(--student-fs-body-sm);
line-height: 1.6;
}
}
/* ============================================================
* 工作台容器
* ============================================================ */
.task-card {
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
display: grid;
gap: 10px;
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 0;
padding-bottom: 0;
border-bottom: 0;
}
.workbench-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--student-panel-border);
}
.workbench-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
flex-shrink: 0;
}
.section-eyebrow {
margin: 0;
color: var(--student-accent);
font-size: var(--student-fs-caption);
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.step-title-row {
margin-bottom: 10px;
}
.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;
}
.step-desc {
margin: 0 0 14px;
color: #4b5563;
font-size: var(--student-fs-body);
line-height: 1.7;
}
/* ============================================================
* 按钮(教育蓝极简)
* ============================================================ */
.outline-action,
.btn-nav {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
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-size: var(--student-fs-button);
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease,
color 0.16s ease, transform 0.16s ease,
box-shadow 0.16s ease;
}
.outline-action:hover:not(:disabled),
.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 2px 6px rgba(37, 99, 235, 0.12);
}
.btn-primary {
border-color: var(--student-accent);
color: #ffffff;
background: var(--student-accent);
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.2), 0 2px 8px rgba(37, 99, 235, 0.15);
}
.btn-primary:hover:not(:disabled) {
border-color: var(--student-accent-deep);
background: var(--student-accent-deep);
color: #ffffff;
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.3), 0 4px 12px rgba(29, 78, 216, 0.22);
}
.btn-save {
border: 1px solid #e5e7eb;
color: #374151;
background: #f9fafb;
}
.btn-save:hover:not(:disabled) {
border-color: #d1d5db;
background: #f3f4f6;
color: #374151;
transform: none;
box-shadow: none;
}
.btn-nav:disabled {
cursor: not-allowed;
opacity: 0.5;
transform: none;
box-shadow: none;
}
/* ============================================================
* 步骤导航(紧凑)
* ============================================================ */
.step-indicator {
display: flex;
align-items: stretch;
gap: 0;
width: 100%;
margin: 0 0 16px;
padding: 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
overflow-x: auto;
scrollbar-width: thin;
}
.step-tab {
display: flex;
flex: 1 0 145px;
align-items: center;
min-width: 0;
min-height: 40px;
padding: 8px 12px;
border: 1px solid var(--student-panel-border);
border-right: none;
border-radius: 0;
color: #6b7280;
background: #ffffff;
font-size: var(--student-fs-body);
font-weight: 600;
text-align: left;
cursor: pointer;
transition: all 0.16s ease;
&:first-child {
border-radius: 8px 0 0 8px;
}
&:last-child {
border-radius: 0 8px 8px 0;
border-right: 1px solid var(--student-panel-border);
}
&:hover:not(.active):not(.complete) {
border-color: var(--student-accent);
background: var(--student-accent-soft);
color: var(--student-accent);
z-index: 1;
}
&.complete:not(.active) {
color: #374151;
.step-marker {
background: rgba(43, 164, 113, 0.12);
color: #16915f;
}
&:hover {
border-color: var(--student-success);
background: #f0fdf6;
.step-marker {
background: var(--student-success);
color: #ffffff;
}
}
}
&.active {
border-color: var(--student-accent);
background: var(--student-accent);
color: #ffffff;
font-weight: 700;
z-index: 2;
.step-marker {
background: #ffffff;
color: var(--student-accent);
}
.step-action {
color: rgba(255, 255, 255, 0.65);
}
&:hover {
background: var(--student-accent-deep);
border-color: var(--student-accent-deep);
}
}
}
.step-marker {
display: inline-flex;
flex: 0 0 22px;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 6px;
background: #f3f4f6;
color: #6b7280;
font-family: ui-monospace, "SF Mono", Consolas, monospace;
font-size: 11px;
font-weight: 700;
line-height: 1;
transition: all 0.16s ease;
}
.step-copy {
display: grid;
min-width: 0;
gap: 2px;
margin-left: 8px;
}
.step-name {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
font-size: var(--student-fs-body);
font-weight: 600;
line-height: 1.25;
color: inherit;
}
.step-action {
color: var(--student-accent);
font-size: var(--student-fs-caption);
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1;
}
.step-connector {
display: none;
}
/* ============================================================
* 步骤卡片
* ============================================================ */
.step-card {
padding: 18px 20px;
border: 1px solid var(--student-panel-border);
border-radius: 12px;
background: #ffffff;
}
/* ============================================================
* 内容区
* ============================================================ */
.step-content {
display: grid;
gap: 12px;
}
.key-point-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin: 0;
}
.key-point-grid span {
padding: 12px 14px;
border: 1px solid #bfdbfe;
border-radius: 8px;
background: var(--student-accent-soft);
color: var(--student-text);
font-size: var(--student-fs-body);
font-weight: 600;
line-height: 1.55;
}
.field-block {
display: block;
margin: 8px 0 0;
}
.field-block > span {
display: block;
margin-bottom: 6px;
color: var(--student-text);
font-size: var(--student-fs-body);
font-weight: 600;
line-height: 1.6;
}
.field-block :deep(.el-input__wrapper),
.field-block :deep(.el-textarea__inner),
.table-wrap :deep(.el-textarea__inner) {
border: 1px solid #d1d5db;
border-radius: 8px;
box-shadow: none !important;
background: #ffffff;
}
.field-block :deep(.el-input__wrapper) {
min-height: 40px;
padding: 0 12px;
}
.field-block :deep(.el-input__inner),
.field-block :deep(.el-textarea__inner),
.table-wrap :deep(.el-textarea__inner) {
color: var(--student-text);
font-size: var(--student-fs-body);
}
.field-block :deep(.el-textarea__inner),
.table-wrap :deep(.el-textarea__inner) {
min-height: 110px !important;
padding: 10px 12px;
line-height: 1.65;
}
.field-block :deep(.el-input__wrapper:hover),
.field-block :deep(.el-textarea__inner:hover) {
border-color: var(--student-accent-light);
}
.field-block :deep(.el-input__wrapper.is-focus),
.field-block :deep(.el-textarea__inner:focus) {
border-color: var(--student-accent);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1) !important;
}
/* ============================================================
* 表格
* ============================================================ */
.table-panel {
margin-top: 14px;
padding: 16px;
border: 1px solid var(--student-panel-border);
border-radius: 10px;
background: #ffffff;
}
.panel-title {
margin-bottom: 10px;
}
.panel-title h4 {
margin: 0 0 4px;
color: var(--student-text);
font-size: var(--student-fs-h2);
font-weight: 700;
}
.panel-title p {
margin: 0;
color: var(--student-text-secondary);
font-size: var(--student-fs-body-sm);
line-height: 1.6;
}
.table-wrap {
overflow-x: auto;
border: 1px solid var(--student-panel-border);
border-radius: 8px;
}
.table-wrap table {
width: 100%;
min-width: 760px;
border-collapse: collapse;
}
.table-wrap th,
.table-wrap td {
padding: 10px 12px;
border-right: 1px solid var(--student-panel-border);
border-bottom: 1px solid var(--student-panel-border);
vertical-align: top;
}
.table-wrap thead th {
color: var(--student-text-secondary);
background: #f9fafb;
font-size: var(--student-fs-body-sm);
font-weight: 700;
text-align: left;
}
.table-wrap tbody th {
width: 18%;
color: var(--student-text);
font-size: var(--student-fs-body-sm);
font-weight: 700;
}
.table-wrap td {
color: var(--student-text);
font-size: var(--student-fs-body);
line-height: 1.6;
}
.table-wrap tr:last-child th,
.table-wrap tr:last-child td {
border-bottom: 0;
}
.table-wrap th:last-child,
.table-wrap td:last-child {
border-right: 0;
}
/* ============================================================
* 底部导航
* ============================================================ */
.step-navigation {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 14px;
padding: 12px 16px;
border: 1px solid var(--student-panel-border);
border-radius: 10px;
background: #ffffff;
}
.training-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
margin-top: 20px;
}
.training-actions :deep(.training-action) {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 120px;
height: 40px;
padding: 0 20px;
border: 1px solid rgba(37, 99, 235, 0.35);
border-radius: 10px;
color: var(--student-accent);
background: #ffffff;
font-size: var(--student-fs-h2);
font-weight: 600;
cursor: pointer;
transition: all 0.16s ease;
}
.training-actions :deep(.training-action--submit) {
border-color: var(--student-accent);
background: var(--student-accent);
color: #ffffff;
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.2), 0 2px 8px rgba(37, 99, 235, 0.15);
}
.training-actions :deep(.training-action--submit:hover) {
background: var(--student-accent-deep);
border-color: var(--student-accent-deep);
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.3), 0 4px 12px rgba(29, 78, 216, 0.22);
}
.training-actions :deep(.training-action--reset) {
border-color: var(--student-panel-border);
background: #f9fafb;
color: var(--student-text-secondary);
}
.training-actions :deep(.training-action--reset:hover) {
border-color: #d1d5db;
background: #f3f4f6;
color: var(--student-text);
}
/* ============================================================
* 响应式
* ============================================================ */
@media (max-width: 1100px) {
.product-standard-page {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.product-standard-page {
padding: 14px;
}
.training-core {
padding: 14px;
border-radius: 10px;
}
.step-indicator {
align-items: stretch;
flex-direction: column;
gap: 8px;
width: 100%;
overflow: visible;
}
.step-tab {
flex: 0 0 auto;
width: 100%;
border: 1px solid var(--student-panel-border);
border-radius: 8px !important;
}
.step-tab:first-child,
.step-tab:last-child {
border-radius: 8px !important;
}
.step-connector {
display: none;
}
.workbench-head,
.workbench-actions,
.step-navigation {
flex-direction: column;
align-items: stretch;
}
.workbench-actions,
.outline-action,
.btn-nav,
.training-actions :deep(.training-action) {
width: 100%;
}
.key-point-grid {
grid-template-columns: 1fr;
}
}
</style>
<style lang="scss">
/* ============================================================
* 非 scoped:步骤导航覆盖(教育蓝主题)
* ============================================================ */
#app .studentLayout section.app-main:has(.product-standard-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 16px !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 12px !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;
flex: 0 0 auto !important;
min-width: 22px !important;
height: 22px !important;
padding: 0 5px !important;
border: none !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;
transition: all 0.16s ease !important;
}
.step-copy {
display: flex !important;
flex-direction: column !important;
gap: 2px !important;
min-width: 0 !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; }
.step-action { color: rgba(255, 255, 255, 0.65) !important; }
&:hover {
background: #1d4ed8 !important;
border-color: #1d4ed8 !important;
}
}
}
}
@media (max-width: 900px) {
#app .studentLayout section.app-main:has(.product-standard-page) .step-indicator {
justify-content: flex-start !important;
gap: 8px !important;
overflow-x: auto !important;
flex-wrap: nowrap !important;
}
#app .studentLayout section.app-main:has(.product-standard-page) .step-indicator .step-tab {
flex: 0 0 140px !important;
max-width: none !important;
border-radius: 8px !important;
border: 1px solid #e5e7eb !important;
}
}
/* ============================================================
* 共用组件换肤:TrainingMaterialButton → 教育蓝
* ============================================================ */
.product-standard-page .training-material-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 14px !important;
border: 1px solid rgba(37, 99, 235, 0.35) !important;
border-radius: 8px !important;
color: #2563eb !important;
background: #ffffff !important;
font-size: 13px !important;
font-weight: 600 !important;
box-shadow: none !important;
transition:
border-color 0.16s ease,
background 0.16s ease,
color 0.16s ease,
box-shadow 0.16s ease !important;
.el-icon {
font-size: 14px !important;
color: inherit !important;
}
&:hover,
&:focus {
border-color: #2563eb !important;
color: #2563eb !important;
background: #eff6ff !important;
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
}
}
/* ============================================================
* 顶部导出按钮(outline-action)→ 教育蓝描边
* ============================================================ */
#app .studentLayout section.app-main:has(.product-standard-page) .outline-action {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 6px !important;
min-height: 34px !important;
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;
font-size: 13px !important;
font-weight: 600 !important;
box-shadow: none !important;
transition:
border-color 0.16s ease,
background 0.16s ease,
color 0.16s ease,
box-shadow 0.16s ease !important;
.el-icon {
font-size: 14px !important;
color: inherit !important;
}
&:hover {
border-color: #2563eb !important;
color: #2563eb !important;
background: #eff6ff !important;
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
transform: translateY(-1px) !important;
}
}
</style>