|
|
<template>
|
|
|
<div class="architecture-page student-training-shell">
|
|
|
<main class="architecture-core">
|
|
|
<section class="task-header">
|
|
|
<div class="task-head-main">
|
|
|
<h1 class="task-title">{{ taskTitle }}</h1>
|
|
|
<p class="task-brief">
|
|
|
<span class="task-brief-text">{{ taskBackground }}</span>
|
|
|
<button type="button" class="task-brief-toggle" @click="briefOpen = !briefOpen">
|
|
|
{{ briefOpen ? "收起" : "展开目标与要求" }}
|
|
|
</button>
|
|
|
<span v-if="briefOpen" class="task-brief-more">
|
|
|
<b>实训目标</b>
|
|
|
<ul>
|
|
|
<li v-for="goal in taskGoals" :key="goal">{{ goal }}</li>
|
|
|
</ul>
|
|
|
<b>实训要求</b>
|
|
|
<p>{{ taskRequirement }}</p>
|
|
|
</span>
|
|
|
</p>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="task-action-strip">
|
|
|
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="checkAndSaveStepFour"
|
|
|
:on-export="exportOutcome" />
|
|
|
</section>
|
|
|
|
|
|
<section class="architecture-workbench">
|
|
|
<nav class="step-indicator" aria-label="产品架构设计步骤">
|
|
|
<template v-for="(step, index) in trainingSteps" :key="`${step}-${index}`">
|
|
|
<button type="button"
|
|
|
:class="['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]"
|
|
|
@click="switchStep(index + 1)">
|
|
|
<span class="step-marker">{{ String(index + 1).padStart(2, '0') }}</span>
|
|
|
<span class="step-copy"><strong class="step-name">{{ step }}</strong></span>
|
|
|
</button>
|
|
|
<span v-if="index < trainingSteps.length - 1" class="step-connector" aria-hidden="true"></span>
|
|
|
</template>
|
|
|
</nav>
|
|
|
|
|
|
<section v-if="currentStep === 1" class="architecture-step-card">
|
|
|
<div class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step 01</p>
|
|
|
<h3>{{ trainingSteps[0] }}</h3>
|
|
|
</div><span class="step-chip">拖拽排序判题</span>
|
|
|
</div>
|
|
|
<p class="step-desc">智能硬件产品通常采用模块化分层架构,从物理硬件到用户功能分为 4
|
|
|
层。请将卡片拖拽为<strong>从顶层到底层</strong>的正确顺序(顶层在上、底层在下);排序正确后才能进入下一步。</p>
|
|
|
|
|
|
<div class="architecture-workspace">
|
|
|
<div class="order-rail" aria-hidden="true"><span>顶层</span><i></i><span>底层</span></div>
|
|
|
<div class="layer-list" role="list" aria-label="产品架构层级排序">
|
|
|
<article v-for="(layer, index) in layerOrder" :key="layer.id" class="layer-card"
|
|
|
:class="{ dragging: dragLayerId === layer.id }" draggable="true" role="listitem"
|
|
|
@dragstart="onDragStart(layer.id)" @dragend="dragLayerId = ''" @dragover.prevent
|
|
|
@drop="onDrop(layer.id)">
|
|
|
<span class="layer-position">{{ index + 1 }}</span>
|
|
|
<span class="drag-handle" title="拖拽排序">⋮⋮</span>
|
|
|
<div class="layer-copy">
|
|
|
<h4>{{ layer.name }}</h4>
|
|
|
<p>{{ layer.description }}</p>
|
|
|
</div>
|
|
|
<div class="move-actions">
|
|
|
<button type="button" :disabled="index === 0 || checking" title="上移"
|
|
|
@click="moveLayer(index, -1)">↑</button>
|
|
|
<button type="button" :disabled="index === layerOrder.length - 1 || checking"
|
|
|
title="下移" @click="moveLayer(index, 1)">↓</button>
|
|
|
</div>
|
|
|
</article>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<p v-if="checkResult" class="result-message" :class="{ success: checkResult.allCorrect }">{{
|
|
|
checkResult.message ||
|
|
|
(checkResult.allCorrect ? '排序正确,已解锁下一步。' : `当前仅
|
|
|
${checkResult.correctPositionCount}/${checkResult.totalCount}
|
|
|
个位置正确,请继续调整。`) }}</p>
|
|
|
<div class="task-actions">
|
|
|
<button type="button" class="btn-nav" :disabled="checking" @click="shuffleLayers"><el-icon>
|
|
|
<RefreshLeft />
|
|
|
</el-icon>重新打乱</button>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section v-else-if="currentStep === 2" class="architecture-step-card">
|
|
|
<div class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step 02</p>
|
|
|
<h3>{{ trainingSteps[1] }}</h3>
|
|
|
</div><span class="step-chip">操作链路排序</span>
|
|
|
</div>
|
|
|
<p class="step-desc">产品运行时,用户操作产生的信号会经过硬件感知、驱动传递、算法处理和应用功能触发,最终再由硬件产生效果。请将以下动作按一次完整运行链路的正确顺序排列。</p>
|
|
|
|
|
|
<div class="flow-workspace">
|
|
|
<div class="flow-caption"><span>操作发起</span><i></i><span>效果反馈</span></div>
|
|
|
<div class="flow-list" role="list" aria-label="产品运行操作链路排序">
|
|
|
<article v-for="(action, index) in flowOrder" :key="action.id" class="flow-card"
|
|
|
:class="{ dragging: dragFlowId === action.id }" draggable="true" role="listitem"
|
|
|
@dragstart="onFlowDragStart(action.id)" @dragend="dragFlowId = ''" @dragover.prevent
|
|
|
@drop="onFlowDrop(action.id)">
|
|
|
<span class="layer-position">{{ index + 1 }}</span>
|
|
|
<span class="drag-handle" title="拖拽排序">⋮⋮</span>
|
|
|
<div class="layer-copy">
|
|
|
<h4>{{ action.name }}</h4>
|
|
|
<p>{{ action.description }}</p>
|
|
|
</div>
|
|
|
<div class="move-actions">
|
|
|
<button type="button" :disabled="index === 0 || checking" title="上移"
|
|
|
@click="moveFlow(index, -1)">↑</button>
|
|
|
<button type="button" :disabled="index === flowOrder.length - 1 || checking"
|
|
|
title="下移" @click="moveFlow(index, 1)">↓</button>
|
|
|
</div>
|
|
|
</article>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<p v-if="stepTwoCheckResult" class="result-message"
|
|
|
:class="{ success: stepTwoCheckResult.allCorrect }">{{
|
|
|
stepTwoCheckResult.message || (stepTwoCheckResult.allCorrect ? '排序正确,已解锁下一步。' : `当前仅
|
|
|
${stepTwoCheckResult.correctPositionCount}/${stepTwoCheckResult.totalCount} 个位置正确,请继续调整。`) }}
|
|
|
</p>
|
|
|
<div class="task-actions">
|
|
|
<button type="button" class="btn-nav" :disabled="checking" @click="shuffleFlow"><el-icon>
|
|
|
<RefreshLeft />
|
|
|
</el-icon>重新打乱</button>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section v-else-if="currentStep === 3" class="architecture-step-card">
|
|
|
<div class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step 03</p>
|
|
|
<h3>{{ trainingSteps[2] }}</h3>
|
|
|
</div><span class="step-chip">场景链路分析</span>
|
|
|
</div>
|
|
|
<p class="step-desc">根据本实训案例资料,至少选择 3 个典型的工作场景,分析产品架构各层级在场景中的具体工作。每一行需完整填写,系统会将这些内容作为后续架构方案设计的依据。
|
|
|
</p>
|
|
|
|
|
|
<div class="scene-toolbar">
|
|
|
<span>已完成 {{ completedScenarioCount }} 个场景 <em>(至少 3 个,空白行不参与校验)</em></span>
|
|
|
<div class="scene-toolbar__actions">
|
|
|
<button type="button" class="btn-nav" :disabled="scenarioRows.length >= 10 || checking"
|
|
|
@click="addScenarioRow">增加一行</button>
|
|
|
<button type="button" class="btn-nav" :disabled="scenarioRows.length <= 3 || checking"
|
|
|
@click="removeScenarioRow">删除一行</button>
|
|
|
<button type="button" class="btn-nav" @click="exportOutcome"><el-icon>
|
|
|
<Download />
|
|
|
</el-icon>导出</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="scenario-table-wrap">
|
|
|
<table class="scenario-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>序号</th>
|
|
|
<th>用户场景</th>
|
|
|
<th>硬件物理层<br><small>(感知什么)</small></th>
|
|
|
<th>驱动感知层<br><small>(传递什么)</small></th>
|
|
|
<th>算法处理层<br><small>(处理什么)</small></th>
|
|
|
<th>应用功能层<br><small>(触发什么)</small></th>
|
|
|
<th>驱动感知层<br><small>(输出什么)</small></th>
|
|
|
<th>硬件物理层<br><small>(产生什么效果)</small></th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="(row, index) in scenarioRows" :key="row.id">
|
|
|
<td class="scenario-index">{{ index + 1 }}</td>
|
|
|
<td><el-input v-model="row.userScenario" type="textarea"
|
|
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
|
|
:placeholder="index === 0 ? '例如:用户说「小美,开灯」' : '填写典型用户场景'" /></td>
|
|
|
<td><el-input v-model="row.hardwareSense" type="textarea"
|
|
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
|
|
:placeholder="index === 0 ? '例如:麦克风阵列采集到声音信号' : '硬件感知到什么'" /></td>
|
|
|
<td><el-input v-model="row.driverTransmit" type="textarea"
|
|
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
|
|
:placeholder="index === 0 ? '例如:音频采集驱动将声音转为数字信号' : '驱动层传递什么'" /></td>
|
|
|
<td><el-input v-model="row.algorithmProcess" type="textarea"
|
|
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
|
|
:placeholder="index === 0 ? '例如:唤醒词检测与命令词匹配' : '算法层处理什么'" /></td>
|
|
|
<td><el-input v-model="row.applicationTrigger" type="textarea"
|
|
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
|
|
:placeholder="index === 0 ? '例如:触发语音开关灯功能' : '应用层触发什么'" /></td>
|
|
|
<td><el-input v-model="row.driverOutput" type="textarea"
|
|
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
|
|
:placeholder="index === 0 ? '例如:LED 调光驱动输出 PWM 信号' : '驱动层输出什么'" /></td>
|
|
|
<td><el-input v-model="row.hardwareEffect" type="textarea"
|
|
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
|
|
:placeholder="index === 0 ? '例如:灯珠点亮并输出亮度和色温' : '硬件产生什么效果'" /></td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<p v-if="stepThreeCheckResult" class="result-message"
|
|
|
:class="{ success: stepThreeCheckResult.valid }">{{
|
|
|
stepThreeCheckResult.message }}</p>
|
|
|
</section>
|
|
|
|
|
|
<section v-else-if="currentStep === 4" class="architecture-step-card">
|
|
|
<div class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step 04</p>
|
|
|
<h3>{{ trainingSteps[3] }}</h3>
|
|
|
</div><span class="step-chip">核心功能架构图</span>
|
|
|
</div>
|
|
|
<p class="step-desc">根据本实训案例资料,选取3个核心功能,画出这3个功能在四层架构中的逻辑调用关系图。要求画清楚应用功能层、算法处理层、驱动感知层、硬件物理层;每个功能从应用层出发,向下标注所调用的算法模块-驱动模块-硬件单元;用箭头标注信号流向。</p>
|
|
|
<section class="architecture-image-grid">
|
|
|
<article v-for="(image, index) in architectureImages" :key="image.key"
|
|
|
class="architecture-image-card"
|
|
|
:class="{ uploading: uploadingArchitectureImage === index }">
|
|
|
<div class="architecture-image-card__head"><span>架构图 {{ index + 1 }}</span><small>{{
|
|
|
image.url ? '已上传' :
|
|
|
'待上传' }}</small></div>
|
|
|
<label class="architecture-image-trigger">
|
|
|
<input type="file" accept="image/png,image/jpeg,image/webp,image/gif"
|
|
|
:disabled="uploadingArchitectureImage !== null || checking"
|
|
|
@change="uploadArchitectureImage($event, index)" />
|
|
|
<el-icon>
|
|
|
<Upload />
|
|
|
</el-icon>
|
|
|
<strong>{{ uploadingArchitectureImage === index ? '上传中…' : image.url ? '替换架构图' : `上传架构图
|
|
|
${index + 1}`
|
|
|
}}</strong>
|
|
|
<span>PNG、JPG、WEBP 或 GIF</span>
|
|
|
</label>
|
|
|
<figure v-if="image.url" class="architecture-image-preview">
|
|
|
<el-image :src="image.url" :preview-src-list="[image.url]" fit="cover"
|
|
|
preview-teleported />
|
|
|
<figcaption><span>{{ image.name || `架构图 ${index + 1}` }}</span><button type="button"
|
|
|
@click="clearArchitectureImage(index)">移除</button></figcaption>
|
|
|
</figure>
|
|
|
</article>
|
|
|
</section>
|
|
|
<p v-if="stepFourCheckResult" class="result-message"
|
|
|
:class="{ success: stepFourCheckResult.valid }">{{
|
|
|
stepFourCheckResult.message }}</p>
|
|
|
</section>
|
|
|
<section v-else class="architecture-step-card placeholder-card">
|
|
|
<p class="section-eyebrow">Step {{ String(currentStep).padStart(2, '0') }}</p>
|
|
|
<h3>{{ trainingSteps[currentStep - 1] }}</h3>
|
|
|
</section>
|
|
|
|
|
|
<div class="step-navigation">
|
|
|
<div class="step-nav-left">
|
|
|
<button v-if="currentStep > 1" type="button" class="btn-nav btn-ghost" :disabled="saving"
|
|
|
@click="previousStep">
|
|
|
<el-icon>
|
|
|
<ArrowLeft />
|
|
|
</el-icon>
|
|
|
上一步
|
|
|
</button>
|
|
|
<button type="button" class="btn-nav btn-save" :disabled="saving" @click="saveCurrentProgress">
|
|
|
<el-icon>
|
|
|
<CircleCheck />
|
|
|
</el-icon>
|
|
|
保存
|
|
|
</button>
|
|
|
</div>
|
|
|
<div class="step-nav-right">
|
|
|
<span class="step-progress">Step {{ String(currentStep).padStart(2, "0") }} / {{
|
|
|
String(trainingSteps.length).padStart(2, "0") }}</span>
|
|
|
<button v-if="currentStep < trainingSteps.length" type="button" class="btn-nav btn-primary"
|
|
|
:disabled="saving" @click="nextStep">
|
|
|
下一步
|
|
|
<el-icon>
|
|
|
<ArrowRight />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
</main>
|
|
|
|
|
|
<TrainingAiSidebar study-tip="先识别物理硬件、驱动感知、算法处理和应用功能各自职责,再从顶层到底层排序(顶层在上)。" reference-title="分层判断提示"
|
|
|
reference-text="· 硬件物理层:实体元器件与电路基础<br />· 驱动感知层:信号采集、转换与驱动<br />· 算法处理层:数据处理、识别与决策<br />· 应用功能层:用户可直接感知和操作的功能"
|
|
|
:progress-items="progressItems" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, Download, Operation, RefreshLeft, Upload } from "@element-plus/icons-vue"
|
|
|
import { checkProductArchitectureDesignStepOne, checkProductArchitectureDesignStepTwo, checkProductArchitectureDesignStepThree, checkProductArchitectureDesignStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask"
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap"
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"
|
|
|
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"
|
|
|
import { SCENARIO_FIELDS, getCompletedScenarioCount, getRowsForScenarioValidation, getScenarioValidationMessage } from "./productArchitectureScenarioValidation.mjs"
|
|
|
|
|
|
const { proxy } = getCurrentInstance()
|
|
|
const TASK_KEY = "product-structure"
|
|
|
const DEFAULT_STEPS = ["智能硬件分层架构", "产品运行操作链路", "产品场景架构分析", "架构评审与优化"]
|
|
|
const LAYERS = [
|
|
|
{ id: "hardware-physical", name: "硬件物理层", description: "产品的物理基础,包含电源模块、主控芯片、传感器、LED灯珠等实体元器件。" },
|
|
|
{ id: "driver-sensing", name: "驱动感知层", description: "连接硬件与算法的中间层,负责信号采集、转换与输出驱动。" },
|
|
|
{ id: "algorithm-processing", name: "算法处理层", description: "数据处理与决策的核心层,负责语音识别、信号分析、控制策略等算法逻辑。" },
|
|
|
{ id: "application-function", name: "应用功能层", description: "面向用户的功能呈现层,是用户能直接感知和操作的功能集合。" },
|
|
|
]
|
|
|
const FLOW_ACTIONS = [
|
|
|
{ id: "user-action", name: "用户做了一个操作", description: "用户在产品或应用中发起一次操作。" },
|
|
|
{ id: "hardware-senses", name: "硬件感知到信号", description: "传感器或硬件模块捕获用户操作形成的输入信号。" },
|
|
|
{ id: "driver-transmits", name: "驱动层传递信号", description: "驱动与感知层完成信号采集、转换并向上层传递。" },
|
|
|
{ id: "algorithm-processes", name: "算法层处理数据", description: "算法层对输入数据进行识别、分析和决策。" },
|
|
|
{ id: "application-triggers", name: "应用层触发功能", description: "应用功能层依据算法结果触发对应的产品功能。" },
|
|
|
{ id: "driver-outputs", name: "驱动层输出指令", description: "驱动层将功能指令转化为可执行的控制输出。" },
|
|
|
{ id: "hardware-effects", name: "硬件产生效果", description: "硬件执行指令并形成用户可感知的最终效果。" },
|
|
|
]
|
|
|
const fallbackTask = {
|
|
|
title: "产品架构设计", background: "智能硬件产品采用模块化分层架构。通过本实训理解各层职责和层间关系,为后续功能与技术方案设计建立基础。",
|
|
|
goals: ["理解智能硬件的四层模块化架构", "区分各层的核心职责", "建立从物理硬件到用户功能的系统化思维"],
|
|
|
requirement: "将四层架构按「从顶层到底层」排序(顶层在上);判题通过后方可进入下一步。",
|
|
|
}
|
|
|
|
|
|
const taskConfig = ref(null)
|
|
|
const briefOpen = ref(true)
|
|
|
const currentStep = ref(1)
|
|
|
const layerOrder = ref(shuffle(duplicateLayers()))
|
|
|
const dragLayerId = ref("")
|
|
|
const checking = ref(false)
|
|
|
const stepOnePassed = ref(false)
|
|
|
const checkResult = ref(null)
|
|
|
const flowOrder = ref(shuffle(duplicateFlowActions()))
|
|
|
const dragFlowId = ref("")
|
|
|
const stepTwoPassed = ref(false)
|
|
|
const stepTwoCheckResult = ref(null)
|
|
|
const scenarioRows = ref(createScenarioRows(3))
|
|
|
const stepThreePassed = ref(false)
|
|
|
const stepThreeCheckResult = ref(null)
|
|
|
const architectureImages = ref(createArchitectureImages())
|
|
|
const uploadingArchitectureImage = ref(null)
|
|
|
const stepFourPassed = ref(false)
|
|
|
const stepFourCheckResult = ref(null)
|
|
|
const taskTitle = computed(() => taskConfig.value?.taskName || fallbackTask.title)
|
|
|
const taskBackground = computed(() => taskConfig.value?.background || fallbackTask.background)
|
|
|
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives, fallbackTask.goals))
|
|
|
const taskRequirement = computed(() => taskConfig.value?.requirements || fallbackTask.requirement)
|
|
|
const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, DEFAULT_STEPS))
|
|
|
const progressItems = computed(() => trainingSteps.value.map((text, index) => ({ text, status: index + 1 < currentStep.value ? "done" : index + 1 === currentStep.value ? "doing" : "" })))
|
|
|
const completedScenarioCount = computed(() => getCompletedScenarioCount(scenarioRows.value))
|
|
|
const saving = computed(() => checking.value || uploadingArchitectureImage.value !== null)
|
|
|
|
|
|
onMounted(async () => { await loadTaskConfig(); await loadSavedProgress() })
|
|
|
|
|
|
function duplicateLayers() { return LAYERS.map((item) => ({ ...item })) }
|
|
|
function duplicateFlowActions() { return FLOW_ACTIONS.map((item) => ({ ...item })) }
|
|
|
function createScenarioRow() { return { id: `scenario-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, userScenario: "", hardwareSense: "", driverTransmit: "", algorithmProcess: "", applicationTrigger: "", driverOutput: "", hardwareEffect: "" } }
|
|
|
function createScenarioRows(total) { return Array.from({ length: total }, () => createScenarioRow()) }
|
|
|
function createArchitectureImages() { return [1, 2, 3].map((index) => ({ key: `architecture-${index}`, url: "", name: "" })) }
|
|
|
function shuffle(items) {
|
|
|
const result = [...items]
|
|
|
for (let index = result.length - 1; index > 0; index -= 1) { const target = Math.floor(Math.random() * (index + 1));[result[index], result[target]] = [result[target], result[index]] }
|
|
|
return result
|
|
|
}
|
|
|
function normalizeOrder(ids) {
|
|
|
if (!Array.isArray(ids) || ids.length !== LAYERS.length || new Set(ids).size !== LAYERS.length) return null
|
|
|
const found = Object.fromEntries(LAYERS.map((item) => [item.id, item]))
|
|
|
return ids.every((id) => found[id]) ? ids.map((id) => ({ ...found[id] })) : null
|
|
|
}
|
|
|
function normalizeFlowOrder(ids) {
|
|
|
if (!Array.isArray(ids) || ids.length !== FLOW_ACTIONS.length || new Set(ids).size !== FLOW_ACTIONS.length) return null
|
|
|
const found = Object.fromEntries(FLOW_ACTIONS.map((item) => [item.id, item]))
|
|
|
return ids.every((id) => found[id]) ? ids.map((id) => ({ ...found[id] })) : null
|
|
|
}
|
|
|
async function loadTaskConfig() { try { taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || null } catch (_) { taskConfig.value = null } }
|
|
|
async function loadSavedProgress() {
|
|
|
try {
|
|
|
const answer = (await getStudentTrainingAnswer(TASK_KEY))?.data
|
|
|
const saved = parseSavedAnswer(answer?.step1Answer)
|
|
|
const stepTwoSaved = parseSavedAnswer(answer?.step2Answer)
|
|
|
const stepThreeSaved = parseSavedAnswer(answer?.step3Answer)
|
|
|
const stepFourSaved = parseSavedAnswer(answer?.step4Answer)
|
|
|
const order = normalizeOrder(saved?.layerIds)
|
|
|
if (order) layerOrder.value = order
|
|
|
stepOnePassed.value = Boolean(saved?.passed)
|
|
|
checkResult.value = saved?.checkResult || null
|
|
|
const flow = normalizeFlowOrder(stepTwoSaved?.actionIds)
|
|
|
if (flow) flowOrder.value = flow
|
|
|
stepTwoPassed.value = Boolean(stepTwoSaved?.passed)
|
|
|
stepTwoCheckResult.value = stepTwoSaved?.checkResult || null
|
|
|
const scenarios = normalizeScenarioRows(stepThreeSaved?.rows)
|
|
|
if (scenarios) scenarioRows.value = scenarios
|
|
|
stepThreePassed.value = Boolean(stepThreeSaved?.passed)
|
|
|
stepThreeCheckResult.value = stepThreeSaved?.checkResult || null
|
|
|
const images = normalizeArchitectureImages(stepFourSaved?.images)
|
|
|
if (images) architectureImages.value = images
|
|
|
stepFourPassed.value = Boolean(stepFourSaved?.passed)
|
|
|
stepFourCheckResult.value = stepFourSaved?.checkResult || null
|
|
|
const savedStep = Number(answer?.currentStep || saved?.currentStep)
|
|
|
if (savedStep > 1) currentStep.value = Math.min(savedStep, trainingSteps.value.length)
|
|
|
} catch (_) { /* Keep the randomized local state when no saved answer exists. */ }
|
|
|
}
|
|
|
function parseSavedAnswer(raw) { if (!raw) return null; try { return typeof raw === "string" ? JSON.parse(raw) : raw } catch (_) { return null } }
|
|
|
function normalizeScenarioRows(rows) {
|
|
|
if (!Array.isArray(rows) || rows.length < 3 || rows.length > 10) return null
|
|
|
return rows.map((row) => ({ id: row?.id || createScenarioRow().id, ...SCENARIO_FIELDS.reduce((result, field) => ({ ...result, [field]: String(row?.[field] || "") }), {}) }))
|
|
|
}
|
|
|
function normalizeArchitectureImages(images) {
|
|
|
if (!Array.isArray(images) || images.length !== 3) return null
|
|
|
return images.map((image, index) => ({ key: `architecture-${index + 1}`, url: String(image?.url || ""), name: String(image?.name || "") }))
|
|
|
}
|
|
|
function buildStepOneAnswer() { return { layerIds: layerOrder.value.map((item) => item.id), passed: stepOnePassed.value, checkResult: checkResult.value, updatedAt: new Date().toISOString() } }
|
|
|
function buildStepTwoAnswer() { return { actionIds: flowOrder.value.map((item) => item.id), passed: stepTwoPassed.value, checkResult: stepTwoCheckResult.value, updatedAt: new Date().toISOString() } }
|
|
|
function buildStepThreeAnswer() { return { rows: scenarioRows.value.map((row) => ({ ...row })), passed: stepThreePassed.value, checkResult: stepThreeCheckResult.value, updatedAt: new Date().toISOString() } }
|
|
|
function buildStepFourAnswer() { return { images: architectureImages.value.map((image) => ({ ...image })), passed: stepFourPassed.value, checkResult: stepFourCheckResult.value, updatedAt: new Date().toISOString() } }
|
|
|
function buildAnswer() { return { currentStep: currentStep.value, step1: buildStepOneAnswer(), step2: buildStepTwoAnswer(), step3: buildStepThreeAnswer(), step4: buildStepFourAnswer() } }
|
|
|
async function persist(saveAction = "SAVE") { await saveStudentTrainingAnswer(TASK_KEY, { saveAction, currentStep: currentStep.value, step1Answer: JSON.stringify(buildStepOneAnswer()), step2Answer: JSON.stringify(buildStepTwoAnswer()), step3Answer: JSON.stringify(buildStepThreeAnswer()), step4Answer: JSON.stringify(buildStepFourAnswer()), submitted: stepFourPassed.value && saveAction === "SUBMIT" }) }
|
|
|
function onDragStart(id) { if (!checking.value) dragLayerId.value = id }
|
|
|
function onDrop(targetId) {
|
|
|
const fromIndex = layerOrder.value.findIndex((item) => item.id === dragLayerId.value); const toIndex = layerOrder.value.findIndex((item) => item.id === targetId)
|
|
|
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return
|
|
|
const next = [...layerOrder.value]; const [moving] = next.splice(fromIndex, 1); next.splice(toIndex, 0, moving); layerOrder.value = next; checkResult.value = null; stepOnePassed.value = false
|
|
|
}
|
|
|
function moveLayer(index, direction) { const target = index + direction; if (target < 0 || target >= layerOrder.value.length) return; const next = [...layerOrder.value];[next[index], next[target]] = [next[target], next[index]]; layerOrder.value = next; checkResult.value = null; stepOnePassed.value = false }
|
|
|
function shuffleLayers() { layerOrder.value = shuffle(duplicateLayers()); checkResult.value = null; stepOnePassed.value = false }
|
|
|
function onFlowDragStart(id) { if (!checking.value) dragFlowId.value = id }
|
|
|
function onFlowDrop(targetId) {
|
|
|
const fromIndex = flowOrder.value.findIndex((item) => item.id === dragFlowId.value); const toIndex = flowOrder.value.findIndex((item) => item.id === targetId)
|
|
|
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return
|
|
|
const next = [...flowOrder.value]; const [moving] = next.splice(fromIndex, 1); next.splice(toIndex, 0, moving); flowOrder.value = next; stepTwoCheckResult.value = null; stepTwoPassed.value = false
|
|
|
}
|
|
|
function moveFlow(index, direction) { const target = index + direction; if (target < 0 || target >= flowOrder.value.length) return; const next = [...flowOrder.value];[next[index], next[target]] = [next[target], next[index]]; flowOrder.value = next; stepTwoCheckResult.value = null; stepTwoPassed.value = false }
|
|
|
function shuffleFlow() { flowOrder.value = shuffle(duplicateFlowActions()); stepTwoCheckResult.value = null; stepTwoPassed.value = false }
|
|
|
function addScenarioRow() { if (scenarioRows.value.length >= 10) return; scenarioRows.value.push(createScenarioRow()); stepThreeCheckResult.value = null; stepThreePassed.value = false }
|
|
|
function removeScenarioRow() { if (scenarioRows.value.length <= 3) return; scenarioRows.value.pop(); stepThreeCheckResult.value = null; stepThreePassed.value = false }
|
|
|
async function checkAndSave() {
|
|
|
if (checking.value) return
|
|
|
checking.value = true
|
|
|
try {
|
|
|
const response = await checkProductArchitectureDesignStepOne(layerOrder.value.map((item) => item.id))
|
|
|
checkResult.value = response?.data || null; stepOnePassed.value = Boolean(checkResult.value?.allCorrect)
|
|
|
await persist("SAVE")
|
|
|
if (stepOnePassed.value) proxy?.$modal?.msgSuccess("排序正确,已保存并解锁下一步"); else proxy?.$modal?.msgWarning("排序尚未正确,请继续调整")
|
|
|
} catch (error) { proxy?.$modal?.msgError(error?.message || "判题失败,请稍后重试") } finally { checking.value = false }
|
|
|
}
|
|
|
async function checkAndSaveStepTwo() {
|
|
|
if (checking.value) return
|
|
|
checking.value = true
|
|
|
try {
|
|
|
const response = await checkProductArchitectureDesignStepTwo(flowOrder.value.map((item) => item.id))
|
|
|
stepTwoCheckResult.value = response?.data || null; stepTwoPassed.value = Boolean(stepTwoCheckResult.value?.allCorrect)
|
|
|
await persist("SAVE")
|
|
|
if (stepTwoPassed.value) proxy?.$modal?.msgSuccess("排序正确,已保存并解锁下一步"); else proxy?.$modal?.msgWarning("排序尚未正确,请继续调整")
|
|
|
} catch (error) { proxy?.$modal?.msgError(error?.message || "判题失败,请稍后重试") } finally { checking.value = false }
|
|
|
}
|
|
|
async function checkAndSaveStepThree() {
|
|
|
if (checking.value) return
|
|
|
const validationMessage = getScenarioValidationMessage(scenarioRows.value)
|
|
|
if (validationMessage) {
|
|
|
stepThreePassed.value = false
|
|
|
stepThreeCheckResult.value = { valid: false, message: validationMessage }
|
|
|
proxy?.$modal?.msgWarning(validationMessage)
|
|
|
return
|
|
|
}
|
|
|
checking.value = true
|
|
|
try {
|
|
|
const response = await checkProductArchitectureDesignStepThree(getRowsForScenarioValidation(scenarioRows.value).map((row) => ({ ...row })))
|
|
|
stepThreeCheckResult.value = response?.data || null; stepThreePassed.value = Boolean(stepThreeCheckResult.value?.valid)
|
|
|
await persist("SAVE")
|
|
|
if (stepThreePassed.value) proxy?.$modal?.msgSuccess("场景架构分析已保存并解锁下一步"); else proxy?.$modal?.msgWarning("请至少完整填写 3 个典型工作场景")
|
|
|
} catch (error) { proxy?.$modal?.msgError(error?.message || "校验失败,请稍后重试") } finally { checking.value = false }
|
|
|
}
|
|
|
async function uploadArchitectureImage(event, index) {
|
|
|
const file = event.target.files?.[0]
|
|
|
if (!file) return
|
|
|
if (!/^image\/(png|jpeg|webp|gif)$/i.test(file.type)) { proxy?.$modal?.msgError?.("请上传 PNG、JPG、JPEG、WEBP 或 GIF 格式的架构图"); event.target.value = ""; return }
|
|
|
uploadingArchitectureImage.value = index
|
|
|
try {
|
|
|
const formData = new FormData(); formData.append("file", file)
|
|
|
const response = await uploadStudentTrainingFile(formData)
|
|
|
const url = response?.url || response?.fileName || ""
|
|
|
if (!url) throw new Error("上传结果缺少文件地址")
|
|
|
architectureImages.value[index] = { ...architectureImages.value[index], url, name: response?.originalFilename || file.name }
|
|
|
stepFourPassed.value = false; stepFourCheckResult.value = null
|
|
|
await persist("SAVE")
|
|
|
proxy?.$modal?.msgSuccess(`架构图 ${index + 1} 上传成功`)
|
|
|
} catch (error) { proxy?.$modal?.msgError?.(error?.message || "架构图上传失败") } finally { uploadingArchitectureImage.value = null; event.target.value = "" }
|
|
|
}
|
|
|
function clearArchitectureImage(index) { architectureImages.value[index] = { ...architectureImages.value[index], url: "", name: "" }; stepFourPassed.value = false; stepFourCheckResult.value = null }
|
|
|
async function saveStepFour() { if (uploadingArchitectureImage.value !== null) return; await persist("SAVE"); proxy?.$modal?.msgSuccess("第 4 步进度已保存") }
|
|
|
async function checkAndSaveStepFour() {
|
|
|
if (checking.value || uploadingArchitectureImage.value !== null) return
|
|
|
checking.value = true
|
|
|
try {
|
|
|
const response = await checkProductArchitectureDesignStepFour(architectureImages.value.map((image) => image.url))
|
|
|
stepFourCheckResult.value = response?.data || null; stepFourPassed.value = Boolean(stepFourCheckResult.value?.valid)
|
|
|
await persist(stepFourPassed.value ? "SUBMIT" : "SAVE")
|
|
|
if (stepFourPassed.value) proxy?.$modal?.msgSuccess("产品架构设计实训已完成"); else proxy?.$modal?.msgWarning("请上传完整的三张核心功能架构图")
|
|
|
} catch (error) { proxy?.$modal?.msgError(error?.message || "校验失败,请稍后重试") } finally { checking.value = false }
|
|
|
}
|
|
|
function downloadBuiltInFile(url, fileName) {
|
|
|
const link = document.createElement("a")
|
|
|
link.href = url
|
|
|
link.download = fileName
|
|
|
document.body.appendChild(link)
|
|
|
link.click()
|
|
|
document.body.removeChild(link)
|
|
|
}
|
|
|
async function goNextStep() {
|
|
|
if ((currentStep.value === 1 && !stepOnePassed.value) || (currentStep.value === 2 && !stepTwoPassed.value) || (currentStep.value === 3 && !stepThreePassed.value)) return
|
|
|
currentStep.value = Math.min(currentStep.value + 1, trainingSteps.value.length); await persist("SAVE")
|
|
|
}
|
|
|
function saveCurrentProgress() {
|
|
|
if (currentStep.value === 1) return checkAndSave()
|
|
|
if (currentStep.value === 2) return checkAndSaveStepTwo()
|
|
|
if (currentStep.value === 3) return checkAndSaveStepThree()
|
|
|
return saveStepFour()
|
|
|
}
|
|
|
function previousStep() { if (currentStep.value > 1) currentStep.value -= 1 }
|
|
|
async function nextStep() {
|
|
|
if (currentStep.value >= trainingSteps.value.length) return
|
|
|
const gates = {
|
|
|
1: [stepOnePassed, "请先完成第 1 步排序并判题通过"],
|
|
|
2: [stepTwoPassed, "请先完成第 2 步排序并判题通过"],
|
|
|
3: [stepThreePassed, "请先完成第 3 步场景架构分析并校验通过"],
|
|
|
}
|
|
|
const gate = gates[currentStep.value]
|
|
|
if (gate && !gate[0].value) { proxy?.$modal?.msgWarning(gate[1]); return }
|
|
|
await goNextStep()
|
|
|
}
|
|
|
function switchStep(step) {
|
|
|
if (step > 1 && !stepOnePassed.value) { proxy?.$modal?.msgWarning("请先完成第 1 步排序并判题通过"); return }
|
|
|
if (step > 2 && !stepTwoPassed.value) { proxy?.$modal?.msgWarning("请先完成第 2 步排序并判题通过"); return }
|
|
|
if (step > 3 && !stepThreePassed.value) { proxy?.$modal?.msgWarning("请先完成第 3 步场景架构分析并校验通过"); return }
|
|
|
currentStep.value = step
|
|
|
}
|
|
|
function exportOutcome() { const blob = new Blob([JSON.stringify(buildAnswer(), 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 lang="scss" scoped>
|
|
|
/* ===== 页面级令牌(教育蓝极简主题) ===== */
|
|
|
.architecture-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;
|
|
|
/* Element Plus 主色:教育蓝 */
|
|
|
--el-color-primary: #2563eb;
|
|
|
--el-color-primary-light-3: #60a5fa;
|
|
|
--el-color-primary-light-5: #93c5fd;
|
|
|
--el-color-primary-light-7: #bfdbfe;
|
|
|
--el-color-primary-light-8: #dbeafe;
|
|
|
--el-color-primary-light-9: #eff6ff;
|
|
|
--el-color-primary-dark-2: #1d4ed8;
|
|
|
--el-input-hover-border-color: #3b82f6;
|
|
|
--el-input-focus-border-color: #2563eb;
|
|
|
--el-input-focus-ring-color: rgba(59, 130, 246, 0.15);
|
|
|
/* 字号梯度统一继承 .student-training-shell,页面不再重复声明 */
|
|
|
|
|
|
min-width: 0;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.architecture-core {
|
|
|
display: grid;
|
|
|
gap: 12px;
|
|
|
align-content: start;
|
|
|
min-width: 0;
|
|
|
border: 0;
|
|
|
background: transparent;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
/* ===== task-header ===== */
|
|
|
.task-header {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
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);
|
|
|
line-height: 1.4;
|
|
|
font-weight: 700;
|
|
|
letter-spacing: -0.01em;
|
|
|
}
|
|
|
|
|
|
.task-brief {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.65;
|
|
|
max-width: 100%;
|
|
|
}
|
|
|
|
|
|
.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;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
&: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;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.architecture-toolbar {
|
|
|
flex: 0 0 auto;
|
|
|
display: flex;
|
|
|
gap: 8px;
|
|
|
justify-content: flex-end;
|
|
|
max-width: 100%;
|
|
|
}
|
|
|
|
|
|
.outline-action {
|
|
|
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-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: var(--student-fs-h2);
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== workbench ===== */
|
|
|
.architecture-workbench {
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
display: grid;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.architecture-step-card {
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
display: grid;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.step-title-row {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: flex-start;
|
|
|
gap: 12px;
|
|
|
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.4;
|
|
|
}
|
|
|
|
|
|
.step-chip {
|
|
|
flex: 0 0 auto;
|
|
|
padding: 4px 10px;
|
|
|
border: 1px solid #bfdbfe;
|
|
|
border-radius: 999px;
|
|
|
color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 600;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.section-eyebrow {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
color: var(--student-accent);
|
|
|
font-family: var(--student-font-data);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.3;
|
|
|
letter-spacing: 0.1em;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
.step-desc {
|
|
|
margin: 0 0 16px;
|
|
|
color: #4b5563;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.7;
|
|
|
}
|
|
|
|
|
|
/* ===== 排序工作区 ===== */
|
|
|
.architecture-workspace {
|
|
|
display: grid;
|
|
|
grid-template-columns: 58px minmax(0, 1fr);
|
|
|
gap: 18px;
|
|
|
max-width: 950px;
|
|
|
margin: 0 auto;
|
|
|
}
|
|
|
|
|
|
.order-rail {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.1em;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
.order-rail i {
|
|
|
flex: 1;
|
|
|
width: 2px;
|
|
|
margin: 9px 0;
|
|
|
background: var(--student-panel-border);
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
.order-rail i::before,
|
|
|
.order-rail i::after {
|
|
|
content: "";
|
|
|
position: absolute;
|
|
|
left: -4px;
|
|
|
border: 5px solid transparent;
|
|
|
}
|
|
|
|
|
|
.order-rail i::before {
|
|
|
top: -1px;
|
|
|
border-bottom-color: var(--student-panel-border);
|
|
|
}
|
|
|
|
|
|
.order-rail i::after {
|
|
|
bottom: -1px;
|
|
|
border-top-color: var(--student-panel-border);
|
|
|
}
|
|
|
|
|
|
.layer-list {
|
|
|
display: grid;
|
|
|
gap: 14px;
|
|
|
}
|
|
|
|
|
|
.layer-card {
|
|
|
display: grid;
|
|
|
grid-template-columns: 32px 22px minmax(0, 1fr) auto;
|
|
|
gap: 14px;
|
|
|
align-items: center;
|
|
|
padding: 17px 18px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
cursor: grab;
|
|
|
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.layer-card:hover,
|
|
|
.layer-card.dragging {
|
|
|
border-color: var(--student-accent);
|
|
|
box-shadow: 0 10px 22px rgba(37, 99, 235, 0.1);
|
|
|
transform: translateY(-2px);
|
|
|
}
|
|
|
|
|
|
.layer-position {
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
width: 30px;
|
|
|
height: 30px;
|
|
|
border-radius: 6px;
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-accent);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.drag-handle {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: 20px;
|
|
|
letter-spacing: -5px;
|
|
|
transform: rotate(90deg);
|
|
|
}
|
|
|
|
|
|
.layer-copy h4 {
|
|
|
margin: 0 0 6px;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.layer-copy p {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.65;
|
|
|
}
|
|
|
|
|
|
.move-actions {
|
|
|
display: grid;
|
|
|
gap: 6px;
|
|
|
}
|
|
|
|
|
|
.move-actions button {
|
|
|
width: 32px;
|
|
|
min-height: 28px;
|
|
|
padding: 0;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 7px;
|
|
|
background: #ffffff;
|
|
|
color: var(--student-text);
|
|
|
font: inherit;
|
|
|
cursor: pointer;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-accent);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
button:disabled {
|
|
|
opacity: 0.45;
|
|
|
cursor: not-allowed;
|
|
|
}
|
|
|
|
|
|
/* ===== flow workspace ===== */
|
|
|
.flow-workspace {
|
|
|
max-width: 1000px;
|
|
|
margin: 0 auto;
|
|
|
}
|
|
|
|
|
|
.flow-caption {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 12px;
|
|
|
margin: 0 0 14px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.12em;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
.flow-caption i {
|
|
|
flex: 1;
|
|
|
height: 1px;
|
|
|
background: var(--student-panel-border);
|
|
|
}
|
|
|
|
|
|
.flow-list {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
gap: 14px;
|
|
|
}
|
|
|
|
|
|
.flow-card {
|
|
|
display: grid;
|
|
|
grid-template-columns: 32px 22px minmax(0, 1fr) auto;
|
|
|
gap: 14px;
|
|
|
align-items: center;
|
|
|
padding: 17px 18px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
cursor: grab;
|
|
|
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.flow-card:hover,
|
|
|
.flow-card.dragging {
|
|
|
border-color: var(--student-accent);
|
|
|
box-shadow: 0 10px 22px rgba(37, 99, 235, 0.1);
|
|
|
transform: translateY(-2px);
|
|
|
}
|
|
|
|
|
|
/* ===== scene toolbar ===== */
|
|
|
.scene-toolbar {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 16px;
|
|
|
margin-bottom: 14px;
|
|
|
color: var(--student-text);
|
|
|
font-weight: 700;
|
|
|
font-size: var(--student-fs-body);
|
|
|
}
|
|
|
|
|
|
.scene-toolbar em {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-style: normal;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 400;
|
|
|
}
|
|
|
|
|
|
.scene-toolbar__actions {
|
|
|
display: flex;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
/* ===== 表格 ===== */
|
|
|
.scenario-table-wrap {
|
|
|
overflow-x: auto;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
}
|
|
|
|
|
|
.scenario-table {
|
|
|
width: 100%;
|
|
|
min-width: 1400px;
|
|
|
border-collapse: collapse;
|
|
|
table-layout: fixed;
|
|
|
}
|
|
|
|
|
|
.scenario-table th,
|
|
|
.scenario-table td {
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
}
|
|
|
|
|
|
.scenario-table th {
|
|
|
padding: 10px 8px;
|
|
|
color: var(--student-text);
|
|
|
background: #f9fafb;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 700;
|
|
|
text-align: left;
|
|
|
line-height: 1.45;
|
|
|
}
|
|
|
|
|
|
.scenario-table th small {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.scenario-table td {
|
|
|
padding: 6px 8px;
|
|
|
vertical-align: top;
|
|
|
}
|
|
|
|
|
|
.scenario-table th:first-child,
|
|
|
.scenario-table td:first-child {
|
|
|
width: 52px;
|
|
|
}
|
|
|
|
|
|
.scenario-table th:nth-child(2),
|
|
|
.scenario-table td:nth-child(2) {
|
|
|
width: 170px;
|
|
|
}
|
|
|
|
|
|
.scenario-index {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-weight: 700;
|
|
|
text-align: center;
|
|
|
font-family: ui-monospace, "SF Mono", Consolas, monospace;
|
|
|
}
|
|
|
|
|
|
.scenario-table :deep(.el-textarea__inner) {
|
|
|
min-height: 72px !important;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
color: var(--student-text);
|
|
|
background: transparent;
|
|
|
box-shadow: none !important;
|
|
|
padding: 6px 8px;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.55;
|
|
|
resize: vertical;
|
|
|
}
|
|
|
|
|
|
.scenario-table :deep(.el-textarea__inner:focus) {
|
|
|
outline: none;
|
|
|
box-shadow: inset 0 0 0 2px var(--student-accent) !important;
|
|
|
}
|
|
|
|
|
|
.scenario-table :deep(.el-textarea__inner::placeholder) {
|
|
|
color: var(--student-text-secondary);
|
|
|
}
|
|
|
|
|
|
.scenario-table tr:last-child td {
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.scenario-table th:last-child,
|
|
|
.scenario-table td:last-child {
|
|
|
border-right: 0;
|
|
|
}
|
|
|
|
|
|
/* ===== 架构图上传 ===== */
|
|
|
.architecture-image-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
gap: 16px;
|
|
|
}
|
|
|
|
|
|
.architecture-image-card {
|
|
|
min-width: 0;
|
|
|
padding: 16px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.architecture-image-card__head {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
margin-bottom: 13px;
|
|
|
color: var(--student-text);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.architecture-image-card__head small {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.architecture-image-trigger {
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
align-content: center;
|
|
|
gap: 7px;
|
|
|
min-height: 154px;
|
|
|
border: 1px dashed var(--student-accent);
|
|
|
border-radius: 10px;
|
|
|
color: var(--student-text);
|
|
|
background: var(--student-accent-soft);
|
|
|
cursor: pointer;
|
|
|
text-align: center;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: var(--student-accent-deep);
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.architecture-image-trigger input {
|
|
|
position: absolute;
|
|
|
width: 1px;
|
|
|
height: 1px;
|
|
|
overflow: hidden;
|
|
|
opacity: 0;
|
|
|
}
|
|
|
|
|
|
.architecture-image-trigger .el-icon {
|
|
|
font-size: 24px;
|
|
|
color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
.architecture-image-trigger span {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.architecture-image-preview {
|
|
|
margin: 13px 0 0;
|
|
|
}
|
|
|
|
|
|
.architecture-image-preview :deep(.el-image) {
|
|
|
display: block;
|
|
|
width: 100%;
|
|
|
height: 170px;
|
|
|
overflow: hidden;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 8px;
|
|
|
background: #fff;
|
|
|
cursor: zoom-in;
|
|
|
}
|
|
|
|
|
|
.architecture-image-preview figcaption {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
gap: 10px;
|
|
|
align-items: center;
|
|
|
margin-top: 9px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.architecture-image-preview figcaption span {
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.architecture-image-preview button {
|
|
|
border: 1px solid var(--student-accent);
|
|
|
border-radius: 6px;
|
|
|
padding: 4px 12px;
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: background 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.architecture-image-preview button {
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
background: #ffffff;
|
|
|
color: var(--student-text);
|
|
|
}
|
|
|
|
|
|
.architecture-image-preview button:hover {
|
|
|
border-color: var(--student-danger);
|
|
|
color: var(--student-danger);
|
|
|
}
|
|
|
|
|
|
|
|
|
/* ===== result ===== */
|
|
|
.result-message {
|
|
|
margin: 16px 0 0;
|
|
|
padding: 10px 14px;
|
|
|
border: 1px solid #f0c78a;
|
|
|
border-radius: 8px;
|
|
|
color: #92400e;
|
|
|
background: rgba(245, 124, 0, 0.08);
|
|
|
font-size: var(--student-fs-body);
|
|
|
}
|
|
|
|
|
|
.result-message.success {
|
|
|
border-color: #a7f3d0;
|
|
|
color: #166534;
|
|
|
background: rgba(43, 164, 113, 0.1);
|
|
|
}
|
|
|
|
|
|
/* ===== 按钮 ===== */
|
|
|
.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-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: var(--student-fs-h2);
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
|
|
|
&:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.45;
|
|
|
transform: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.task-actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
justify-content: flex-end;
|
|
|
margin-top: 18px;
|
|
|
}
|
|
|
|
|
|
/* ===== placeholder ===== */
|
|
|
.placeholder-card {
|
|
|
min-height: 200px;
|
|
|
padding: 40px 20px;
|
|
|
text-align: center;
|
|
|
border: 1px dashed var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
background: var(--student-panel-bg);
|
|
|
}
|
|
|
|
|
|
.placeholder-card h3 {
|
|
|
margin: 8px 0 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h2);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
/* ===== 响应式 ===== */
|
|
|
@media (max-width: 1000px) {
|
|
|
.architecture-image-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 840px) {
|
|
|
.flow-list {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.scene-toolbar {
|
|
|
align-items: flex-start;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.scene-toolbar__actions {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.scene-toolbar__actions .btn-nav {
|
|
|
flex: 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
.architecture-page {
|
|
|
padding: 12px;
|
|
|
}
|
|
|
|
|
|
.architecture-workspace {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.order-rail {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.layer-card {
|
|
|
grid-template-columns: 30px 17px minmax(0, 1fr);
|
|
|
}
|
|
|
|
|
|
.move-actions {
|
|
|
grid-column: 3;
|
|
|
grid-template-columns: repeat(2, 32px);
|
|
|
justify-content: flex-end;
|
|
|
}
|
|
|
|
|
|
.flow-card {
|
|
|
grid-template-columns: 30px 17px minmax(0, 1fr);
|
|
|
}
|
|
|
|
|
|
.flow-card .move-actions {
|
|
|
grid-column: 3;
|
|
|
grid-template-columns: repeat(2, 32px);
|
|
|
}
|
|
|
|
|
|
.task-header {
|
|
|
flex-direction: column;
|
|
|
align-items: flex-start;
|
|
|
}
|
|
|
|
|
|
.architecture-toolbar {
|
|
|
width: 100%;
|
|
|
justify-content: flex-start;
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.step-navigation {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.btn-nav {
|
|
|
width: 100%;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.task-action-strip {
|
|
|
margin: 0;
|
|
|
padding: 10px 16px;
|
|
|
}
|
|
|
|
|
|
.task-action-strip :deep(.training-task-toolbar) {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
justify-content: flex-end;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
/* ===== 底部步骤导航按钮 ===== */
|
|
|
.step-navigation {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 10px;
|
|
|
margin-top: 16px;
|
|
|
padding: 12px 16px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
flex-wrap: nowrap;
|
|
|
}
|
|
|
|
|
|
.step-nav-right {
|
|
|
margin-left: auto;
|
|
|
}
|
|
|
|
|
|
.btn-primary {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid var(--student-accent);
|
|
|
border-radius: 8px;
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.2), 0 2px 8px rgba(37, 99, 235, 0.15);
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent-deep);
|
|
|
background: var(--student-accent-deep);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.3), 0 4px 12px rgba(29, 78, 216, 0.22);
|
|
|
}
|
|
|
|
|
|
&:active:not(:disabled) {
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-ghost {
|
|
|
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-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-save {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid #e5e7eb;
|
|
|
border-radius: 8px;
|
|
|
color: #4b5563;
|
|
|
background: #f9fafb;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: #d1d5db;
|
|
|
background: #f3f4f6;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-primary:disabled,
|
|
|
.btn-ghost:disabled,
|
|
|
.btn-save:disabled,
|
|
|
.btn-nav:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
transform: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.step-progress {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-family: var(--student-font-data);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.05em;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<style lang="scss">
|
|
|
/* ============================================================
|
|
|
* 步骤导航:分段连接胶囊(教育蓝主题)
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.architecture-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 20px !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 14px !important;
|
|
|
border: 1px solid #e5e7eb !important;
|
|
|
border-right: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-size: 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-bar {
|
|
|
display: none !important;
|
|
|
}
|
|
|
|
|
|
.step-no {
|
|
|
position: static !important;
|
|
|
flex: 0 0 auto !important;
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
min-width: 22px !important;
|
|
|
height: 22px !important;
|
|
|
padding: 0 5px !important;
|
|
|
border-radius: 6px !important;
|
|
|
background: #f3f4f6 !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-family: var(--student-font-data), ui-monospace, "SF Mono", Consolas, monospace !important;
|
|
|
font-size: var(--student-fs-body-sm) !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: all 0.16s ease !important;
|
|
|
|
|
|
&::after {
|
|
|
display: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.step-name {
|
|
|
color: inherit !important;
|
|
|
}
|
|
|
|
|
|
&:hover:not(.active):not(.complete) {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
color: #2563eb !important;
|
|
|
z-index: 1 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.complete:not(.active) {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #374151 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: rgba(43, 164, 113, 0.12) !important;
|
|
|
color: #16915f !important;
|
|
|
}
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #2ba471 !important;
|
|
|
background: #f0fdf6 !important;
|
|
|
z-index: 1 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: #2ba471 !important;
|
|
|
color: #ffffff !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.active {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
font-weight: 700 !important;
|
|
|
z-index: 2 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: #ffffff !important;
|
|
|
color: #2563eb !important;
|
|
|
}
|
|
|
|
|
|
&:hover {
|
|
|
background: #1d4ed8 !important;
|
|
|
border-color: #1d4ed8 !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
#app .studentLayout section.app-main:has(.architecture-page) .step-indicator {
|
|
|
justify-content: flex-start !important;
|
|
|
overflow-x: auto !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.architecture-page) .step-indicator .step-tab {
|
|
|
flex: 0 0 140px !important;
|
|
|
max-width: none !important;
|
|
|
}
|
|
|
}
|
|
|
</style>
|