|
|
<template>
|
|
|
<div class="product-process student-training-shell">
|
|
|
<main class="process-core">
|
|
|
<section class="task-header">
|
|
|
<div class="task-head-main">
|
|
|
<h1 class="task-title">{{ taskTitle }}</h1>
|
|
|
<div 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>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="task-action-strip">
|
|
|
<TrainingTaskToolbar :task-key="TASK_KEY" :material-name="taskConfig?.materialName"
|
|
|
:material-url="taskConfig?.materialUrl" :training-data-name="taskConfig?.trainingDataName"
|
|
|
:training-data-url="taskConfig?.trainingDataUrl" :guide-manual-name="taskConfig?.guideManualName"
|
|
|
:guide-manual-url="taskConfig?.guideManualUrl" :ai-manual-name="taskConfig?.aiManualName"
|
|
|
:ai-manual-url="taskConfig?.aiManualUrl" />
|
|
|
</section>
|
|
|
|
|
|
<section class="process-workbench">
|
|
|
<nav v-if="trainingSteps.length" class="step-indicator" aria-label="产品开发主要流程步骤">
|
|
|
<button v-for="(step, index) in trainingSteps" :key="`${step}-${index}`" type="button"
|
|
|
:class="['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]"
|
|
|
:aria-label="`切换至第 ${index + 1} 步:${step}`"
|
|
|
:aria-current="currentStep === index + 1 ? 'step' : undefined" :title="step"
|
|
|
@click="currentStep = index + 1">
|
|
|
<span class="step-bar" aria-hidden="true"></span>
|
|
|
<span class="step-no" aria-hidden="true">{{ String(index + 1).padStart(2, '0') }}</span>
|
|
|
<span class="step-name">{{ step }}</span>
|
|
|
</button>
|
|
|
</nav>
|
|
|
|
|
|
<section class="process-step-card">
|
|
|
<template v-if="currentStep === 1">
|
|
|
<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="process-intro">
|
|
|
互联网产品开发在拆解任务之前,团队必须先明确做什么样的产品、开发过程中有哪些卡点。这是后续排期的前提。请判断哪些开发环节需要特别关注:关键任务会影响全项目进度与最终落地效果,非关键任务可在核心任务约束下灵活协调。
|
|
|
</p>
|
|
|
|
|
|
<div class="training-table-wrap">
|
|
|
<table class="process-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>阶段</th>
|
|
|
<th>任务编号</th>
|
|
|
<th>具体工作任务</th>
|
|
|
<th>是否关键任务</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="row in processRows" :key="row.taskNo" :class="feedbackClass(row.taskNo)">
|
|
|
<td class="stage-cell">{{ row.stage }}</td>
|
|
|
<td class="task-no-cell">{{ row.taskNo }}</td>
|
|
|
<td class="task-cell">{{ row.task }}</td>
|
|
|
<td class="select-cell">
|
|
|
<select v-model="row.classification" :disabled="checking"
|
|
|
@change="clearFeedback(row.taskNo)">
|
|
|
<option value="" disabled>请选择</option>
|
|
|
<option v-for="option in CLASSIFICATION_OPTIONS" :key="option"
|
|
|
:value="option">{{ option }}</option>
|
|
|
</select>
|
|
|
<p v-if="feedbackByTaskNo[row.taskNo]" class="classification-feedback">{{
|
|
|
feedbackByTaskNo[row.taskNo].message }}</p>
|
|
|
</td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
<p v-if="checkResult" class="classification-summary"
|
|
|
:class="{ 'is-success': checkResult.allCorrect }">本次判题:{{ checkResult.correctCount }} / {{
|
|
|
checkResult.totalCount }} 项分类正确</p>
|
|
|
</template>
|
|
|
<template v-else-if="currentStep === 2">
|
|
|
<div class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step 02</p>
|
|
|
<h3>{{ trainingSteps[1] }}</h3>
|
|
|
</div>
|
|
|
<div class="step-title-actions">
|
|
|
<button type="button" class="outline-action schedule-export-action"
|
|
|
@click="exportScheduleList"><el-icon>
|
|
|
<Download />
|
|
|
</el-icon>导出列表</button>
|
|
|
<span class="step-chip">项目排期练习</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<p class="process-intro">将开发一款互联网产品的模糊目标,拆解为可执行的具体工作任务。根据案例信息,填写参考工期和项目依赖关系。</p>
|
|
|
<p v-if="!hasCompletedStepOne" class="step-warning">请先完成第 1 步并判题通过;关键任务结果会自动带入本表。</p>
|
|
|
<div class="training-table-wrap">
|
|
|
<table class="process-table schedule-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>阶段</th>
|
|
|
<th>任务编号</th>
|
|
|
<th>具体工作任务</th>
|
|
|
<th>是否关键任务</th>
|
|
|
<th>开始时间</th>
|
|
|
<th>完成时间</th>
|
|
|
<th>项目依赖关系</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="row in processRows" :key="row.taskNo">
|
|
|
<td class="stage-cell">{{ row.stage }}</td>
|
|
|
<td class="task-no-cell">{{ row.taskNo }}</td>
|
|
|
<td class="task-cell">{{ row.task }}</td>
|
|
|
<td class="classification-cell">{{ row.classification || '—' }}</td>
|
|
|
<td><input v-model="row.startDate" type="date"
|
|
|
:disabled="!hasCompletedStepOne || checking" /></td>
|
|
|
<td><input v-model="row.endDate" type="date"
|
|
|
:disabled="!hasCompletedStepOne || checking" /></td>
|
|
|
<td><input v-model.trim="row.dependency" class="dependency-input"
|
|
|
:disabled="!hasCompletedStepOne || checking" placeholder="示例:开始-开始(SS)"
|
|
|
maxlength="100" /></td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</template>
|
|
|
<template v-else-if="currentStep === 3">
|
|
|
<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="process-intro">根据上表数据,使用工具绘制甘特图,并上传。</p>
|
|
|
<p v-if="!stepTwoPassed" class="step-warning">请先保存第 2 步项目排期,再上传甘特图。</p>
|
|
|
<section class="gantt-upload-panel">
|
|
|
<label class="gantt-upload-trigger" :class="{ disabled: !stepTwoPassed || uploadingGantt }">
|
|
|
<input type="file" accept="image/png,image/jpeg,image/webp,image/gif"
|
|
|
:disabled="!stepTwoPassed || uploadingGantt" @change="uploadGanttImage" />
|
|
|
<el-icon>
|
|
|
<Upload />
|
|
|
</el-icon>
|
|
|
<strong>{{ uploadingGantt ? '上传中…' : ganttImage.url ? '替换甘特图' : '上传甘特图' }}</strong>
|
|
|
<span>支持 PNG、JPG、JPEG、WEBP、GIF</span>
|
|
|
</label>
|
|
|
<figure v-if="ganttImage.url" class="gantt-preview">
|
|
|
<img :src="resolveGanttImageUrl(ganttImage.url)" alt="已上传的甘特图"
|
|
|
@click="previewGanttImage" />
|
|
|
<figcaption>{{ ganttImage.name }}</figcaption>
|
|
|
</figure>
|
|
|
</section>
|
|
|
</template>
|
|
|
<template v-else-if="currentStep === 4">
|
|
|
<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 v-if="!stepThreePassed" class="step-warning">请先上传并保存第 3 步甘特图,再完成本步复盘。</p>
|
|
|
<section class="process-reflection" :class="{ disabled: !stepThreePassed }">
|
|
|
<label class="reflection-field">
|
|
|
<span><b class="reflection-no">01</b>观察甘特图,思考电商运营在产品开发过程中,可以提前介入同步完成哪些工作,以缩短运营周期、提高效率。(如“预热蓄水”“内容种草”等)</span>
|
|
|
<textarea v-model.trim="stepFourForm.parallelWorkAnalysis"
|
|
|
:disabled="!stepThreePassed || checking"
|
|
|
placeholder="请结合甘特图说明可同步推进的电商运营工作、介入节点及预期效率提升"></textarea>
|
|
|
</label>
|
|
|
<div class="reflection-question"><b class="reflection-no">02</b>在互联网产品开发管理中,你认为哪些环节可以借助AI工具提升效率?请列举至少2个具体应用场景。</div>
|
|
|
<div class="scenario-grid">
|
|
|
<label class="reflection-field"><span>场景1:</span><textarea
|
|
|
v-model.trim="stepFourForm.aiScenarioOne"
|
|
|
:disabled="!stepThreePassed || checking"
|
|
|
placeholder="例如:使用 AI 汇总竞品评价与用户需求,辅助生成PRD初稿"></textarea></label>
|
|
|
<label class="reflection-field"><span>场景2:</span><textarea
|
|
|
v-model.trim="stepFourForm.aiScenarioTwo"
|
|
|
:disabled="!stepThreePassed || checking"
|
|
|
placeholder="例如:使用 AI 生成电商详情页文案、主图脚本或运营素材"></textarea></label>
|
|
|
</div>
|
|
|
</section>
|
|
|
</template>
|
|
|
<div v-else class="step-placeholder">
|
|
|
<p>第 {{ currentStep }} 步内容待配置</p><span>第 1 步答案已保存,可从上方步骤栏返回查看。</span>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<div class="step-navigation">
|
|
|
<div class="step-nav-left">
|
|
|
<button v-if="currentStep > 1" type="button" class="btn-nav btn-ghost" @click="prevStep">
|
|
|
<el-icon>
|
|
|
<ArrowLeft />
|
|
|
</el-icon>
|
|
|
上一步
|
|
|
</button>
|
|
|
<button type="button" class="btn-nav btn-save" :disabled="saving || uploadingGantt"
|
|
|
@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="checking || saving || uploadingGantt" @click="nextStep">
|
|
|
{{ checking ? '判题中…' : '下一步' }}
|
|
|
<el-icon>
|
|
|
<ArrowRight />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
</main>
|
|
|
|
|
|
<TrainingAiSidebar study-tip="请根据产品开发流程中任务对总进度和最终落地效果的影响,判断其是否为关键任务。" reference-title="关键任务判断提示"
|
|
|
reference-text="· 关键任务:直接影响产品定义、核心技术验证、量产交付或上市节奏<br />· 非关键任务:可在核心任务约束下进行资源协调,不直接阻塞整体排期"
|
|
|
:progress-items="progressItems"></TrainingAiSidebar>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, Operation, RefreshLeft, Upload } from "@element-plus/icons-vue"
|
|
|
import * as XLSX from "xlsx"
|
|
|
import { checkProductDevelopmentProcessStepOne, checkProductDevelopmentProcessStepTwo, checkProductDevelopmentProcessStepThree, checkProductDevelopmentProcessStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask"
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap"
|
|
|
import { isExternal } from "@/utils/validate"
|
|
|
import useUserStore from "@/store/modules/user"
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"
|
|
|
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"
|
|
|
|
|
|
const { proxy } = getCurrentInstance()
|
|
|
const userStore = useUserStore()
|
|
|
const TASK_KEY = "product-development-process"
|
|
|
const CLASSIFICATION_OPTIONS = ["关键任务", "非关键任务"]
|
|
|
const DEFAULT_STEPS = ["产品开发关键任务识别", "任务依赖关系分析", "开发排期制定", "流程复盘优化"]
|
|
|
const PROCESS_TASKS = [
|
|
|
["需求锚定", "1.1", "竞品调研与用户需求分析"], ["", "1.2", "产品概念确认与核心卖点提炼"],
|
|
|
["产品定义", "2.1", "产品需求文档(PRD)撰写与评审"], ["", "2.2", "外观ID设计"], ["", "2.3", "功能规格确认"],
|
|
|
["开发验证", "3.1", "硬件方案选型与原理图设计"], ["", "3.2", "软件架构及软硬件协同设计"], ["", "3.3", "结构设计评审"], ["", "3.4", "功能手板制作与外观评审"], ["", "3.5", "工程验证测试与设计验证测试"], ["", "3.6", "PVT小批量试产"],
|
|
|
["供应链对接", "4.1", "供应商评估与选定"], ["", "4.2", "模具正式开模与试模修模"], ["", "4.3", "产品认证及质量检测"], ["", "4.4", "BOM成本核算与采购合同签订"],
|
|
|
["上市推广", "5.1", "产品量产及入库"], ["", "5.2", "电商详情页全渠道上线"], ["", "5.3", "正式开售与首销爆发"], ["", "5.4", "首批用户评价监控与差评预警"],
|
|
|
]
|
|
|
const defaultTask = {
|
|
|
title: "产品开发主要流程",
|
|
|
background: "产品开发需要识别影响整体交付、量产和上市节奏的关键环节,并据此安排资源与排期。",
|
|
|
goals: ["理解产品开发的主要阶段与任务", "识别影响项目整体进度的关键任务", "形成后续排期和资源协调的判断依据"],
|
|
|
requirement: "请按任务对整体开发进度和最终落地效果的影响,判断其为关键任务或非关键任务。",
|
|
|
}
|
|
|
|
|
|
const taskConfig = ref(null)
|
|
|
const processRows = ref(createProcessRows())
|
|
|
const currentStep = ref(1)
|
|
|
const briefOpen = ref(true)
|
|
|
const checking = ref(false)
|
|
|
const saving = ref(false)
|
|
|
const draftReady = ref(false)
|
|
|
const checkResult = ref(null)
|
|
|
const stepOnePassed = ref(false)
|
|
|
const stepTwoPassed = ref(false)
|
|
|
const stepThreePassed = ref(false)
|
|
|
const ganttImage = ref({ url: "", name: "" })
|
|
|
const uploadingGantt = ref(false)
|
|
|
const stepFourForm = ref({ parallelWorkAnalysis: "", aiScenarioOne: "", aiScenarioTwo: "" })
|
|
|
const taskTitle = computed(() => taskConfig.value?.taskName || defaultTask.title)
|
|
|
const taskBackground = computed(() => taskConfig.value?.background || defaultTask.background)
|
|
|
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultTask.goals))
|
|
|
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultTask.requirement)
|
|
|
const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, DEFAULT_STEPS))
|
|
|
const feedbackByTaskNo = computed(() => Object.fromEntries((checkResult.value?.items || []).map((item) => [item.taskNo, item])))
|
|
|
const hasCompletedStepOne = computed(() => stepOnePassed.value)
|
|
|
const progressItems = computed(() => [
|
|
|
{ text: `进行中:${trainingSteps.value[Math.max(0, currentStep.value - 1)] || taskTitle.value}`, status: "doing" },
|
|
|
{ text: "完成本步后进入第 2 步", status: "" },
|
|
|
{ text: "可选:导出实训成果", status: "" },
|
|
|
])
|
|
|
|
|
|
onMounted(async () => {
|
|
|
await loadTaskConfig()
|
|
|
loadDraft()
|
|
|
await loadSavedAnswer()
|
|
|
draftReady.value = true
|
|
|
})
|
|
|
|
|
|
watch(() => JSON.stringify(buildDraft()), persistDraft)
|
|
|
|
|
|
function createProcessRows() {
|
|
|
return PROCESS_TASKS.map(([stage, taskNo, task]) => ({ stage, taskNo, task, classification: "", startDate: "", endDate: "", dependency: "" }))
|
|
|
}
|
|
|
|
|
|
function parseArray(value, fallback) {
|
|
|
if (Array.isArray(value)) return value.length ? value : fallback
|
|
|
if (!value) return fallback
|
|
|
try {
|
|
|
const parsed = JSON.parse(value)
|
|
|
return Array.isArray(parsed) && parsed.length ? parsed.map((item) => String(item?.name ?? item).trim()).filter(Boolean) : fallback
|
|
|
} catch (error) {
|
|
|
const list = String(value).split(/[;;|]/).map((item) => item.trim()).filter(Boolean)
|
|
|
return list.length ? list : fallback
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function loadTaskConfig() {
|
|
|
try { taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || null } catch (error) { taskConfig.value = null }
|
|
|
}
|
|
|
|
|
|
async function loadSavedAnswer() {
|
|
|
try {
|
|
|
const answer = (await getStudentTrainingAnswer(TASK_KEY))?.data
|
|
|
if (answer?.step1Answer) {
|
|
|
restoreOutcome(answer.step1Answer)
|
|
|
stepOnePassed.value = processRows.value.every((row) => CLASSIFICATION_OPTIONS.includes(row.classification))
|
|
|
clearDraft()
|
|
|
}
|
|
|
if (answer?.step2Answer) restoreStepTwoOutcome(answer.step2Answer)
|
|
|
if (answer?.step2Answer) stepTwoPassed.value = true
|
|
|
if (answer?.step3Answer) {
|
|
|
restoreStepThreeOutcome(answer.step3Answer)
|
|
|
stepThreePassed.value = true
|
|
|
}
|
|
|
if (answer?.step4Answer) restoreStepFourOutcome(answer.step4Answer)
|
|
|
const savedStep = Number(answer?.currentStep)
|
|
|
if (Number.isInteger(savedStep) && savedStep > 0) currentStep.value = savedStep
|
|
|
} catch (error) {
|
|
|
// Keep the browser draft available while the answer endpoint is unavailable.
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function restoreOutcome(value) {
|
|
|
try {
|
|
|
const parsed = typeof value === "string" ? JSON.parse(value) : value
|
|
|
const savedRows = Array.isArray(parsed?.items) ? parsed.items : []
|
|
|
const byTaskNo = Object.fromEntries(savedRows.map((item) => [item.taskNo, item]))
|
|
|
processRows.value = createProcessRows().map((row) => ({ ...row, classification: byTaskNo[row.taskNo]?.classification || "" }))
|
|
|
} catch (error) {
|
|
|
processRows.value = createProcessRows()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function restoreStepTwoOutcome(value) {
|
|
|
try {
|
|
|
const parsed = typeof value === "string" ? JSON.parse(value) : value
|
|
|
const byTaskNo = Object.fromEntries((Array.isArray(parsed?.items) ? parsed.items : []).map((item) => [item.taskNo, item]))
|
|
|
processRows.value = processRows.value.map((row) => ({
|
|
|
...row,
|
|
|
startDate: byTaskNo[row.taskNo]?.startDate || "",
|
|
|
endDate: byTaskNo[row.taskNo]?.endDate || "",
|
|
|
dependency: byTaskNo[row.taskNo]?.dependency || "",
|
|
|
}))
|
|
|
} catch (error) {
|
|
|
// Ignore a malformed historical second-step answer.
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function restoreStepThreeOutcome(value) {
|
|
|
try {
|
|
|
const parsed = typeof value === "string" ? JSON.parse(value) : value
|
|
|
ganttImage.value = { url: parsed?.ganttImageUrl || "", name: parsed?.ganttImageName || "" }
|
|
|
} catch (error) {
|
|
|
ganttImage.value = { url: "", name: "" }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function restoreStepFourOutcome(value) {
|
|
|
try {
|
|
|
const parsed = typeof value === "string" ? JSON.parse(value) : value
|
|
|
stepFourForm.value = {
|
|
|
parallelWorkAnalysis: parsed?.parallelWorkAnalysis || "",
|
|
|
aiScenarioOne: parsed?.aiScenarioOne || "",
|
|
|
aiScenarioTwo: parsed?.aiScenarioTwo || "",
|
|
|
}
|
|
|
} catch (error) {
|
|
|
stepFourForm.value = { parallelWorkAnalysis: "", aiScenarioOne: "", aiScenarioTwo: "" }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function getLocalDraftKey() {
|
|
|
const info = userStore.userInfo || {}
|
|
|
return `${TASK_KEY}:draft:${info.userId || info.user_id || info.username || info.userName || "anonymous"}`
|
|
|
}
|
|
|
|
|
|
function buildOutcome() {
|
|
|
return { title: taskTitle.value, items: processRows.value.map(({ taskNo, classification }) => ({ taskNo, classification })) }
|
|
|
}
|
|
|
|
|
|
function buildStepTwoOutcome() {
|
|
|
return {
|
|
|
title: taskTitle.value,
|
|
|
items: processRows.value.map(({ taskNo, classification, startDate, endDate, dependency }) => ({ taskNo, classification, startDate, endDate, dependency })),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function getStepTwoValidationMessage() {
|
|
|
for (const row of processRows.value) {
|
|
|
if (!String(row.dependency || "").trim()) return `请填写任务 ${row.taskNo} 的项目依赖关系`
|
|
|
if (!String(row.startDate || "").trim()) return `请填写任务 ${row.taskNo} 的开始时间`
|
|
|
if (!String(row.endDate || "").trim()) return `请填写任务 ${row.taskNo} 的完成时间`
|
|
|
if (row.startDate > row.endDate) return `任务 ${row.taskNo} 的完成时间不能早于开始时间`
|
|
|
}
|
|
|
return ""
|
|
|
}
|
|
|
|
|
|
function buildStepThreeOutcome() {
|
|
|
return { title: taskTitle.value, ganttImageUrl: ganttImage.value.url, ganttImageName: ganttImage.value.name }
|
|
|
}
|
|
|
|
|
|
function buildStepFourOutcome() {
|
|
|
return { ...stepFourForm.value }
|
|
|
}
|
|
|
|
|
|
function buildDraft() { return { step1: buildOutcome(), step2: buildStepTwoOutcome(), step3: buildStepThreeOutcome(), step4: buildStepFourOutcome(), currentStep: currentStep.value } }
|
|
|
|
|
|
function persistDraft() {
|
|
|
if (!draftReady.value) return
|
|
|
try { localStorage.setItem(getLocalDraftKey(), JSON.stringify(buildDraft())) } catch (error) { /* Browser draft is optional. */ }
|
|
|
}
|
|
|
|
|
|
function loadDraft() {
|
|
|
try {
|
|
|
const raw = localStorage.getItem(getLocalDraftKey())
|
|
|
if (raw) {
|
|
|
const draft = JSON.parse(raw)
|
|
|
restoreOutcome(draft?.step1 || draft)
|
|
|
if (draft?.step2) restoreStepTwoOutcome(draft.step2)
|
|
|
if (draft?.step3) restoreStepThreeOutcome(draft.step3)
|
|
|
if (draft?.step4) restoreStepFourOutcome(draft.step4)
|
|
|
if (Number(draft?.currentStep) > 0) currentStep.value = Number(draft.currentStep)
|
|
|
}
|
|
|
} catch (error) { clearDraft() }
|
|
|
}
|
|
|
|
|
|
function clearDraft() {
|
|
|
try { localStorage.removeItem(getLocalDraftKey()) } catch (error) { /* Ignore storage failures. */ }
|
|
|
}
|
|
|
|
|
|
function feedbackClass(taskNo) {
|
|
|
const feedback = feedbackByTaskNo.value[taskNo]
|
|
|
return feedback ? (feedback.correct ? "is-correct" : "is-incorrect") : ""
|
|
|
}
|
|
|
|
|
|
function clearFeedback(taskNo) {
|
|
|
if (!feedbackByTaskNo.value[taskNo]) return
|
|
|
checkResult.value = { ...checkResult.value, items: checkResult.value.items.filter((item) => item.taskNo !== taskNo) }
|
|
|
}
|
|
|
|
|
|
async function saveAndGoToStepTwo() {
|
|
|
if (checking.value) return
|
|
|
checking.value = true
|
|
|
try {
|
|
|
const response = await checkProductDevelopmentProcessStepOne(buildOutcome().items)
|
|
|
checkResult.value = response?.data || null
|
|
|
if (!checkResult.value?.allCorrect) {
|
|
|
proxy?.$modal?.msgError?.("存在分类错误,请根据提示修改后再进入第 2 步")
|
|
|
return
|
|
|
}
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildOutcome()), currentStep: 2, submitted: false, saveAction: "SAVE" })
|
|
|
clearDraft()
|
|
|
stepOnePassed.value = true
|
|
|
currentStep.value = 2
|
|
|
proxy?.$modal?.msgSuccess("已保存,已进入第 2 步")
|
|
|
} catch (error) {
|
|
|
proxy?.$modal?.msgError?.(error?.message || "请完成全部任务分类后再判题")
|
|
|
} finally {
|
|
|
checking.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function resetTraining() {
|
|
|
processRows.value = createProcessRows()
|
|
|
currentStep.value = 1
|
|
|
checkResult.value = null
|
|
|
stepOnePassed.value = false
|
|
|
stepTwoPassed.value = false
|
|
|
stepThreePassed.value = false
|
|
|
ganttImage.value = { url: "", name: "" }
|
|
|
stepFourForm.value = { parallelWorkAnalysis: "", aiScenarioOne: "", aiScenarioTwo: "" }
|
|
|
clearDraft()
|
|
|
try {
|
|
|
checking.value = true
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildOutcome()), currentStep: 1, submitted: false, saveAction: "RESET" })
|
|
|
} catch (error) {
|
|
|
// The local reset remains effective if the backend is temporarily unavailable.
|
|
|
} finally {
|
|
|
checking.value = false
|
|
|
}
|
|
|
proxy?.$modal?.msgSuccess("已清空填写内容")
|
|
|
}
|
|
|
|
|
|
async function saveAndGoToStepThree() {
|
|
|
if (checking.value || !hasCompletedStepOne.value) return
|
|
|
const validationMessage = getStepTwoValidationMessage()
|
|
|
if (validationMessage) {
|
|
|
proxy?.$modal?.msgWarning?.(validationMessage)
|
|
|
return
|
|
|
}
|
|
|
checking.value = true
|
|
|
try {
|
|
|
await checkProductDevelopmentProcessStepTwo(buildStepTwoOutcome().items)
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: 3, submitted: false, saveAction: "SAVE" })
|
|
|
clearDraft()
|
|
|
stepTwoPassed.value = true
|
|
|
currentStep.value = 3
|
|
|
proxy?.$modal?.msgSuccess("已保存,已进入第 3 步")
|
|
|
} catch (error) {
|
|
|
proxy?.$modal?.msgError?.(error?.message || "请完整填写任务工期和项目依赖关系")
|
|
|
} finally {
|
|
|
checking.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function resetStepTwo() {
|
|
|
processRows.value = processRows.value.map((row) => ({ ...row, startDate: "", endDate: "", dependency: "" }))
|
|
|
proxy?.$modal?.msgSuccess("已清空第 2 步填写内容")
|
|
|
}
|
|
|
|
|
|
async function uploadGanttImage(event) {
|
|
|
const file = event.target.files?.[0]
|
|
|
if (!file || !stepTwoPassed.value) return
|
|
|
if (!/^image\/(png|jpeg|webp|gif)$/i.test(file.type)) {
|
|
|
proxy?.$modal?.msgError?.("请上传 PNG、JPG、JPEG、WEBP 或 GIF 格式的甘特图")
|
|
|
event.target.value = ""
|
|
|
return
|
|
|
}
|
|
|
uploadingGantt.value = true
|
|
|
try {
|
|
|
const formData = new FormData()
|
|
|
formData.append("file", file)
|
|
|
const response = await uploadStudentTrainingFile(formData)
|
|
|
ganttImage.value = {
|
|
|
url: response?.url || response?.fileName || "",
|
|
|
name: response?.originalFilename || file.name,
|
|
|
}
|
|
|
if (!ganttImage.value.url) throw new Error("上传结果缺少文件地址")
|
|
|
proxy?.$modal?.msgSuccess("甘特图上传成功")
|
|
|
} catch (error) {
|
|
|
ganttImage.value = { url: "", name: "" }
|
|
|
proxy?.$modal?.msgError?.(error?.message || "甘特图上传失败")
|
|
|
} finally {
|
|
|
uploadingGantt.value = false
|
|
|
event.target.value = ""
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function saveAndGoToStepFour() {
|
|
|
if (uploadingGantt.value || !stepTwoPassed.value) return
|
|
|
checking.value = true
|
|
|
try {
|
|
|
await checkProductDevelopmentProcessStepThree(buildStepThreeOutcome())
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" })
|
|
|
clearDraft()
|
|
|
stepThreePassed.value = true
|
|
|
currentStep.value = 4
|
|
|
proxy?.$modal?.msgSuccess("已保存,已进入第 4 步")
|
|
|
} catch (error) {
|
|
|
proxy?.$modal?.msgError?.(error?.message || "请先上传甘特图")
|
|
|
} finally {
|
|
|
checking.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function resetStepThree() {
|
|
|
ganttImage.value = { url: "", name: "" }
|
|
|
proxy?.$modal?.msgSuccess("已清空第 3 步上传内容")
|
|
|
}
|
|
|
|
|
|
async function submitTraining() {
|
|
|
if (checking.value || !stepThreePassed.value) return
|
|
|
checking.value = true
|
|
|
try {
|
|
|
await checkProductDevelopmentProcessStepFour(buildStepFourOutcome())
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: true, saveAction: "SUBMIT" })
|
|
|
clearDraft()
|
|
|
proxy?.$modal?.msgSuccess("任务提交成功")
|
|
|
} catch (error) {
|
|
|
proxy?.$modal?.msgError?.(error?.message || "请完成并行协同复盘与两个AI应用场景")
|
|
|
} finally {
|
|
|
checking.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function resetStepFour() {
|
|
|
stepFourForm.value = { parallelWorkAnalysis: "", aiScenarioOne: "", aiScenarioTwo: "" }
|
|
|
proxy?.$modal?.msgSuccess("已清空第 4 步填写内容")
|
|
|
}
|
|
|
|
|
|
function resolveGanttImageUrl(url) {
|
|
|
const source = String(url || "").trim()
|
|
|
if (!source || source.startsWith("data:") || source.startsWith("blob:")) return source
|
|
|
if (!isExternal(source)) return source.startsWith("/") ? source : `/${source}`
|
|
|
|
|
|
try {
|
|
|
const parsed = new URL(source)
|
|
|
if (["localhost", "127.0.0.1", "::1"].includes(parsed.hostname)) {
|
|
|
return `${parsed.pathname}${parsed.search}${parsed.hash}`
|
|
|
}
|
|
|
} catch {
|
|
|
return source
|
|
|
}
|
|
|
return source
|
|
|
}
|
|
|
|
|
|
function previewGanttImage() {
|
|
|
window.open(resolveGanttImageUrl(ganttImage.value.url), "_blank", "noopener,noreferrer")
|
|
|
}
|
|
|
|
|
|
|
|
|
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)
|
|
|
}
|
|
|
|
|
|
function exportScheduleList() {
|
|
|
const rows = processRows.value.map((row) => ({
|
|
|
阶段: row.stage,
|
|
|
任务编号: row.taskNo,
|
|
|
具体工作任务: row.task,
|
|
|
是否关键任务: row.classification,
|
|
|
开始时间: row.startDate,
|
|
|
完成时间: row.endDate,
|
|
|
项目依赖关系: row.dependency,
|
|
|
}))
|
|
|
const worksheet = XLSX.utils.json_to_sheet(rows)
|
|
|
worksheet["!cols"] = [
|
|
|
{ wch: 12 }, { wch: 10 }, { wch: 28 }, { wch: 14 }, { wch: 14 }, { wch: 14 }, { wch: 22 },
|
|
|
]
|
|
|
const workbook = XLSX.utils.book_new()
|
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "WBS排期表")
|
|
|
XLSX.writeFile(workbook, "产品开发WBS任务拆解与排期表.xlsx")
|
|
|
proxy?.$modal?.msgSuccess("列表导出成功")
|
|
|
}
|
|
|
|
|
|
function exportOutcome() {
|
|
|
const blob = new Blob([JSON.stringify(buildDraft(), null, 2)], { type: "application/json;charset=utf-8" })
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
const link = document.createElement("a")
|
|
|
link.href = url
|
|
|
link.download = "产品开发主要流程-实训成果.json"
|
|
|
document.body.appendChild(link)
|
|
|
link.click()
|
|
|
document.body.removeChild(link)
|
|
|
URL.revokeObjectURL(url)
|
|
|
proxy?.$modal?.msgSuccess("导出成功")
|
|
|
}
|
|
|
|
|
|
function prevStep() {
|
|
|
if (currentStep.value > 1) {
|
|
|
currentStep.value -= 1
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function saveCurrentProgress() {
|
|
|
if (saving.value || uploadingGantt.value) return
|
|
|
saving.value = true
|
|
|
try {
|
|
|
let payload
|
|
|
if (currentStep.value === 1) {
|
|
|
payload = { step1Answer: JSON.stringify(buildOutcome()), currentStep: 1, submitted: false, saveAction: "SAVE" }
|
|
|
} else if (currentStep.value === 2) {
|
|
|
payload = { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: 2, submitted: false, saveAction: "SAVE" }
|
|
|
} else if (currentStep.value === 3) {
|
|
|
payload = { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 3, submitted: false, saveAction: "SAVE" }
|
|
|
} else {
|
|
|
payload = { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }
|
|
|
}
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, payload)
|
|
|
clearDraft()
|
|
|
proxy?.$modal?.msgSuccess("保存成功")
|
|
|
} catch (error) {
|
|
|
proxy?.$modal?.msgError?.(error?.message || "保存失败,请稍后重试")
|
|
|
} finally {
|
|
|
saving.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function nextStep() {
|
|
|
if (checking.value || saving.value) return
|
|
|
if (currentStep.value === 1) {
|
|
|
await saveAndGoToStepTwo()
|
|
|
} else if (currentStep.value === 2) {
|
|
|
await saveAndGoToStepThree()
|
|
|
} else if (currentStep.value === 3) {
|
|
|
await saveAndGoToStepFour()
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
/*
|
|
|
* 页面级令牌(墨黑极简主题):纯白画布 + 墨黑主操作 + 中性灰层次。
|
|
|
* 覆盖全局主色/文字/边框令牌,使所有 var(--student-*) 规则自动重着色。
|
|
|
*/
|
|
|
.product-process {
|
|
|
/* 主色:教育蓝 */
|
|
|
--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);
|
|
|
--el-textarea-hover-border-color: #3b82f6;
|
|
|
--el-textarea-focus-border-color: #2563eb;
|
|
|
/* 字号梯度统一继承 .student-training-shell,页面不再重复声明 */
|
|
|
min-width: 0;
|
|
|
color: var(--student-text);
|
|
|
}
|
|
|
|
|
|
/* 主列布局:容器栅格与内边距由全局 .student-training-shell 统一提供 */
|
|
|
.process-core {
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
align-content: start;
|
|
|
}
|
|
|
|
|
|
/* 任务头:白卡 + 左侧教育蓝竖条 + 徽章 + 标题 + 简介 */
|
|
|
.task-header {
|
|
|
position: relative;
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.task-title {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
line-height: 1.35;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.task-brief {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.task-brief-text {
|
|
|
display: block;
|
|
|
}
|
|
|
|
|
|
.task-brief-toggle {
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
color: var(--student-accent-deep);
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover {
|
|
|
text-decoration: underline;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.task-brief-more {
|
|
|
display: grid;
|
|
|
gap: 6px;
|
|
|
margin-top: 8px;
|
|
|
|
|
|
b {
|
|
|
color: var(--student-text);
|
|
|
}
|
|
|
|
|
|
ul {
|
|
|
display: grid;
|
|
|
gap: 4px;
|
|
|
margin: 0;
|
|
|
padding-left: 18px;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 工作台:白底卡片 + 12px 圆角 */
|
|
|
.process-workbench {
|
|
|
margin: 0;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: var(--student-card-bg);
|
|
|
|
|
|
&::before {
|
|
|
display: none !important;
|
|
|
content: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 操作工具栏横条:内联布局,去掉悬浮和边框
|
|
|
* ============================================================ */
|
|
|
.task-action-strip {
|
|
|
margin: 0;
|
|
|
padding: 10px 16px;
|
|
|
|
|
|
:deep(.training-task-toolbar) {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
justify-content: flex-end;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 步骤导航(极简文字下划线)的换肤见非 scoped 块 */
|
|
|
|
|
|
.step-name {
|
|
|
display: block;
|
|
|
overflow: hidden;
|
|
|
min-width: 0;
|
|
|
color: inherit;
|
|
|
text-overflow: ellipsis;
|
|
|
}
|
|
|
|
|
|
/* 步骤卡片:12px 圆角白卡 */
|
|
|
.process-step-card {
|
|
|
margin: 0;
|
|
|
padding: 18px 20px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: var(--student-card-bg);
|
|
|
}
|
|
|
|
|
|
.step-title-row {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
margin-bottom: 10px;
|
|
|
}
|
|
|
|
|
|
.step-title-row h3 {
|
|
|
margin: 2px 0 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
.step-title-actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: flex-end;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
/* 标题行内联导出:次按钮的紧凑小号(继承 .outline-action 描边与圆角语言) */
|
|
|
.schedule-export-action {
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 600;
|
|
|
border-radius: 8px;
|
|
|
}
|
|
|
|
|
|
/* Step 编号小标签:教育蓝引导文字 */
|
|
|
.section-eyebrow {
|
|
|
margin: 0;
|
|
|
color: var(--student-accent);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 800;
|
|
|
line-height: 1.3;
|
|
|
letter-spacing: 0.12em;
|
|
|
}
|
|
|
|
|
|
/* 步骤小徽章:教育蓝浅底 */
|
|
|
.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;
|
|
|
}
|
|
|
|
|
|
/* 步骤说明 */
|
|
|
.process-intro {
|
|
|
margin: 0 0 12px;
|
|
|
color: #4b5563;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
/* 表格:纯白 + 中性边框 */
|
|
|
.training-table-wrap {
|
|
|
overflow-x: auto;
|
|
|
border: 1px solid #e5e7eb;
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.process-table {
|
|
|
width: 100%;
|
|
|
min-width: 920px;
|
|
|
border-collapse: collapse;
|
|
|
table-layout: fixed;
|
|
|
}
|
|
|
|
|
|
.process-table th {
|
|
|
padding: 9px 10px;
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
color: #374151;
|
|
|
background: #ffffff;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 700;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.process-table td {
|
|
|
padding: 9px 10px;
|
|
|
border-top: 1px solid #f0f1f3;
|
|
|
color: #374151;
|
|
|
background: #ffffff;
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
|
|
|
.stage-cell,
|
|
|
.task-no-cell {
|
|
|
color: #1f2937;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 700;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.task-cell {
|
|
|
color: #374151;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.55;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
/* 下拉与日期输入:中性描边,聚焦墨黑 */
|
|
|
.select-cell select,
|
|
|
.schedule-table input {
|
|
|
box-sizing: border-box;
|
|
|
width: 100%;
|
|
|
min-height: 34px;
|
|
|
padding: 5px 9px;
|
|
|
border: 1px solid #d1d5db;
|
|
|
border-radius: 8px;
|
|
|
outline: none;
|
|
|
color: #1f2937;
|
|
|
background: #ffffff;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.select-cell select:focus,
|
|
|
.schedule-table input:focus {
|
|
|
border-color: #2563eb;
|
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
|
|
}
|
|
|
|
|
|
/* 判题反馈:仅文字着色,不铺行底色 */
|
|
|
.classification-feedback {
|
|
|
margin: 5px 0 0;
|
|
|
font-size: var(--student-fs-caption);
|
|
|
line-height: 1.4;
|
|
|
}
|
|
|
|
|
|
tr.is-correct .classification-feedback {
|
|
|
color: var(--student-success);
|
|
|
}
|
|
|
|
|
|
tr.is-incorrect .classification-feedback {
|
|
|
color: var(--student-danger);
|
|
|
}
|
|
|
|
|
|
/* 判题汇总横幅 */
|
|
|
.classification-summary {
|
|
|
margin: 12px 0 0;
|
|
|
padding: 8px 12px;
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-danger);
|
|
|
background: rgba(217, 85, 76, 0.08);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.classification-summary.is-success {
|
|
|
color: var(--student-success);
|
|
|
background: rgba(74, 157, 110, 0.08);
|
|
|
}
|
|
|
|
|
|
/* 步骤操作按钮:居中紧凑排列(与标准页 .step-navigation 同规格) */
|
|
|
.task-actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: center;
|
|
|
gap: 14px;
|
|
|
margin-top: 20px;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
* 次按钮(清空填写/本步):白底 + 墨黑半透明描边 + 墨黑字;
|
|
|
* 与标准样例一致:36px 高、8px 圆角、700 字重、hover 轻微浮起。
|
|
|
*/
|
|
|
.btn-nav {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 8px;
|
|
|
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-body);
|
|
|
font-weight: 700;
|
|
|
line-height: 1;
|
|
|
text-shadow: none;
|
|
|
cursor: pointer;
|
|
|
transition: transform 0.16s ease, border-color 0.16s ease, color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
|
|
|
}
|
|
|
|
|
|
.btn-nav:hover:not(:disabled) {
|
|
|
transform: translateY(-1px);
|
|
|
border-color: var(--student-accent);
|
|
|
color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
|
|
|
.btn-nav:active:not(:disabled) {
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
|
|
|
/* 主按钮(保存进入下一步 / 提交任务):墨黑实底白字 + 中性柔影 */
|
|
|
.btn-nav.btn-submit {
|
|
|
border-color: var(--student-accent);
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28);
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border-radius: 8px;
|
|
|
}
|
|
|
|
|
|
.btn-nav.btn-submit:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent-deep);
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent-deep);
|
|
|
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32);
|
|
|
}
|
|
|
|
|
|
.btn-nav:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
transform: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 底部步骤导航:上一步 / 保存 / 下一步
|
|
|
* ============================================================ */
|
|
|
.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-body);
|
|
|
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: var(--student-fs-h2);
|
|
|
}
|
|
|
|
|
|
&: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-body);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.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-body);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: var(--student-fs-h2);
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: #d1d5db;
|
|
|
background: #f3f4f6;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-primary:disabled,
|
|
|
.btn-ghost:disabled,
|
|
|
.btn-save: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;
|
|
|
}
|
|
|
|
|
|
@media (max-width: 1100px) {
|
|
|
.step-nav-right {
|
|
|
margin-left: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
.step-navigation {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
justify-content: center;
|
|
|
}
|
|
|
|
|
|
.btn-primary,
|
|
|
.btn-ghost,
|
|
|
.btn-save {
|
|
|
width: 100%;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 空步骤占位 */
|
|
|
.step-placeholder {
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
min-height: 220px;
|
|
|
padding: 24px;
|
|
|
border: 1px dashed #d1d5db;
|
|
|
border-radius: 12px;
|
|
|
color: #9ca3af;
|
|
|
background: #fafbfc;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.step-placeholder p {
|
|
|
margin: 0 0 6px;
|
|
|
color: #4b5563;
|
|
|
font-size: var(--student-fs-h2);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
/* 步骤门控提示:中性灰底,安静提示 */
|
|
|
.step-warning {
|
|
|
margin: 0 0 12px;
|
|
|
padding: 8px 12px;
|
|
|
border: 1px solid #e5e7eb;
|
|
|
border-left: 3px solid #9ca3af;
|
|
|
border-radius: 8px;
|
|
|
color: #6b7280;
|
|
|
background: #eff6ff;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
/* 排期表列宽 */
|
|
|
.schedule-table {
|
|
|
min-width: 1020px;
|
|
|
}
|
|
|
|
|
|
.schedule-table .stage-cell {
|
|
|
width: 7%;
|
|
|
}
|
|
|
|
|
|
.schedule-table .task-no-cell {
|
|
|
width: 7%;
|
|
|
}
|
|
|
|
|
|
.schedule-table .task-cell {
|
|
|
width: 18%;
|
|
|
}
|
|
|
|
|
|
.classification-cell {
|
|
|
width: 12%;
|
|
|
color: #1f2937;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 700;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.schedule-table th:nth-child(5),
|
|
|
.schedule-table td:nth-child(5),
|
|
|
.schedule-table th:nth-child(6),
|
|
|
.schedule-table td:nth-child(6) {
|
|
|
width: 18%;
|
|
|
}
|
|
|
|
|
|
.schedule-table .dependency-input {
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
.schedule-table .dependency-input::placeholder {
|
|
|
color: #9ca3af;
|
|
|
opacity: 1;
|
|
|
}
|
|
|
|
|
|
.schedule-table th:nth-child(7),
|
|
|
.schedule-table td:nth-child(7) {
|
|
|
width: 20%;
|
|
|
}
|
|
|
|
|
|
.schedule-table input:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.6;
|
|
|
}
|
|
|
|
|
|
/* 甘特图上传区:白底 + 中性虚线,无光晕 */
|
|
|
.gantt-upload-panel {
|
|
|
display: grid;
|
|
|
justify-items: center;
|
|
|
gap: 16px;
|
|
|
min-height: 260px;
|
|
|
padding: 24px 16px;
|
|
|
border: 1px dashed #d1d5db;
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
/* 上传触发器:教育蓝实底主操作 */
|
|
|
.gantt-upload-trigger {
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
align-content: center;
|
|
|
gap: 8px;
|
|
|
width: 240px;
|
|
|
height: 110px;
|
|
|
border-radius: 10px;
|
|
|
color: #ffffff;
|
|
|
background: #2563eb;
|
|
|
cursor: pointer;
|
|
|
transition: background 0.18s ease;
|
|
|
}
|
|
|
|
|
|
.gantt-upload-trigger:hover:not(.disabled) {
|
|
|
background: #1d4ed8;
|
|
|
}
|
|
|
|
|
|
.gantt-upload-trigger input {
|
|
|
position: absolute;
|
|
|
width: 1px;
|
|
|
height: 1px;
|
|
|
opacity: 0;
|
|
|
}
|
|
|
|
|
|
.gantt-upload-trigger .el-icon {
|
|
|
font-size: 22px;
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
|
|
|
.gantt-upload-trigger span {
|
|
|
color: #ffffff;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.gantt-upload-trigger.disabled {
|
|
|
cursor: not-allowed;
|
|
|
background: #9ca3af;
|
|
|
}
|
|
|
|
|
|
.gantt-preview {
|
|
|
width: min(100%, 720px);
|
|
|
margin: 0;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.gantt-preview img {
|
|
|
display: block;
|
|
|
width: 100%;
|
|
|
max-height: 380px;
|
|
|
object-fit: contain;
|
|
|
border: 1px solid #e5e7eb;
|
|
|
border-radius: 8px;
|
|
|
background: #fff;
|
|
|
cursor: zoom-in;
|
|
|
}
|
|
|
|
|
|
.gantt-preview figcaption {
|
|
|
margin-top: 6px;
|
|
|
color: #9ca3af;
|
|
|
font-size: var(--student-fs-caption);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 复盘区 */
|
|
|
.process-reflection {
|
|
|
display: grid;
|
|
|
gap: 18px;
|
|
|
}
|
|
|
|
|
|
.reflection-field {
|
|
|
display: grid;
|
|
|
gap: 8px;
|
|
|
color: #4b5563;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.reflection-field span {
|
|
|
color: #1f2937;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.reflection-field textarea {
|
|
|
width: 100%;
|
|
|
box-sizing: border-box;
|
|
|
min-height: 150px;
|
|
|
resize: vertical;
|
|
|
padding: 10px 12px;
|
|
|
border: 1px solid #d1d5db;
|
|
|
border-radius: 8px;
|
|
|
color: #1f2937;
|
|
|
background: #ffffff;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.reflection-field textarea:focus {
|
|
|
outline: 0;
|
|
|
border-color: #2563eb;
|
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
|
|
}
|
|
|
|
|
|
.reflection-field textarea:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.6;
|
|
|
background: #eff6ff;
|
|
|
}
|
|
|
|
|
|
.reflection-question {
|
|
|
color: #1f2937;
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 600;
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
/* 第 4 步序号徽章:复刻参考稿——实底蓝正方形 + 白字前导零 */
|
|
|
.reflection-no {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
width: 26px;
|
|
|
height: 26px;
|
|
|
margin-right: 10px;
|
|
|
border-radius: 6px;
|
|
|
color: #ffffff;
|
|
|
background: #3b82f6;
|
|
|
font-family: var(--student-font-data);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 600;
|
|
|
line-height: 1;
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
|
|
|
/* 两个场景并排 */
|
|
|
.scenario-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
gap: 18px;
|
|
|
}
|
|
|
|
|
|
.scenario-grid .reflection-field textarea {
|
|
|
min-height: 150px;
|
|
|
}
|
|
|
|
|
|
.process-reflection.disabled {
|
|
|
opacity: 0.7;
|
|
|
}
|
|
|
|
|
|
@media (max-width: 1100px) {
|
|
|
.product-process {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
.product-process {
|
|
|
padding: 12px;
|
|
|
}
|
|
|
|
|
|
.process-core {
|
|
|
padding: 14px;
|
|
|
border-radius: 16px;
|
|
|
}
|
|
|
|
|
|
.step-title-row {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
.step-indicator {
|
|
|
width: 100%;
|
|
|
overflow-x: auto;
|
|
|
}
|
|
|
|
|
|
.step-tab {
|
|
|
flex: 0 0 auto;
|
|
|
}
|
|
|
|
|
|
.step-chip {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.scenario-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
/* 按钮区窄屏全宽的强制覆盖见非 scoped 块 */
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<style lang="scss">
|
|
|
/* ===== AI 侧栏样式已收敛进 TrainingAiSidebar 组件(墨黑极简),此处不再覆盖 ===== */
|
|
|
|
|
|
/* ============================================================
|
|
|
* 步骤导航:胶囊分段 + 连接(教育蓝主题)
|
|
|
* 与 new-product-survey.vue 完全一致,覆盖全局 !important 规则
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.product-process) .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-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:教育蓝边浅底 */
|
|
|
&: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(.product-process) .step-indicator {
|
|
|
justify-content: flex-start !important;
|
|
|
overflow-x: auto !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .step-indicator .step-tab {
|
|
|
flex: 0 0 140px !important;
|
|
|
max-width: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 按钮风格强制同步:教育蓝
|
|
|
* 全局 student-training.scss 用 !important 把 .task-actions 按钮锁成浅蓝大胶囊,
|
|
|
* scoped 规则无法取胜。这里用 id 锚点(特异性含 #app)+ !important 稳定覆盖。
|
|
|
* ============================================================ */
|
|
|
|
|
|
/* Step 02 标题行"导出列表":36px 紧凑小号 */
|
|
|
#app .studentLayout section.app-main:has(.product-process) .step-title-actions button.outline-action {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 5px !important;
|
|
|
width: auto !important;
|
|
|
height: 34px !important;
|
|
|
min-height: 34px !important;
|
|
|
padding: 0 14px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
text-shadow: none !important;
|
|
|
font-size: var(--student-fs-body-sm) !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1 !important;
|
|
|
white-space: nowrap !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .step-title-actions button.outline-action .el-icon {
|
|
|
color: inherit !important;
|
|
|
font-size: var(--student-fs-body-sm) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .step-title-actions button.outline-action:hover {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
transform: none !important;
|
|
|
}
|
|
|
|
|
|
/* --- 底部步骤按钮:36px 标准按钮 --- */
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-actions {
|
|
|
gap: 14px !important;
|
|
|
margin-top: 20px !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-actions>button {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 8px !important;
|
|
|
min-width: 0 !important;
|
|
|
width: auto !important;
|
|
|
height: 34px !important;
|
|
|
min-height: 34px !important;
|
|
|
padding: 0 14px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
text-shadow: none !important;
|
|
|
font-size: var(--student-fs-body) !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-actions>button .el-icon {
|
|
|
display: inline-flex !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-actions>button:hover:not(:disabled) {
|
|
|
transform: translateY(-1px) !important;
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
|
|
|
/* 主按钮:教育蓝实底白字 + 蓝色柔影 */
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-actions>button.btn-submit {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
background: #2563eb !important;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-actions>button.btn-submit:hover:not(:disabled) {
|
|
|
border-color: #1d4ed8 !important;
|
|
|
color: #ffffff !important;
|
|
|
background: #1d4ed8 !important;
|
|
|
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-actions>button:disabled {
|
|
|
cursor: not-allowed !important;
|
|
|
opacity: 0.5 !important;
|
|
|
transform: none !important;
|
|
|
box-shadow: none !important;
|
|
|
}
|
|
|
|
|
|
/* 窄屏:按钮纵向全宽(全局容器 min-width/布局均为 !important,须在此覆盖) */
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .step-title-actions button.outline-action,
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-actions>button {
|
|
|
width: 100% !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-actions {
|
|
|
flex-direction: column !important;
|
|
|
align-items: stretch !important;
|
|
|
}
|
|
|
|
|
|
/* task-action-strip 响应式 */
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-action-strip {
|
|
|
position: static !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-action-strip :deep(.training-task-toolbar) {
|
|
|
justify-content: stretch !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.product-process) .task-action-strip :deep(.training-task-toolbar > *) {
|
|
|
flex: 1 1 auto !important;
|
|
|
}
|
|
|
}
|
|
|
</style>
|