You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/training/GenericTrainingPage.vue

1379 lines
34 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="student-training-page student-training-shell" :class="{ 'product-development-process-page': isProductDevelopmentProcess }">
<main class="training-core">
<section class="task-header">
<div class="task-badge">核心实训 · 产品开发基础</div>
<h1 class="task-title">{{ page.title }}</h1>
</section>
<TrainingTaskBrief :background="page.background" :goals="page.goals" :requirements="page.requirements" :show-material="false" />
<section class="task-card">
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<el-button
class="material-download"
:plain="!page.material?.url"
@click="handleMaterialDownload"
>
<el-icon><Download /></el-icon>
{{ page.material?.name || "案例文档" }}
</el-button>
<el-button class="outline-action" @click="exportOutcome">
<el-icon><Download /></el-icon>
导出实训成果
</el-button>
</div>
</div>
<nav v-if="configuredSteps.length" class="step-indicator" aria-label="实训步骤">
<template v-for="(step, index) in configuredSteps" :key="`${step}-${index}`">
<button
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"
@click="currentStep = index + 1"
>
<span class="step-marker" aria-hidden="true">{{ String(index + 1).padStart(2, "0") }}</span>
<span class="step-copy">
<strong class="step-name" :title="step">{{ step }}</strong>
<span class="step-action">{{ currentStep === index + 1 ? "当前步骤" : "点击切换" }} <span aria-hidden="true"></span></span>
</span>
</button>
<span v-if="index < configuredSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section v-if="processRows.length" class="process-step-card">
<div v-if="configuredSteps.length" class="step-title-row">
<div>
<p class="section-eyebrow">第 {{ String(currentStep).padStart(2, "0") }} 步</p>
<h3>{{ activeStepName }}</h3>
</div>
</div>
<div class="process-training">
<p class="process-intro">{{ page.task }}</p>
<div class="process-table-wrap">
<table class="process-table">
<thead>
<tr>
<th class="serial-col">序号</th>
<th class="name-col">任务名称</th>
<th class="desc-col">任务说明</th>
<th>工作内容</th>
</tr>
</thead>
<tbody>
<tr v-for="row in processRows" :key="row.no">
<td class="serial-cell">{{ row.no }}</td>
<td class="task-name-cell">{{ row.taskName }}</td>
<td class="desc-cell">{{ row.description }}</td>
<td>
<textarea
v-model="row.workContent"
class="process-textarea"
placeholder="填写该阶段需要完成的具体工作、产出物、参与角色和预计周期"
/>
</td>
</tr>
</tbody>
</table>
</div>
<p class="upload-tip">{{ page.processUploadTip }}</p>
<div class="upload-action">
<input ref="uploadInputRef" class="hidden-file-input" type="file" accept="image/*,.xlsx,.xls,.pdf" @change="handleProcessFileChange" />
<button type="button" class="upload-btn" :disabled="uploading || isDemo" @click="triggerUpload">
<el-icon><Upload /></el-icon>
{{ uploading ? "上传中..." : "上传产品开发全流程甘特图" }}
</button>
<span v-if="form.ganttFileName" class="upload-file-name">{{ form.ganttFileName }}</span>
</div>
</div>
</section>
<section v-if="!processRows.length" class="generic-step-card">
<div v-if="configuredSteps.length" class="step-title-row">
<div>
<p class="section-eyebrow">第 {{ String(currentStep).padStart(2, "0") }} 步</p>
<h3>{{ activeStepName }}</h3>
</div>
</div>
<div v-if="page.keyPoints?.length" class="key-point-grid">
<article v-for="item in page.keyPoints" :key="item" class="key-point-card">
{{ item }}
</article>
</div>
<el-form class="training-form" label-position="top">
<div class="form-grid">
<el-form-item v-for="field in page.fields" :key="field.key" :label="field.label">
<el-input
v-if="field.type === 'textarea'"
v-model="form[field.key]"
type="textarea"
:autosize="{ minRows: 4, maxRows: 8 }"
:placeholder="field.placeholder"
/>
<el-input v-else v-model="form[field.key]" :placeholder="field.placeholder" />
</el-form-item>
</div>
</el-form>
<div v-if="workRows.length" class="table-panel">
<div class="panel-title">
<h4>{{ page.tableTitle || "实训工作表" }}</h4>
<p>请结合自己的产品对象,在每一项后填写分析内容,形成可提交的实训成果。</p>
</div>
<el-table :data="workRows" border class="training-table">
<el-table-column label="项目" prop="name" width="190" />
<el-table-column label="说明" prop="description" min-width="280" />
<el-table-column label="学生填写" min-width="360">
<template #default="{ row }">
<el-input v-model="row.answer" type="textarea" :autosize="{ minRows: 3, maxRows: 6 }" placeholder="填写本项分析内容" />
</template>
</el-table-column>
</el-table>
</div>
<div class="training-actions">
<el-button class="training-action training-action--save" :loading="saving" @click="saveCurrentProgress">保存当前进度</el-button>
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">提交任务</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">重新实训</el-button>
</div>
</section>
<div v-if="processRows.length" class="training-actions">
<el-button class="training-action training-action--save" :loading="saving" @click="saveCurrentProgress">保存当前进度</el-button>
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">提交任务</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">重新实训</el-button>
</div>
</section>
</main>
<div class="training-ai-tools">
<TrainingAiSidebar
:task-key="pageKey"
:study-tip="`当前任务建议:围绕“${page.title}”先明确产品对象,再按任务流程逐步补全分析内容。`"
:progress-items="progressItems"
/>
</div>
</div>
</template>
<script setup>
import { Download, Upload } from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { getStudentTrainingPage } from "./studentTrainingPages";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { isStudentDemo } from "@/utils/studentDemo";
const route = useRoute();
const { proxy } = getCurrentInstance();
const pageKey = computed(() => route.meta?.pageKey || route.params?.pageKey);
const isDemo = computed(() => isStudentDemo());
const isProductDevelopmentProcess = computed(() => pageKey.value === "product-development-process");
const remoteTask = ref(null);
const page = computed(() => mergeTrainingPage(getStudentTrainingPage(pageKey.value), remoteTask.value));
const buildInitialForm = () =>
Object.fromEntries((page.value.fields || []).map((field) => [field.key, ""]));
const buildInitialRows = () =>
(page.value.tableRows || []).map(([name, description]) => ({
name,
description,
answer: "",
}));
const buildInitialProcessRows = () =>
(page.value.processRows || []).map((row) => ({
...row,
workContent: "",
}));
const form = ref(buildInitialForm());
const workRows = ref(buildInitialRows());
const processRows = ref(buildInitialProcessRows());
const hasAnyAnswer = computed(() =>
Object.values(form.value).some(hasMeaningfulAnswer) ||
workRows.value.some((row) => hasMeaningfulAnswer(row.answer)) ||
processRows.value.some((row) => hasMeaningfulAnswer(row.workContent))
);
const uploadInputRef = ref(null);
const saving = ref(false);
const uploading = ref(false);
const currentStep = ref(1);
const configuredSteps = computed(() => parseArray(remoteTask.value?.steps));
const activeStepName = computed(() => configuredSteps.value[currentStep.value - 1] || configuredSteps.value[0] || "");
const progressItems = computed(() => [
{ text: `进行中:${page.value.title}`, status: "doing" },
{ text: "待完成:提交实训成果", status: "" },
{ text: "可选:导出实训成果", status: "" },
]);
watch(
() => pageKey.value,
async () => {
await loadRemoteTask();
form.value = buildInitialForm();
workRows.value = buildInitialRows();
processRows.value = buildInitialProcessRows();
await loadSavedAnswer();
},
{ immediate: true }
);
async function loadRemoteTask() {
remoteTask.value = null;
if (!pageKey.value) return;
try {
const res = await getTrainingTaskByKey(pageKey.value);
remoteTask.value = res?.data || null;
} catch (error) {
remoteTask.value = null;
}
}
function mergeTrainingPage(basePage, task) {
if (!task) {
return {
...basePage,
requirements: basePage.requirements || basePage.task,
};
}
const goals = parseArray(task.objectives);
const flow = parseArray(task.steps);
const material = normalizeMaterial(task.materialName, task.materialUrl);
return {
...basePage,
title: task.taskName || basePage.title,
background: task.background || basePage.background,
goals: goals.length ? goals : basePage.goals,
requirements: task.requirements || basePage.task,
task: basePage.processRows?.length ? basePage.task : task.requirements || basePage.task,
flow: flow.length ? flow : basePage.flow,
material,
};
}
function normalizeMaterial(materialName, materialUrl) {
const name = String(materialName || "").trim();
const url = String(materialUrl || "").trim();
if (!name || !url || (name === "案例资料.rar" && url === "/file/example.rar")) {
return null;
}
return { name, url };
}
function parseArray(value) {
if (Array.isArray(value)) {
return value.map((item) => String(item?.name ?? item ?? "").trim()).filter(Boolean);
}
if (!value) {
return [];
}
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed.map((item) => String(item?.name ?? item ?? "").trim()).filter(Boolean) : [];
} catch (error) {
return String(value)
.split(/[;|]/)
.map((item) => item.trim())
.filter(Boolean);
}
}
function hasMeaningfulAnswer(value) {
if (typeof value === "string") return value.trim().length > 0;
return value !== null && value !== undefined && value !== "";
}
function buildOutcome() {
return {
title: page.value.title,
form: form.value,
rows: workRows.value,
processRows: processRows.value,
file: form.value.ganttFileUrl
? {
name: form.value.ganttFileName || "",
url: form.value.ganttFileUrl,
originalFilename: form.value.ganttOriginalFilename || form.value.ganttFileName || "",
}
: null,
};
}
function applySavedAnswer(value) {
if (!value) return;
try {
const parsed = JSON.parse(value);
if (parsed?.currentStep && Number(parsed.currentStep) > 0) {
currentStep.value = Number(parsed.currentStep);
}
if (parsed?.form) {
form.value = {
...form.value,
...parsed.form,
};
}
if (Array.isArray(parsed?.rows) && parsed.rows.length) {
workRows.value = buildInitialRows().map((row, index) => ({
...row,
answer: parsed.rows[index]?.answer || "",
}));
}
if (Array.isArray(parsed?.processRows) && parsed.processRows.length) {
const savedByNo = parsed.processRows.reduce((map, row) => {
if (row?.no != null) {
map[row.no] = row;
}
return map;
}, {});
processRows.value = buildInitialProcessRows().map((row, index) => ({
...row,
workContent: savedByNo[row.no]?.workContent || parsed.processRows[index]?.workContent || "",
}));
}
} catch (error) {
// Ignore malformed historical answers.
}
}
async function loadSavedAnswer() {
if (!pageKey.value) return;
try {
const res = await getStudentTrainingAnswer(pageKey.value);
const answer = res?.data;
if (answer?.currentStep && Number(answer.currentStep) > 0) {
currentStep.value = Number(answer.currentStep);
}
if (answer?.step1Answer) {
applySavedAnswer(answer.step1Answer);
}
} catch (error) {
// Keep the page usable when the answer endpoint is unavailable.
}
}
function buildAnswerPayload(saveAction = "SAVE") {
return {
step1Answer: JSON.stringify(buildOutcome()),
currentStep: currentStep.value,
submitted: saveAction === "SUBMIT",
saveAction,
};
}
async function saveAnswer(saveAction = "SAVE") {
if (!pageKey.value || saving.value) return;
saving.value = true;
try {
await saveStudentTrainingAnswer(pageKey.value, buildAnswerPayload(saveAction));
proxy?.$modal?.msgSuccess(saveAction === "SUBMIT" ? "提交成功" : "已保存");
} catch (error) {
proxy?.$modal?.msgError?.(saveAction === "SUBMIT" ? "提交失败" : "保存失败");
} finally {
saving.value = false;
}
}
function saveCurrentProgress() {
return saveAnswer("SAVE");
}
function submitTask() {
return saveAnswer("SUBMIT");
}
function handleMaterialDownload() {
const material = page.value.material;
if (!material?.url) {
proxy?.$modal?.msgWarning?.("暂无案例文档");
return;
}
const link = document.createElement("a");
link.href = material.url;
link.target = "_blank";
link.rel = "noopener";
if (material.name) {
link.download = material.name;
}
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
async function resetTraining() {
form.value = buildInitialForm();
workRows.value = buildInitialRows();
processRows.value = buildInitialProcessRows();
try {
await saveAnswer("RESET");
} catch (error) {
// saveAnswer already reports failures.
}
}
function triggerUpload() {
if (isDemo.value) { proxy?.$modal?.msgWarning?.("演示模式不上传文件"); return; }
uploadInputRef.value?.click();
}
async function handleProcessFileChange(event) {
const file = event.target.files?.[0];
if (!file) return;
const formData = new FormData();
formData.append("file", file);
uploading.value = true;
try {
const res = await uploadStudentTrainingFile(formData);
form.value = {
...form.value,
ganttFileName: res?.originalFilename || file.name,
ganttOriginalFilename: res?.originalFilename || file.name,
ganttFileUrl: res?.url || res?.fileName || "",
};
proxy?.$modal?.msgSuccess("文件已上传");
} catch (error) {
proxy?.$modal?.msgError?.("文件上传失败");
} finally {
uploading.value = false;
if (event.target) {
event.target.value = "";
}
}
}
function exportOutcome() {
const blob = new Blob([JSON.stringify(buildOutcome(), null, 2)], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${page.value.title}-实训成果.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
proxy?.$modal?.msgSuccess("导出成功");
}
</script>
<style lang="scss" scoped>
.student-training-page {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: minmax(720px, 1fr) 340px;
gap: 24px;
width: 100%;
max-width: 100%;
min-height: calc(100vh - 106px);
margin: 0;
padding: 24px 24px 32px;
color: #e8f7ff;
background: #06111d;
}
.training-core,
.ai-card {
border: 1px solid rgba(0, 180, 255, 0.25);
background: rgba(8, 18, 28, 0.6);
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.22), inset 0 1px 0 rgba(169, 229, 255, 0.08);
backdrop-filter: blur(8px);
}
.training-ai-tools {
display: flex;
flex-direction: column;
gap: 22px;
min-width: 0;
}
.training-core {
padding: 28px;
border-radius: 36px;
}
.task-header {
margin-bottom: 24px;
}
.task-badge {
display: inline-flex;
margin-bottom: 12px;
padding: 4px 14px;
border: 1px solid rgba(0, 170, 255, 0.5);
border-radius: 30px;
color: #d6f6ff;
background: rgba(0, 170, 255, 0.2);
font-size: 12px;
font-weight: 700;
}
.task-title {
margin: 0;
color: transparent;
background: linear-gradient(135deg, #ffffff, #8bcbff);
-webkit-background-clip: text;
background-clip: text;
font-size: 30px;
line-height: 1.3;
font-weight: 900;
}
.goal-bg {
margin-bottom: 28px;
padding: 16px 20px;
border: 1px solid #2d5f7e;
border-radius: 24px;
background: rgba(0, 35, 55, 0.6);
p,
li {
color: #d7ecff;
font-size: 14px;
line-height: 1.8;
}
p,
ol {
margin: 0;
}
ol {
padding-left: 18px;
}
}
.subtitle-icon {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
color: #b9e2ff;
font-weight: 800;
}
.subtitle-icon--spaced {
margin-top: 12px;
}
.task-card,
.generic-step-card,
.process-step-card {
padding: 18px 22px;
border: 1px solid rgba(82, 174, 226, 0.42);
border-radius: 26px;
background: rgba(0, 25, 42, 0.64);
}
.task-card {
margin-top: 0;
}
.generic-step-card,
.process-step-card {
margin-top: 32px;
}
.training-form,
.table-panel {
margin: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
.workbench-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 18px;
margin-bottom: 22px;
padding-bottom: 18px;
border-bottom: 1px solid rgba(0, 180, 255, 0.2);
h3 {
margin: 4px 0 8px;
color: #ffffff;
font-size: 26px !important;
font-weight: 800 !important;
line-height: 1.3 !important;
}
p {
margin: 0;
color: #d7ecff;
font-size: 15px;
font-weight: 400;
line-height: 1.8;
}
}
.workbench-actions {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
margin-left: auto;
justify-content: flex-end;
gap: 10px;
}
.material-download {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 260px;
min-height: 32px;
padding: 0 12px;
border: 1px solid rgba(97, 219, 255, 0.55);
border-radius: 6px;
color: #a9ecff;
background: rgba(7, 54, 78, 0.72);
font-size: 13px;
font-weight: 800;
line-height: 1.2;
text-decoration: none;
&:hover {
color: #ffffff;
border-color: rgba(119, 232, 255, 0.92);
background: rgba(10, 82, 116, 0.82);
}
}
.section-eyebrow {
margin: 0;
color: #71dfff;
font-size: 12px;
font-weight: 800;
line-height: 1.4;
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
overflow-x: auto;
scrollbar-width: thin;
}
.step-tab {
display: flex;
align-items: center;
flex: 1 0 145px;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font-family: inherit;
cursor: pointer;
text-align: left;
transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
&:hover {
border-color: rgba(89, 223, 255, 0.82);
color: #ffffff;
background: rgba(8, 92, 131, 0.52);
box-shadow: 0 7px 18px rgba(0, 0, 0, 0.24), inset 0 1px 0 rgba(152, 239, 255, 0.16);
transform: translateY(-2px);
.step-action {
color: #9bf4ff;
}
}
&:focus-visible {
outline: 2px solid #72e8ff;
outline-offset: 4px;
}
&.active {
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
.step-marker {
border-color: #72e8ff;
color: #ffffff;
background: linear-gradient(135deg, #08b7e8, #1375d0);
box-shadow: 0 0 0 5px rgba(25, 179, 237, 0.14), 0 0 18px rgba(46, 208, 255, 0.58);
}
}
&.complete {
color: #dffbff;
.step-marker {
border-color: rgba(84, 230, 221, 0.85);
color: #062b37;
background: #6ef0e1;
box-shadow: 0 0 12px rgba(79, 231, 218, 0.35);
}
}
}
.step-marker {
display: inline-flex;
flex: 0 0 38px;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border: 1px solid rgba(101, 176, 208, 0.6);
border-radius: 50%;
color: #a6c4d9;
background: rgba(3, 25, 40, 0.8);
box-shadow: inset 0 0 12px rgba(38, 174, 233, 0.08);
font-size: 12px;
font-weight: 900;
letter-spacing: 0.04em;
transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.step-copy {
display: grid;
gap: 4px;
min-width: 0;
margin-left: 10px;
}
.step-name {
display: -webkit-box;
overflow: hidden;
color: inherit;
font-size: 14px;
font-weight: 800;
line-height: 1.25;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.step-action {
color: #66b6d4;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.06em;
line-height: 1;
transition: color 0.2s ease;
}
.step-connector {
position: relative;
flex: 0 1 40px;
min-width: 18px;
height: 2px;
margin: 0 8px;
background: rgba(98, 169, 201, 0.34);
}
.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-tab.complete + .step-connector {
background: linear-gradient(90deg, #6ef0e1, #1cb8e9);
box-shadow: 0 0 9px rgba(53, 218, 235, 0.46);
}
.step-tab.complete + .step-connector::after {
border-color: #63e6f1;
}
.step-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 14px;
h3 {
margin: 4px 0 0;
color: #ffffff;
font-size: 22px;
font-weight: 900;
line-height: 1.3;
}
}
.flow-rail {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin-top: 18px;
}
.process-training {
margin-top: 4px;
}
.process-intro,
.upload-tip {
margin: 0 0 18px;
color: #d7ecff;
font-size: 15px;
line-height: 1.8;
}
.process-table-wrap {
overflow-x: auto;
border: 1px solid rgba(82, 174, 226, 0.45);
border-radius: 20px;
background: rgba(0, 13, 24, 0.68);
}
.process-table {
width: 100%;
min-width: 1080px;
border-collapse: collapse;
table-layout: fixed;
th {
padding: 14px 16px;
border-right: 1px solid rgba(134, 220, 255, 0.24);
color: #effdff;
background: linear-gradient(180deg, #0b7fb3, #075c88);
font-size: 16px;
font-weight: 900;
text-align: center;
}
td {
padding: 16px;
border-top: 1px solid rgba(87, 159, 200, 0.38);
border-right: 1px solid rgba(87, 159, 200, 0.28);
vertical-align: middle;
}
th:last-child,
td:last-child {
border-right: 0;
}
}
.serial-col {
width: 72px;
}
.name-col {
width: 190px;
}
.desc-col {
width: 280px;
}
.serial-cell,
.task-name-cell {
color: #eafaff;
font-size: 16px;
font-weight: 800;
text-align: center;
}
.desc-cell {
color: #d1e8f5;
font-size: 14px;
line-height: 1.75;
text-align: center;
}
.process-textarea {
width: 100%;
min-height: 112px;
padding: 12px 14px;
border: 1px solid rgba(84, 177, 230, 0.62);
border-radius: 18px;
outline: none;
resize: vertical;
color: #f4fbff;
background: rgba(9, 24, 36, 0.94);
line-height: 1.65;
transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
&::placeholder {
color: #6f8da3;
}
&:focus {
border-color: #5fe7ff;
background: rgba(10, 31, 46, 0.98);
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
.upload-tip {
margin-top: 26px;
margin-bottom: 22px;
}
.upload-action {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 14px;
}
.hidden-file-input {
display: none;
}
.upload-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 46px;
padding: 0 26px;
border: 1px solid rgba(61, 221, 255, 0.9);
border-radius: 12px;
color: #ffffff;
background: linear-gradient(95deg, #10a7d5, #0878af);
box-shadow: 0 12px 24px rgba(0, 138, 220, 0.24);
font-size: 15px;
font-weight: 900;
cursor: pointer;
}
.upload-file-name {
color: #9fe8ff;
font-size: 13px;
font-weight: 700;
}
.flow-step,
.key-point-card {
border: 1px solid rgba(75, 184, 232, 0.24);
border-radius: 24px;
background: rgba(0, 20, 35, 0.65);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.flow-step {
min-height: 86px;
padding: 18px 16px;
span {
display: block;
margin-bottom: 8px;
color: #69ddff;
font-size: 18px;
font-weight: 900;
}
strong {
color: #f1fbff;
font-size: 15px;
line-height: 1.5;
}
}
.key-point-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin: 0 0 20px;
}
.key-point-card {
padding: 16px;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 20px;
color: #d7ecff;
background: rgba(1, 14, 26, 0.72);
font-size: 14px;
font-weight: 700;
line-height: 1.7;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.panel-title {
margin: 22px 0 16px;
h4 {
margin: 0 0 8px;
color: #ffffff;
font-size: 18px;
}
p {
margin: 0;
color: #d7ecff;
font-size: 15px;
line-height: 1.8;
}
}
:deep(.el-form-item__label) {
color: #dff5ff;
font-size: 15px;
font-weight: 800;
line-height: 1.8;
}
:deep(.el-input__wrapper),
:deep(.el-textarea__inner) {
border: 1px solid #2d5f7e;
border-radius: 20px;
box-shadow: none;
background: #0f1a24;
}
:deep(.el-textarea__inner) {
padding: 12px 16px;
}
:deep(.el-input__inner),
:deep(.el-textarea__inner) {
color: #f2fbff;
}
:deep(.el-input__inner::placeholder),
:deep(.el-textarea__inner::placeholder) {
color: #6f8da3;
}
.training-table {
--el-table-border-color: rgba(77, 167, 220, 0.24);
--el-table-header-bg-color: rgba(8, 55, 82, 0.98);
--el-table-header-text-color: #eaffff;
--el-table-row-hover-bg-color: rgba(21, 107, 151, 0.42);
--el-table-bg-color: rgba(1, 14, 26, 0.72);
--el-table-tr-bg-color: rgba(1, 14, 26, 0.72);
--el-table-text-color: #d7f1ff;
border-radius: 22px;
overflow: hidden;
}
.training-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 52px;
margin-top: 36px;
}
.training-actions :deep(.training-action) {
min-width: 172px;
height: 64px;
padding: 0 28px;
border: 1px solid #2c83aa;
border-radius: 6px;
color: #dff7ff;
background: rgba(13, 64, 88, 0.62);
box-shadow: none;
font-size: 24px;
font-weight: 900;
line-height: 1;
letter-spacing: 0;
text-shadow: none;
}
.training-actions :deep(.training-action span) {
display: inline-flex;
align-items: center;
color: inherit;
font-size: inherit;
line-height: 1;
}
.training-actions :deep(.training-action--submit),
.training-actions :deep(.training-action--reset) {
border-color: #2c83aa;
color: #dff7ff;
background: rgba(13, 64, 88, 0.62);
}
.training-actions :deep(.training-action:hover:not(.is-disabled)) {
border-color: #46b5e7;
color: #ffffff;
background: rgba(18, 85, 116, 0.78);
}
.product-development-process-page {
.goal-bg {
margin-bottom: 28px;
padding: 18px 22px;
border: 1px solid rgba(82, 174, 226, 0.42);
border-radius: 26px;
background: rgba(0, 25, 42, 0.64);
}
.task-card {
margin-top: 0;
padding: 18px 22px;
border: 1px solid rgba(82, 174, 226, 0.42);
border-radius: 26px;
background: rgba(0, 25, 42, 0.64);
}
.workbench-head h3 {
font-size: 26px !important;
font-weight: 800 !important;
line-height: 1.3 !important;
}
.workbench-head .section-eyebrow {
color: #71dfff;
font-size: 12px;
font-weight: 800;
line-height: 1.4;
}
.workbench-description {
color: #d7ecff !important;
font-size: 15px !important;
font-weight: 400;
line-height: 1.8 !important;
}
.workbench-actions :deep(.material-download),
.workbench-actions :deep(.outline-action) {
min-width: 0 !important;
min-height: 40px !important;
height: 40px !important;
padding: 0 18px !important;
border: 1px solid rgba(99, 217, 255, 0.55) !important;
border-radius: 999px !important;
color: #e9fbff !important;
background: rgba(17, 126, 178, 0.36) !important;
box-shadow: none !important;
font-size: 14px !important;
font-weight: 800 !important;
line-height: 1 !important;
}
.process-step-card {
padding: 18px 22px;
border: 1px solid rgba(82, 174, 226, 0.42);
border-radius: 26px;
background: rgba(0, 25, 42, 0.64);
}
.step-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 14px;
h3 {
margin: 4px 0 0;
color: #ffffff;
font-size: 22px;
font-weight: 900;
line-height: 1.3;
}
}
.process-intro,
.upload-tip {
color: #d7ecff;
font-size: 15px;
line-height: 1.8;
}
.process-table-wrap {
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 22px;
background: rgba(1, 14, 26, 0.72);
}
.process-table {
th {
padding: 12px;
border-right-color: rgba(77, 167, 220, 0.2);
background: rgba(8, 55, 82, 0.98);
font-size: 14px;
}
td {
padding: 12px;
border-top-color: rgba(77, 167, 220, 0.2);
border-right-color: rgba(77, 167, 220, 0.2);
background: rgba(1, 14, 26, 0.48);
vertical-align: top;
}
}
.serial-cell,
.task-name-cell,
.desc-cell {
font-size: 14px;
}
.process-textarea {
min-height: 110px;
padding: 12px 14px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
color: #eef5ff;
background: #0f1a24;
}
.upload-action {
justify-content: flex-start;
}
.upload-btn {
min-width: 0 !important;
min-height: 40px !important;
height: 40px !important;
padding: 0 18px !important;
border: 1px solid rgba(92, 224, 255, 0.7) !important;
border-radius: 999px !important;
background: linear-gradient(95deg, #0b84bd, #096491) !important;
box-shadow: none !important;
font-size: 14px !important;
font-weight: 800 !important;
}
.upload-file-name {
color: #9fe8ff;
font-size: 13px;
font-weight: 700;
}
}
@media (max-width: 1100px) {
.student-training-page {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.student-training-page {
margin: 0;
padding: 14px 14px 18px;
}
.training-core {
padding: 18px;
border-radius: 24px;
}
.workbench-head,
.workbench-actions,
.step-title-row {
flex-direction: column;
align-items: stretch;
}
.workbench-actions :deep(.material-download),
.workbench-actions :deep(.outline-action) {
width: 100%;
}
.workbench-head {
flex-direction: column;
}
.form-grid {
grid-template-columns: 1fr;
}
.step-indicator {
width: 100%;
margin: 0 0 32px;
padding: 16px;
overflow: visible;
flex-direction: column;
align-items: stretch;
border-radius: 20px;
}
.step-tab {
flex: 0 0 auto;
width: 100%;
min-width: 0;
}
.step-connector {
flex: 0 0 20px;
width: 2px;
min-width: 0;
height: 20px;
align-self: flex-start;
margin: 4px 0 4px 18px;
}
.step-connector::after {
top: auto;
right: auto;
bottom: -1px;
left: 50%;
transform: translateX(-50%) rotate(135deg);
}
.product-development-process-page {
.workbench-actions,
.step-title-row,
.upload-action {
flex-direction: column;
align-items: stretch;
}
.upload-btn {
width: 100%;
}
}
.training-actions {
flex-direction: column;
align-items: stretch;
gap: 14px;
}
.training-actions :deep(.training-action) {
width: 100%;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>