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...

1159 lines
38 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-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(true)
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-training-shell,页面不再重复声明 */
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: block;
}
.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: var(--student-fs-caption);
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 {
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: var(--student-fs-body) !important;
font-weight: 600 !important;
line-height: 1.3 !important;
white-space: nowrap !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: var(--student-fs-body-sm) !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: var(--student-fs-body) !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: var(--student-fs-h2) !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: var(--student-fs-body) !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: var(--student-fs-h2) !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>