|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="architecture-page student-training-shell">
|
|
|
|
|
|
<main class="training-core">
|
|
|
|
|
|
<section class="task-header">
|
|
|
|
|
|
<div class="task-badge"><el-icon><Operation /></el-icon>产品规划与设计</div>
|
|
|
|
|
|
<h1 class="task-title">{{ taskTitle }}</h1>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" :show-toolbar="false" />
|
|
|
|
|
|
|
|
|
|
|
|
<section class="task-card">
|
|
|
|
|
|
<div class="workbench-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="section-eyebrow">Smart hardware architecture</p>
|
|
|
|
|
|
<h3>{{ trainingSteps[currentStep - 1] || '产品架构设计' }}</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<TrainingTaskToolbar class="architecture-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<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>{{ step }}</strong><small>{{ currentStep === index + 1 ? '当前步骤' : '点击切换' }} →</small></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span v-if="index < trainingSteps.length - 1" class="step-connector" aria-hidden="true"></span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-if="currentStep === 1" class="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 btn-submit" :disabled="checking" @click="checkAndSave">{{ checking ? '判题中…' : '检查排序并保存' }}</button>
|
|
|
|
|
|
<button type="button" class="btn-nav" :disabled="checking" @click="shuffleLayers"><el-icon><RefreshLeft /></el-icon>重新打乱</button>
|
|
|
|
|
|
<button type="button" class="btn-nav btn-primary" :disabled="!stepOnePassed" @click="goNextStep">进入第 2 步<el-icon><ArrowRight /></el-icon></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-else-if="currentStep === 2" class="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 btn-submit" :disabled="checking" @click="checkAndSaveStepTwo">{{ checking ? '判题中…' : '检查排序并保存' }}</button>
|
|
|
|
|
|
<button type="button" class="btn-nav" :disabled="checking" @click="shuffleFlow"><el-icon><RefreshLeft /></el-icon>重新打乱</button>
|
|
|
|
|
|
<button type="button" class="btn-nav btn-primary" :disabled="!stepTwoPassed" @click="goNextStep">进入第 3 步<el-icon><ArrowRight /></el-icon></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-else-if="currentStep === 3" class="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>
|
|
|
|
|
|
<div class="task-actions">
|
|
|
|
|
|
<button type="button" class="btn-nav btn-submit" :disabled="checking" @click="checkAndSaveStepThree">{{ checking ? '校验中…' : '校验并保存' }}</button>
|
|
|
|
|
|
<button type="button" class="btn-nav btn-primary" :disabled="!stepThreePassed" @click="goNextStep">进入第 4 步<el-icon><ArrowRight /></el-icon></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-else-if="currentStep === 4" class="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 个核心功能绘制四层架构的逻辑调用关系图。每张图应从应用功能层向下标注算法、驱动及硬件单元,并以箭头标注信号流向。</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>
|
|
|
|
|
|
<section class="architecture-guide"><h4>参考工具及方案</h4><ol><li>下载模板,按应用功能层、算法处理层、驱动感知层、硬件物理层补全逻辑调用关系。</li><li>每张图围绕一个核心功能绘制,使用箭头清晰标注信号流向。</li></ol><button type="button" @click="downloadArchitectureTemplate">下载模板</button></section>
|
|
|
|
|
|
<p v-if="stepFourCheckResult" class="result-message" :class="{ success: stepFourCheckResult.valid }">{{ stepFourCheckResult.message }}</p>
|
|
|
|
|
|
<div class="task-actions"><button type="button" class="btn-nav" :disabled="uploadingArchitectureImage !== null || checking" @click="saveStepFour">保存进度</button><button type="button" class="btn-nav btn-submit" :disabled="uploadingArchitectureImage !== null || checking" @click="checkAndSaveStepFour">{{ checking ? '校验中…' : '校验并完成实训' }}</button></div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
<section v-else class="step-card placeholder-card"><p class="section-eyebrow">Step {{ String(currentStep).padStart(2, '0') }}</p><h3>{{ trainingSteps[currentStep - 1] }}</h3></section>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<TrainingAiSidebar study-tip="先识别物理硬件、驱动感知、算法处理和应用功能各自职责,再从底层到顶层排序。" reference-title="分层判断提示" reference-text="· 硬件物理层:实体元器件与电路基础<br />· 驱动感知层:信号采集、转换与驱动<br />· 算法处理层:数据处理、识别与决策<br />· 应用功能层:用户可直接感知和操作的功能" :progress-items="progressItems" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ArrowLeft, ArrowRight, 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 TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
|
|
|
|
|
|
import { SCENARIO_FIELDS, getCompletedScenarioCount, getRowsForScenarioValidation, getScenarioValidationMessage } from "./productArchitectureScenarioValidation.mjs";
|
|
|
|
|
|
|
|
|
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
|
|
|
const TASK_KEY = "product-structure";
|
|
|
|
|
|
const PRODUCT_ARCHITECTURE_TEMPLATE_URL = "/file/training-materials/product-architecture/13.产品架构图绘制模版.pptx";
|
|
|
|
|
|
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 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));
|
|
|
|
|
|
|
|
|
|
|
|
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 downloadArchitectureTemplate() {
|
|
|
|
|
|
downloadBuiltInFile(PRODUCT_ARCHITECTURE_TEMPLATE_URL, "13.产品架构图绘制模版.pptx");
|
|
|
|
|
|
}
|
|
|
|
|
|
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 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 scoped lang="scss">
|
|
|
|
|
|
.architecture-page{display:grid;grid-template-columns:minmax(720px,1fr) 340px;gap:24px;min-height:calc(100vh - 76px);padding:24px;color:#eef5ff;background:#06111d}.training-core{min-width:0;padding:28px;border:1px solid rgba(0,180,255,.25);border-radius:36px;background:rgba(8,18,28,.6);box-shadow:inset 0 1px 0 rgba(169,229,255,.08),0 22px 48px rgba(0,0,0,.22)}.task-header{margin-bottom:24px}.task-badge,.step-chip{display:inline-flex;align-items:center;gap:8px;border:1px solid rgba(99,217,255,.55);border-radius:999px;color:#dff5ff;background:rgba(17,126,178,.36);font-size:12px;font-weight:700}.task-badge{min-height:28px;padding:4px 14px}.task-title{margin:12px 0 0;color:#fff;font-size:30px;font-weight:900}.task-card,.step-card{border:1px solid rgba(82,174,226,.42);border-radius:26px;background:rgba(0,25,42,.64)}.task-card{margin-top:28px;padding:18px 22px 22px}.workbench-head{display:flex;justify-content:space-between;gap:18px;margin-bottom:22px;padding-bottom:18px;border-bottom:1px solid rgba(0,180,255,.2)}.section-eyebrow{margin:0;color:#71dfff;font-size:12px;font-weight:800}.workbench-head h3,.step-title-row h3,.placeholder-card h3{margin:5px 0 0;color:#fff;font-size:24px}.workbench-actions,.task-actions{display:flex;flex-wrap:wrap;align-items:center;gap:10px}.workbench-actions{justify-content:flex-end}.outline-action,.btn-nav,.move-actions button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:0 18px;border:1px solid rgba(99,217,255,.55);border-radius:999px;color:#e9fbff;background:rgba(17,126,178,.36);font:inherit;font-size:14px;font-weight:800;cursor:pointer}.outline-action:hover,.btn-nav:hover:not(:disabled),.move-actions button:hover:not(:disabled){border-color:#74ebff;background:rgba(18,146,204,.5)}button:disabled{opacity:.45;cursor:not-allowed}.btn-submit,.btn-primary{background:linear-gradient(135deg,#08a5d6,#147bb9);border-color:transparent}.step-indicator{display:flex;align-items:center;margin:0 -12px 30px;padding:18px 22px;overflow-x:auto;border:1px solid rgba(0,180,255,.3);border-radius:18px;background:rgba(1,18,31,.62)}.step-tab{display:flex;flex:1 0 145px;min-width:0;align-items:center;gap:10px;min-height:66px;padding:8px;border:1px solid rgba(68,141,177,.35);border-radius:8px;color:#9ab3d0;background:rgba(3,27,43,.52);font:inherit;text-align:left;cursor:pointer}.step-tab.active{border-color:#53d8fb;background:linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));color:#fff}.step-tab.complete{color:#c8edff}.step-marker{display:grid;place-items:center;width:34px;height:34px;border:1px solid currentColor;border-radius:50%;font-weight:900}.step-copy{display:grid;gap:3px}.step-copy small{font-size:11px;color:inherit}.step-connector{position:relative;flex:0 0 24px;height:2px;overflow:visible;background:rgba(86,198,235,.48)}.step-connector::after{position:absolute;top:50%;right:-1px;width:6px;height:6px;border-top:1px solid #73b1ce;border-right:1px solid #73b1ce;content:"";transform:translateY(-50%) rotate(45deg)}.step-card{padding:24px}.step-title-row{display:flex;align-items:center;justify-content:space-between;gap:16px}.step-chip{padding:7px 12px}.step-desc{margin:18px 0 24px;color:#d7ecff;font-size:15px;line-height:1.8}.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:#71dfff;font-size:12px;font-weight:800}.order-rail i{flex:1;width:2px;margin:9px 0;background:linear-gradient(#167da9,#71dfff);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:#167da9}.order-rail i::after{bottom:-1px;border-top-color:#71dfff}.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 rgba(102,194,232,.42);border-radius:14px;background:linear-gradient(100deg,rgba(21,91,125,.5),rgba(12,39
|
|
|
|
|
|
.flow-workspace{max-width:1000px;margin:0 auto}.flow-caption{display:flex;align-items:center;gap:12px;margin:0 0 14px;color:#71dfff;font-size:12px;font-weight:800}.flow-caption i{flex:1;height:1px;background:linear-gradient(90deg,#167da9,#71dfff)}.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 rgba(102,194,232,.42);border-radius:14px;background:linear-gradient(100deg,rgba(21,91,125,.5),rgba(12,39,58,.8));cursor:grab;transition:.2s}.flow-card:hover,.flow-card.dragging{border-color:#6de5ff;box-shadow:0 10px 22px rgba(0,0,0,.22);transform:translateY(-2px)}@media (max-width:720px){.flow-list{grid-template-columns:1fr}.flow-card{grid-template-columns:30px 17px minmax(0,1fr)}.flow-card .move-actions{grid-column:3;grid-template-columns:repeat(2,32px)}}
|
|
|
|
|
|
.scene-toolbar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px;color:#d9efff;font-weight:800}.scene-toolbar em{color:#71dfff;font-style:normal;font-size:12px}.scene-toolbar__actions{display:flex;gap:8px}.scenario-table-wrap{overflow:auto;border:1px solid rgba(102,194,232,.42);border-radius:14px;background:rgba(3,22,36,.6)}.scenario-table{width:100%;min-width:1400px;border-collapse:collapse;table-layout:fixed}.scenario-table th{padding:13px 8px;border-right:1px solid rgba(102,194,232,.42);border-bottom:1px solid rgba(102,194,232,.42);color:#dff7ff;background:rgba(14,126,177,.48);font-size:14px;line-height:1.45}.scenario-table th small{color:#9edff5;font-size:12px}.scenario-table td{padding:9px;border-right:1px solid rgba(102,194,232,.28);border-bottom:1px solid rgba(102,194,232,.28);vertical-align:middle}.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:#71dfff;font-weight:900;text-align:center}.scenario-table :deep(.el-textarea__inner){min-height:82px!important;border:0;border-radius:8px;color:#edfaff;background:rgba(9,51,75,.52);box-shadow:none;line-height:1.55}.scenario-table :deep(.el-textarea__inner::placeholder){color:#91b7c9}.scenario-table tr:last-child td{border-bottom:0}.scenario-table th:last-child,.scenario-table td:last-child{border-right:0}@media (max-width:720px){.scene-toolbar{align-items:flex-start;flex-direction:column}.scene-toolbar__actions{width:100%}.scene-toolbar__actions .btn-nav{flex:1}}
|
|
|
|
|
|
.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 rgba(102,194,232,.42);border-radius:16px;background:linear-gradient(145deg,rgba(13,71,98,.55),rgba(3,23,38,.82))}.architecture-image-card__head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:13px;color:#fff;font-weight:900}.architecture-image-card__head small{color:#7ce4ff;font-size:12px}.architecture-image-trigger{display:grid;place-items:center;align-content:center;gap:7px;min-height:154px;border:1px dashed rgba(105,221,255,.65);border-radius:12px;color:#e8fbff;background:rgba(5,73,104,.22);cursor:pointer;text-align:center;transition:.2s}.architecture-image-trigger:hover{border-color:#77edff;background:rgba(8,122,169,.28)}.architecture-image-trigger input{position:absolute;width:1px;height:1px;overflow:hidden;opacity:0}.architecture-image-trigger .el-icon{font-size:24px;color:#76e5ff}.architecture-image-trigger span{color:#a3d9e9;font-size:12px}.architecture-image-preview{margin:13px 0 0}.architecture-image-preview :deep(.el-image){display:block;width:100%;height:170px;overflow:hidden;border:1px solid rgba(109,229,255,.5);border-radius:10px;background:#fff;cursor:zoom-in}.architecture-image-preview figcaption{display:flex;justify-content:space-between;gap:10px;align-items:center;margin-top:9px;color:#add8e8;font-size:12px}.architecture-image-preview figcaption span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.architecture-image-preview button,.architecture-guide button{border:0;border-radius:7px;padding:5px 9px;color:#fff;background:#0d91c7;font:inherit;cursor:pointer}.architecture-guide{margin-top:24px;padding:18px 22px;border-left:3px solid #27c8ff;border-radius:0 14px 14px 0;color:#c9e8f5;background:rgba(11,79,109,.22);line-height:1.75}.architecture-guide h4{margin:0 0 8px;color:#fff}.architecture-guide ol{margin:0 0 10px;padding-left:20px}.architecture-guide li{margin:5px 0}@media (max-width:1000px){.architecture-image-grid{grid-template-columns:1fr}}
|
|
|
|
|
|
.architecture-toolbar { flex: 0 0 auto; justify-content: flex-end; max-width: 100%; }
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
|
.architecture-toolbar { width: 100%; justify-content: flex-start; }
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|