diff --git a/src/api/studentTrainingAnswer.js b/src/api/studentTrainingAnswer.js index 6339be2..21973cd 100644 --- a/src/api/studentTrainingAnswer.js +++ b/src/api/studentTrainingAnswer.js @@ -1883,7 +1883,7 @@ export function checkBusinessFeasibilityStepThree(payload) { if (!reportUrl || !supported.test(reportUrl) && !supported.test(reportName)) return Promise.reject(new Error("请上传 PDF、Word、PNG 或 JPG 格式的可行性论证报告")); return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } }); } - return request({ url: "/api/student-training-answers/business-feasibility/step-3/validate", method: "post", data: { reportUrl, reportName }, headers: { repeatSubmit: false } }); + return request({ url: "/api/student-training-answers/business-feasibility/step-3/validate", method: "post", data: { reportUrl, reportName }, headers: { repeatSubmit: false }, skipErrorMessage: true }); } export function checkCompetitiveEnvironmentAnalysisStepOne(payload) { diff --git a/src/utils/request.js b/src/utils/request.js index 09fc5be..cc4b383 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -92,9 +92,11 @@ service.interceptors.response.use( } else if (code === 601) { ElMessage({ message: msg, type: "warning" }); return Promise.reject(new Error(msg)); - } else if (code !== 200) { - ElNotification.error({ title: msg }); - return Promise.reject("error"); + } else if (code !== 200) { + if (!res.config?.skipErrorMessage) { + ElNotification.error({ title: msg }); + } + return Promise.reject(new Error(msg)); } else { return Promise.resolve(res.data); } diff --git a/src/views/assessment/index.vue b/src/views/assessment/index.vue index cb3ba88..f5edae7 100644 --- a/src/views/assessment/index.vue +++ b/src/views/assessment/index.vue @@ -55,6 +55,9 @@

+
+ +

功效系数法:绩效评估的标准化工具‌,将各指标数据通过标准化处理消除量纲差异,转化为“功效值”后加权计算综合得分,适用于多维度绩效评估‌,可用于分析电商产品表现。

@@ -198,6 +201,7 @@ import TrainingLegacyActions from "@/views/components/TrainingLegacyActions.vue"; import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; +import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"; import TrainingIntro from '@/views/components/TrainingIntro.vue'; import useUserStore from "@/store/modules/user"; const userStore = useUserStore(); @@ -433,4 +437,3 @@ onMounted(() => { - diff --git a/src/views/components/TrainingTaskBrief.vue b/src/views/components/TrainingTaskBrief.vue index ae1052b..affb0d6 100644 --- a/src/views/components/TrainingTaskBrief.vue +++ b/src/views/components/TrainingTaskBrief.vue @@ -36,8 +36,7 @@ 实训要求
- import { Aim, Clock, DataAnalysis } from "@element-plus/icons-vue"; -import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; +import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"; const activeTab = ref("background"); @@ -93,7 +92,7 @@ const props = defineProps({ default: true, }, }); -const { showMaterial, materialName, materialUrl } = toRefs(props); +const { materialName, materialUrl } = toRefs(props); const displayBackground = computed(() => props.background || "请根据教师端配置的实训背景理解本次任务场景。"); const displayGoals = computed(() => normalizeGoals(props.goals)); diff --git a/src/views/components/TrainingTaskToolbar.vue b/src/views/components/TrainingTaskToolbar.vue new file mode 100644 index 0000000..422af1f --- /dev/null +++ b/src/views/components/TrainingTaskToolbar.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/src/views/demand/business-feasibility.vue b/src/views/demand/business-feasibility.vue index 6e8e25f..6e8c8e4 100644 --- a/src/views/demand/business-feasibility.vue +++ b/src/views/demand/business-feasibility.vue @@ -174,7 +174,7 @@ async function saveStepTwo() { if (saving.value) return; saving.value = true; tr function resetStepTwo() { stepTwo.value = createStepTwo(); stepTwoCompleted.value = false; proxy?.$modal?.msgSuccess("已清空第 2 步填写内容"); } function handleDecisionChange() { if (stepTwo.value.decision === "not-feasible") stepTwo.value.priorities = []; } function openReportPicker() { reportInput.value?.click(); } -async function handleReportUpload(event) { const file = event?.target?.files?.[0]; if (event?.target) event.target.value = ""; if (!file) return; const extension = file.name.split(".").pop()?.toLowerCase(); if (!["pdf", "doc", "docx", "png", "jpg", "jpeg"].includes(extension)) { proxy?.$modal?.msgError("仅支持 PDF、Word、PNG、JPG 格式的报告附件"); return; } if (file.size > 20 * 1024 * 1024) { proxy?.$modal?.msgError("报告附件不能超过 20MB"); return; } uploading.value = true; try { const data = new FormData(); data.append("file", file); const response = await uploadStudentTrainingFile(data); const url = typeof response?.data === "string" ? response.data : response?.data?.url; if (!url) throw new Error("上传结果缺少文件地址"); report.value = { url, name: file.name, size: file.size }; proxy?.$modal?.msgSuccess("报告附件上传成功,可点击预览核对"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "报告附件上传失败,请重试"); } finally { uploading.value = false; } } +async function handleReportUpload(event) { const file = event?.target?.files?.[0]; if (event?.target) event.target.value = ""; if (!file) return; const extension = file.name.split(".").pop()?.toLowerCase(); if (!["pdf", "doc", "docx", "png", "jpg", "jpeg"].includes(extension)) { proxy?.$modal?.msgError("仅支持 PDF、Word、PNG、JPG 格式的报告附件"); return; } if (file.size > 20 * 1024 * 1024) { proxy?.$modal?.msgError("报告附件不能超过 20MB"); return; } uploading.value = true; try { const data = new FormData(); data.append("file", file); const response = await uploadStudentTrainingFile(data); const url = response?.url || response?.data?.url || ""; if (!url) throw new Error("上传未返回文件地址,请重新选择报告附件"); report.value = { url, name: file.name, size: file.size }; proxy?.$modal?.msgSuccess("报告附件上传成功,可点击预览核对"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "报告附件上传失败,请重试"); } finally { uploading.value = false; } } function resolveReportKind(file) { const name = `${file?.name || ""} ${file?.url || ""}`.toLowerCase(); if (/\.(png|jpe?g)(?:\?|$)/.test(name)) return "image"; if (/\.pdf(?:\?|$)/.test(name)) return "pdf"; return "word"; } function readableSize(size) { const value = Number(size || 0); return value > 1024 * 1024 ? `${(value / 1024 / 1024).toFixed(1)} MB` : value > 1024 ? `${(value / 1024).toFixed(1)} KB` : "附件已上传"; } function openReportPreview() { if (report.value.url) reportPreviewVisible.value = true; } diff --git a/src/views/student/index.vue b/src/views/student/index.vue index 4c3aea5..8adfef2 100644 --- a/src/views/student/index.vue +++ b/src/views/student/index.vue @@ -31,7 +31,7 @@
- + @@ -136,7 +136,8 @@ const renderScoreChart = () => { radius: ["42%", "72%"], center: ["34%", "50%"], avoidLabelOverlap: true, - label: { formatter: "{b}" }, + label: { show: false }, + labelLine: { show: false }, data: displayModules.value.map((module, index) => ({ name: module.moduleName, value: Number(module.score) || 0, @@ -252,7 +253,7 @@ onBeforeUnmount(() => { small { color: #8a98af; font-size: 13px; } } -.score-layout { display: grid; grid-template-columns: minmax(400px, 0.9fr) minmax(540px, 1.3fr); min-height: 330px; } +.score-layout { display: grid; grid-template-columns: minmax(520px, 0.95fr) minmax(760px, 1.35fr); min-height: 330px; } .score-visual { position: relative; @@ -277,8 +278,9 @@ onBeforeUnmount(() => { } .summary-label { margin-bottom: 4px; font-size: 13px; } -.score-table-wrap { display: flex; align-items: center; min-width: 0; padding: 8px 0 8px 24px; } -.score-table { width: 100%; } +.score-table-wrap { display: flex; align-items: center; min-width: 0; overflow-x: auto; padding: 8px 0 8px 24px; } +.score-table { width: 100%; min-width: 760px; } +.score-table :deep(.el-table__cell) { white-space: nowrap; } .progress-layout { display: grid; grid-template-columns: 190px minmax(370px, 1fr) minmax(320px, 0.9fr); gap: 24px; align-items: stretch; } diff --git a/tests/business-feasibility-report-upload.static.test.cjs b/tests/business-feasibility-report-upload.static.test.cjs new file mode 100644 index 0000000..289977d --- /dev/null +++ b/tests/business-feasibility-report-upload.static.test.cjs @@ -0,0 +1,35 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); + +const source = fs.readFileSync( + path.resolve(__dirname, "../src/views/demand/business-feasibility.vue"), + "utf8", +); +const api = fs.readFileSync( + path.resolve(__dirname, "../src/api/studentTrainingAnswer.js"), + "utf8", +); +const request = fs.readFileSync( + path.resolve(__dirname, "../src/utils/request.js"), + "utf8", +); + +test("商业可行性报告上传兼容公共上传接口的顶层文件地址", () => { + assert.match( + source, + /const url = response\?\.url \|\| response\?\.data\?\.url \|\| ""/, + ); +}); + +test("商业可行性报告校验失败仅交由页面显示中文提示", () => { + assert.match( + api, + /business-feasibility\/step-3\/validate[\s\S]{0,240}skipErrorMessage:\s*true/, + ); + assert.match( + request, + /code !== 200[\s\S]{0,260}skipErrorMessage[\s\S]{0,260}Promise\.reject\(new Error\(msg\)\)/, + ); +}); diff --git a/tests/student-home-score-layout.static.test.cjs b/tests/student-home-score-layout.static.test.cjs new file mode 100644 index 0000000..9c6aef4 --- /dev/null +++ b/tests/student-home-score-layout.static.test.cjs @@ -0,0 +1,17 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); +const assert = require("node:assert/strict"); + +const source = fs.readFileSync(path.resolve(__dirname, "../src/views/student/index.vue"), "utf8"); + +test("首页成绩环图不再同时展示会与图例重叠的外部标签", () => { + assert.match(source, /label:\s*\{\s*show:\s*false\s*\}/); + assert.match(source, /legend:\s*\{\s*orient:\s*"vertical"/); +}); + +test("首页成绩表的数值保持单行,并在窄空间内横向滚动", () => { + assert.match(source, /\.score-table-wrap[^}]*overflow-x:\s*auto/); + assert.match(source, /\.score-table[^}]*min-width:/); + assert.match(source, /\.score-table\s*:deep\([^)]*\)\s*\{[^}]*white-space:\s*nowrap/); +}); diff --git a/tests/training-task-toolbar.static.test.cjs b/tests/training-task-toolbar.static.test.cjs new file mode 100644 index 0000000..e52d65a --- /dev/null +++ b/tests/training-task-toolbar.static.test.cjs @@ -0,0 +1,57 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); +const assert = require("node:assert/strict"); + +const frontendRoot = path.resolve(__dirname, ".."); +const toolbarPath = path.join(frontendRoot, "src/views/components/TrainingTaskToolbar.vue"); + +const studentTrainingPages = [ + "foundation/new-product-survey.vue", + "foundation/product-development-factors.vue", + "foundation/product-development-process.vue", + "demand/industry-demand-analysis.vue", + "demand/competitive-environment-analysis.vue", + "demand/business-feasibility.vue", + "demand/people.vue", + "demand/consumer-behavior.vue", + "demand/consumer-scenario.vue", + "demand/feature-conversion.vue", + "product/market-opportunity-selection.vue", + "product/differentiation.vue", + "product/structure.vue", + "product/after-sales-service.vue", + "product/requirements-doc.vue", + "product/hardware-cost.vue", + "product/price.vue", + "test/index.vue", + "test/promotion.vue", + "channel/index.vue", + "channel/supplier.vue", + "assessment/index.vue", + "assessment/diagnosis.vue", + "assessment/optimization.vue", +]; + +test("统一实训工具栏同时提供案例下载、提交、重新实训和导出入口", () => { + const source = fs.readFileSync(toolbarPath, "utf8"); + + assert.match(source, /TrainingMaterialButton/); + assert.match(source, /提交任务/); + assert.match(source, /重新实训/); + assert.match(source, /导出实训成果/); +}); + +test("每个学生端实训菜单均通过任务说明区接入统一顶部工具栏", () => { + for (const relativePath of studentTrainingPages) { + const source = fs.readFileSync(path.join(frontendRoot, "src/views", relativePath), "utf8"); + if (relativePath === "assessment/index.vue") { + assert.match(source, /