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