From 802bd35593b22bcc90b3dc024479dd62e6b06335 Mon Sep 17 00:00:00 2001 From: qinzhenpen Date: Mon, 28 Sep 2026 10:16:33 +0800 Subject: [PATCH 1/4] =?UTF-8?q?style:=20=E6=9B=B4=E6=96=B0=E5=AD=A6?= =?UTF-8?q?=E7=94=9F=E7=AB=AF=E5=88=86=E9=A1=B5=E6=A0=B7=E5=BC=8F=E4=B8=BA?= =?UTF-8?q?=E7=A7=91=E6=8A=80=E8=93=9D=EF=BC=8C=E4=BC=98=E5=8C=96=E8=AF=84?= =?UTF-8?q?=E5=88=86=E8=A1=A8=E6=A0=BC=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优化了评分表格的结构和样式,调整了各项评分的计算方式,确保视觉一致性和用户体验 --- src/assets/styles/index.scss | 46 +++++++++---- src/views/channel/supplier.vue | 118 ++++++++++++++++++++------------- 2 files changed, 105 insertions(+), 59 deletions(-) diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss index 8e559e7..15b8255 100644 --- a/src/assets/styles/index.scss +++ b/src/assets/styles/index.scss @@ -496,13 +496,13 @@ aside { // color: #eef5ff !important; // } -/* === 学生端分页:墨黑极简(覆盖上方裸深色 !important 规则) === */ +/* === 学生端分页:科技蓝(与学生端亮色主题一致,覆盖上方裸深色 !important 规则) === */ #app .studentLayout .pagination-container .el-pagination, #app .studentLayout .el-pagination { --el-pagination-bg-color: transparent !important; --el-pagination-button-bg-color: #ffffff !important; - --el-pagination-hover-color: #1f2937 !important; - --el-pagination-button-disabled-bg-color: #ffffff !important; + --el-pagination-hover-color: #2563eb !important; + --el-pagination-button-disabled-bg-color: #f9fafb !important; --el-pagination-button-disabled-color: #9ca3af !important; color: #4b5563 !important; } @@ -517,21 +517,30 @@ aside { border-radius: 8px !important; background-color: #ffffff !important; color: #374151 !important; - font-weight: 600; + font-weight: 500; } +/* 悬浮:软蓝浅底 + 蓝字蓝边 */ #app .studentLayout .pagination-container .el-pagination .el-pager li:hover, #app .studentLayout .el-pager li:hover, -#app .studentLayout .el-pagination button:hover { - border-color: #1f2937 !important; - background-color: #f9fafb !important; - color: #1f2937 !important; +#app .studentLayout .el-pagination button:hover:not(:disabled) { + border-color: #2563eb !important; + background-color: #eff6ff !important; + color: #2563eb !important; } +/* 当前页:实心教育蓝(与学生端主按钮同色系) */ #app .studentLayout .pagination-container .el-pagination .el-pager li.is-active, #app .studentLayout .el-pager li.is-active { - border-color: #1f2937 !important; - background: #1f2937 !important; + border-color: #2563eb !important; + background: #2563eb !important; + color: #ffffff !important; +} + +#app .studentLayout .pagination-container .el-pagination .el-pager li.is-active:hover, +#app .studentLayout .el-pager li.is-active:hover { + border-color: #1d4ed8 !important; + background: #1d4ed8 !important; color: #ffffff !important; } @@ -539,8 +548,8 @@ aside { #app .studentLayout .pagination-container .el-pagination .btn-next:disabled, #app .studentLayout .el-pagination .btn-prev:disabled, #app .studentLayout .el-pagination .btn-next:disabled { - background-color: #ffffff !important; - color: #9ca3af !important; + background-color: #f9fafb !important; + color: #b0c0d0 !important; border-color: #e5e7eb !important; } @@ -550,14 +559,23 @@ aside { font-weight: 400; } -#app .studentLayout .el-pagination .el-select .el-input__wrapper { +/* 每页条数下拉 + 跳页输入框:白底灰边,hover/focus 教育蓝 */ +#app .studentLayout .el-pagination .el-input__wrapper { background: #ffffff !important; border: 1px solid #e5e7eb !important; border-radius: 8px !important; box-shadow: none !important; } -#app .studentLayout .el-pagination .el-select .el-input__inner { +#app .studentLayout .el-pagination .el-input__wrapper:hover { + border-color: #3b82f6 !important; +} + +#app .studentLayout .el-pagination .el-input__wrapper.is-focus { + border-color: #2563eb !important; +} + +#app .studentLayout .el-pagination .el-input__inner { color: #374151 !important; } diff --git a/src/views/channel/supplier.vue b/src/views/channel/supplier.vue index 235b5b4..9733b8d 100644 --- a/src/views/channel/supplier.vue +++ b/src/views/channel/supplier.vue @@ -94,26 +94,37 @@

Step 02

为供应渠道评分并比较结果

-

每项评分为 0–10 分,渠道名称和评分均由学生填写;系统会按第一步已保存的权重自动计算加权总分。

+

每项评分为 0–10 分,渠道名称与评分由学生填写;系统按第一步已保存的试产/量产权重分别计算两个加权总分。

-
+
-
{{ section.label }}

{{ section.title }}

- 加权总分 = Σ(维度评分 × 权重) +
渠道评分

供应渠道评分与加权对比

+ 试产加权总分 = Σ(评分 × 试产权重) · 量产加权总分 = Σ(评分 × 量产权重)
- +
- + + + + + + + + + + + - - - + + - + +
渠道{{ dimension.name }}加权总分
渠道维度评分(0–10 分)试产加权总分量产加权总分
{{ dimension.name }}Σ × 试产权重Σ × 量产权重
+
{{ weightedScore(row, section.weights) }}{{ weightedScore(row, 'smallBatch') }}{{ weightedScore(row, 'largeBatch') }}
@@ -268,15 +279,18 @@ function normalizeScores(source = {}) { }, {}); } -function createScoreRows(savedRows = []) { - const source = Array.isArray(savedRows) ? savedRows : []; +function createChannelRows(smallSaved = [], largeSaved = []) { + const small = Array.isArray(smallSaved) ? smallSaved : []; + const large = Array.isArray(largeSaved) ? largeSaved : []; return Array.from({ length: 5 }, (_, index) => { - const saved = source[index] || {}; + const s = small[index] || {}; + const l = large[index] || {}; return { id: `${Date.now()}-${index}-${Math.random().toString(16).slice(2)}`, - channel: String(saved.channel || ""), - scores: normalizeScores(saved.scores), - weightedTotal: saved.weightedTotal ?? null, + channel: String(s.channel || l.channel || ""), + scores: normalizeScores(s.scores ?? l.scores), + smallWeightedTotal: s.weightedTotal ?? null, + largeWeightedTotal: l.weightedTotal ?? null, }; }); } @@ -302,8 +316,7 @@ function createRiskRows(savedRows = []) { })); } -const smallBatchRows = ref(createScoreRows()); -const largeBatchRows = ref(createScoreRows()); +const channelRows = ref(createChannelRows()); const scenarioAnalyses = ref(createScenarioAnalyses()); const procurementRecommendation = ref(""); const riskRows = ref(createRiskRows()); @@ -312,16 +325,12 @@ const smallWeightTotal = computed(() => rows.value.reduce((sum, row) => sum + Nu const largeWeightTotal = computed(() => rows.value.reduce((sum, row) => sum + Number(row.largeBatchWeight || 0), 0)); const smallWeights = computed(() => rows.value.reduce((result, row) => ({ ...result, [row.key]: Number(row.smallBatchWeight || 0) }), {})); const largeWeights = computed(() => rows.value.reduce((result, row) => ({ ...result, [row.key]: Number(row.largeBatchWeight || 0) }), {})); -const scoreSections = computed(() => [ - { key: "small", label: "场景 A", title: "小批量试产(100台)渠道评分", rows: smallBatchRows.value, weights: smallWeights.value }, - { key: "large", label: "场景 B", title: "大规模量产(10000台)渠道评分", rows: largeBatchRows.value, weights: largeWeights.value }, -]); function scoreFor(row, key) { if (!Object.prototype.hasOwnProperty.call(row.scores, key)) row.scores[key] = 0; return { get score() { return row.scores[key]; }, - set score(value) { row.scores[key] = value; row.weightedTotal = null; }, + set score(value) { row.scores[key] = value; row.smallWeightedTotal = null; row.largeWeightedTotal = null; }, }; } @@ -330,18 +339,18 @@ function toNumber(value) { return Number.isFinite(numeric) ? numeric : 0; } -function weightedScore(row, weights) { +function weightedScore(row, scenario) { + const weights = scenario === "smallBatch" ? smallWeights.value : largeWeights.value; const score = rows.value.reduce((sum, item) => sum + toNumber(row.scores[item.key]) * toNumber(weights[item.key]) / 100, 0); return score.toFixed(2); } function channelRanking(scenario) { - const source = scenario === "smallBatch" ? smallBatchRows.value : largeBatchRows.value; - const weights = scenario === "smallBatch" ? smallWeights.value : largeWeights.value; - return source + const storedKey = scenario === "smallBatch" ? "smallWeightedTotal" : "largeWeightedTotal"; + return channelRows.value .map((row) => { - const stored = Number(row.weightedTotal); - return { channel: String(row.channel || "").trim(), score: Number.isFinite(stored) ? stored : Number(weightedScore(row, weights)) }; + const stored = Number(row[storedKey]); + return { channel: String(row.channel || "").trim(), score: Number.isFinite(stored) ? stored : Number(weightedScore(row, scenario)) }; }) .filter((item) => item.channel) .sort((left, right) => right.score - left.score || left.channel.localeCompare(right.channel, "zh-CN")); @@ -356,12 +365,6 @@ function formatScore(value) { return Number.isFinite(score) ? score.toFixed(2) : "--"; } -function syncChannelNames() { - largeBatchRows.value.forEach((row, index) => { - row.channel = String(smallBatchRows.value[index]?.channel || "").trim(); - }); -} - function buildStepOnePayload() { return { items: rows.value.map((row) => ({ @@ -373,16 +376,17 @@ function buildStepOnePayload() { } function buildStepTwoPayload() { - const serializeChannels = (source) => source.map((row) => ({ + const serializeChannels = (storedKey) => channelRows.value.map((row) => ({ channel: String(row.channel || "").trim(), scores: rows.value.map((dimension) => ({ dimension: dimension.name, score: toNumber(row.scores[dimension.key]), })), + weightedTotal: row[storedKey], })); return { - smallBatchChannels: serializeChannels(smallBatchRows.value), - largeBatchChannels: serializeChannels(largeBatchRows.value), + smallBatchChannels: serializeChannels("smallWeightedTotal"), + largeBatchChannels: serializeChannels("largeWeightedTotal"), }; } @@ -421,9 +425,7 @@ function applyStepOne(answer) { function applyStepTwo(answer) { const result = parseJson(answer?.step2Answer); if (!Array.isArray(result?.smallBatchChannels) || !Array.isArray(result?.largeBatchChannels)) return false; - smallBatchRows.value = createScoreRows(result.smallBatchChannels); - largeBatchRows.value = createScoreRows(result.largeBatchChannels); - syncChannelNames(); + channelRows.value = createChannelRows(result.smallBatchChannels, result.largeBatchChannels); return true; } @@ -451,14 +453,11 @@ async function saveStepOne() { async function saveStepTwo() { try { - syncChannelNames(); const result = (await validateSupplierEvaluationStepTwo(buildStepTwoPayload()))?.data || {}; if (!result?.valid) throw new Error(result?.message || "渠道评分校验未通过"); - smallBatchRows.value = createScoreRows(result.smallBatchChannels); - largeBatchRows.value = createScoreRows(result.largeBatchChannels); + channelRows.value = createChannelRows(result.smallBatchChannels, result.largeBatchChannels); await saveStudentTrainingAnswer(TASK_KEY, { currentStep: 2, saveAction: "SAVE", progressStatus: "IN_PROGRESS", step2Answer: JSON.stringify({ weights: result.weights, smallBatchChannels: result.smallBatchChannels, largeBatchChannels: result.largeBatchChannels }) }); stepTwoSaved.value = true; - syncChannelNames(); ElMessage.success("渠道评分已保存,可进入第 3 步形成采购建议"); } catch (error) { ElMessage.error(error?.message || "保存失败,请检查渠道和评分"); @@ -878,6 +877,35 @@ onMounted(async () => { .score-table th:first-child, .score-table td:first-child { width: 175px; } .score-table :deep(.el-input-number) { width: 100%; } +/* 合并评分表:双列组布局 */ +.merged-score-table { min-width: 1080px; table-layout: fixed; } +.merged-score-table .col-channel { width: 150px; } +.merged-score-table .col-dimension { width: auto; } +.merged-score-table .col-total { width: 110px; } + +/* 顶层分组表头:维度组中性底,总分组着色 */ +.merged-score-table .group-header th { + text-align: center; + font-size: var(--student-fs-body-sm); +} +.merged-score-table .group-dimensions { + background: #fafbfc; + color: var(--student-text); +} +.merged-score-table .group-total { color: #ffffff; font-weight: 800; } +.merged-score-table .group-total-small { background: var(--student-accent); } +.merged-score-table .group-total-large { background: var(--student-accent-deep); } + +/* 第二行子表头 */ +.merged-score-table .col-dimension, +.merged-score-table .col-total { background: #fafbfc; } +.merged-score-table .col-total-small { color: var(--student-accent); } +.merged-score-table .col-total-large { color: var(--student-accent-deep); } + +/* 列组竖向分隔线 */ +.merged-score-table .col-channel, +.merged-score-table .col-total-small { border-right: 2px solid var(--student-panel-border); } + .total-score { color: var(--student-accent) !important; font-size: var(--student-fs-data); From 6edf32a7b573a0293671dc63daabd1ec0e76a1b0 Mon Sep 17 00:00:00 2001 From: qinzhenpen Date: Mon, 28 Sep 2026 14:18:57 +0800 Subject: [PATCH 2/4] feat(studentDemo): implement demo session handling and role management Added functionality to manage student demo sessions, ensuring the role is fixed to student to prevent unauthorized access. Introduced a new method to unwrap demo payloads and save session data securely. Updated navigation logic to redirect users appropriately based on their role, enhancing the user experience during demo sessions. --- src/views/studentDemo/index.vue | 46 ++- src/views/teacherEnd/student/index.vue | 10 +- .../taskReview/StudentTaskReadonlyPreview.vue | 161 +++++++++ src/views/teacherEnd/trainingTask/index.vue | 331 ++++++++++++++++-- 4 files changed, 504 insertions(+), 44 deletions(-) diff --git a/src/views/studentDemo/index.vue b/src/views/studentDemo/index.vue index cb09e39..d908a8c 100644 --- a/src/views/studentDemo/index.vue +++ b/src/views/studentDemo/index.vue @@ -7,6 +7,23 @@ import { exchangeStudentDemoSession } from "@/api/studentDemo"; import { saveStudentDemoSession } from "@/utils/studentDemo"; import useUserStore from "@/store/modules/user"; +// 学生端演示登录态的角色:固定为学生 (roleId === 4),避免后端返回 +// 教师身份导致跳转到教师首页。后端若返回 demoTeachingClassId,会被 +// 透传保留,登录页/Navbar/AppMain 可据此识别为"演示学生"。 +const STUDENT_DEMO_ROLE_ID = "4"; + +function unwrapDemoPayload(payload) { + if (!payload || typeof payload !== "object") { + return { userInfo: { roleId: STUDENT_DEMO_ROLE_ID }, token: "" }; + } + // 兼容两种返回结构: + // 1) { token, roleId, userId, ... } —— 扁平结构 + // 2) { token, userInfo: { roleId, ... } } —— userInfo 嵌套 + const inner = payload.userInfo && typeof payload.userInfo === "object" ? payload.userInfo : payload; + const token = inner.token || payload.token || ""; + return { userInfo: { ...inner, token, roleId: STUDENT_DEMO_ROLE_ID }, token }; +} + const userStore = useUserStore(); const message = ref("正在连接教师演示会话…"); let readyTimer; @@ -15,15 +32,36 @@ function notifyReady() { if (window.opener) window.opener.postMessage({ type: "STUDENT_DEMO_READY" }, window.location.origin); } +function applyDemoSession(demoPayload) { + const { userInfo, token } = unwrapDemoPayload(demoPayload); + // 保存到 sessionStorage(getUserInfo/getToken 优先从这里读取) + // 注意:演示 session 只放在新窗口的 sessionStorage,避免覆盖 + // 教师主窗口的 cookie/sessionStorage 而把教师侧误切到学生身份。 + saveStudentDemoSession(userInfo); + // 立即更新 Pinia,避免 reload 之前出现教师内容闪烁 + userStore.token = token; + userStore.userInfo = userInfo; +} + function receiveTicket(event) { if (event.origin !== window.location.origin || event.source !== window.opener || event.data?.type !== "STUDENT_DEMO_TICKET") return; exchangeStudentDemoSession(event.data.ticket).then((res) => { - saveStudentDemoSession(res.data); - userStore.token = res.data.token; - userStore.userInfo = res.data; + if (!res || !res.data) { + message.value = "演示服务返回数据为空,请稍后重试"; + return; + } + applyDemoSession(res.data); window.clearInterval(readyTimer); window.removeEventListener("message", receiveTicket); - window.location.hash = "#/index"; + // 跳转学生端基础课首页 #/foundation/new-product-survey: + // roleId 已被改写为 4,filterDynamicRoutes 会加载 student 路由, + // 避开 /index 的角色分发逻辑(role==='3' 会落到 ), + // 强制落到学生工作台,避免任何教师侧兜底。 + const targetHash = "#/foundation/new-product-survey"; + if (window.location.hash !== targetHash) { + // 先改 hash 再 reload,避免仅 hash 变化时浏览器不重新加载 + window.location.hash = targetHash; + } window.location.reload(); }).catch(() => { message.value = "演示链接已失效,请返回教师端重新打开。"; }); } diff --git a/src/views/teacherEnd/student/index.vue b/src/views/teacherEnd/student/index.vue index 0f921b3..bff1c41 100644 --- a/src/views/teacherEnd/student/index.vue +++ b/src/views/teacherEnd/student/index.vue @@ -486,7 +486,7 @@ const beforeImportStudents = (file) => { return true } const getClassList = () => { - indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId }).then((res) => { + return indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId }).then((res) => { classList.value = res.data }) } @@ -542,11 +542,15 @@ const initPassword = (row, status) => { } onMounted(async () => { await userStore.ensureSchoolProductConfig().catch(() => null) - getList() - getClassList() if (!isTeacherRosterMode.value) { getFacultyList() } + await getClassList() + // 默认展示第一个班级数据:把 select 绑到第一个教学班并立即拉取学生列表 + if (!paramsquery.value.schoolClassId && studentClassOptions.value.length) { + paramsquery.value.schoolClassId = studentClassOptions.value[0].schoolClassId + } + getList() }) diff --git a/src/views/teacherEnd/trainingTask/index.vue b/src/views/teacherEnd/trainingTask/index.vue index 78420bd..24ae49b 100644 --- a/src/views/teacherEnd/trainingTask/index.vue +++ b/src/views/teacherEnd/trainingTask/index.vue @@ -156,6 +156,17 @@ {{ task.steps?.length || 0 }} 个步骤
+ +
+ + + + + 对应学生页: + {{ resolveStudentPage(task).label }} + {{ resolveStudentPage(task).path }} +
@@ -311,31 +322,39 @@

上传案例资料,配置实训步骤

-
-
- - - - -
- {{ taskForm.materialName - }} - 暂未上传案例资料 +
+
+
+ + + +
+ {{ slot.label }} + {{ slot.material.name }} + 暂未上传{{ slot.label }} +
+
+
+ + + + + 预览 + + + + + + + {{ slot.material.name ? "重新上传" : "上传" }} + +
- - - - - - - - {{ materialUploading ? "上传中..." : "上传案例资料" }} - -
@@ -399,7 +418,19 @@
-
+ + + +
+ +
+ +
该资料无法在线预览,请下载后查看
+
+ +
+ + diff --git a/src/views/teacherEnd/exam/editor.vue b/src/views/teacherEnd/exam/editor.vue index ef7c800..a0daf03 100644 --- a/src/views/teacherEnd/exam/editor.vue +++ b/src/views/teacherEnd/exam/editor.vue @@ -9,14 +9,14 @@ -
+

基础设置

填写考试名称、时间与班级,分值矩阵自动汇总

@@ -53,7 +53,7 @@
-
+

考试设置

设置试题展示、作答行为和多选题计分方式

@@ -103,7 +103,7 @@
-
+

选择试题

勾选题目并设置该题分值,选择结果实时同步到试卷概览

@@ -148,7 +148,7 @@
-
+

试卷概览

{{ form.questions.length }} 道题,共 {{ totalScore }} 分

导出 Word 试卷
@@ -214,7 +214,7 @@ import { saveAs } from "file-saver"; import * as examApi from "@/api/exam"; const route = useRoute(); const router = useRouter(); const id = computed(() => route.params.id); const isNew = computed(() => id.value === "new"); -const loading = ref(false); const activeStep = ref(1); const activeType = ref("SINGLE"); const status = ref("DRAFT"); const classes = ref([]); const bank = ref([]); const formRef = ref(); const duration = ref(60); +const loading = ref(false); const activeStep = ref(0); const activeType = ref("SINGLE"); const status = ref("DRAFT"); const classes = ref([]); const bank = ref([]); const formRef = ref(); const duration = ref(60); const typeOptions = [{ label: "单选题", value: "SINGLE" }, { label: "多选题", value: "MULTIPLE" }, { label: "判断题", value: "TRUE_FALSE" }, { label: "简答题", value: "SHORT_ANSWER" }]; const statusMeta = { DRAFT: { label: "未发布", tone: "info" }, NOT_STARTED: { label: "未开始", tone: "" }, IN_PROGRESS: { label: "进行中", tone: "success" }, ENDED: { label: "已结束", tone: "warning" } }; const form = reactive({ name: "", startTime: "", endTime: "", classIds: [], questionOrderMode: "FIXED", optionOrderMode: "FIXED", allowCopyQuestion: false, allowPasteAnswer: false, allowRollback: true, multipleScoreMode: "ALL_OR_NOTHING", questions: [] }); @@ -235,8 +235,8 @@ function selectedByType(type) { const ids = new Set(bank.value.filter((item) => function questionCount(type) { return bank.value.filter((item) => item.type === type).length; } function scoreByType(type) { return selectedQuestions(type).reduce((sum, item) => sum + Number(selectedScore(item.id)), 0); } function selectedQuestions(type) { const selected = new Set(form.questions.map((item) => item.questionId)); return bank.value.filter((item) => item.type === type && selected.has(item.id)); } -async function nextStep() { if (activeStep.value === 0) { try { await formRef.value.validate(); const start = new Date(form.startTime.replace(" ", "T")); const end = new Date(form.endTime.replace(" ", "T")); if (end <= start) return ElMessage.warning("结束时间必须晚于开始时间"); duration.value = Math.round((end - start) / 60000); } catch { return; } } if (activeStep.value === 1 && !form.questions.length) return ElMessage.warning("至少选择一道试题"); activeStep.value++; } -async function save(publish) { if (inProgress.value) { await examApi.updateExam(id.value, form); ElMessage.success("结束时间已更新"); return router.push("/exam/index"); } try { await formRef.value?.validate(); } catch { activeStep.value = 0; return; } if (!form.questions.length) { activeStep.value = 1; return ElMessage.warning("至少选择一道试题"); } const result = isNew.value ? await examApi.createExam(form) : await examApi.updateExam(id.value, form); const examId = result.data.id; if (publish) await examApi.publishExam(examId); ElMessage.success(publish ? "考试已保存并发布" : "考试已保存"); router.push("/exam/index"); } +async function nextStep() { if (activeStep.value === 0 && !form.questions.length) return ElMessage.warning("至少选择一道试题"); if (activeStep.value === 2) { try { await formRef.value.validate(); const start = new Date(form.startTime.replace(" ", "T")); const end = new Date(form.endTime.replace(" ", "T")); if (end <= start) return ElMessage.warning("结束时间必须晚于开始时间"); duration.value = Math.round((end - start) / 60000); } catch { return; } } activeStep.value++; } +async function save(publish) { if (inProgress.value) { await examApi.updateExam(id.value, form); ElMessage.success("结束时间已更新"); return router.push("/exam/index"); } try { await formRef.value?.validate(); } catch { activeStep.value = 2; return; } if (!form.questions.length) { activeStep.value = 0; return ElMessage.warning("至少选择一道试题"); } const result = isNew.value ? await examApi.createExam(form) : await examApi.updateExam(id.value, form); const examId = result.data.id; if (publish) await examApi.publishExam(examId); ElMessage.success(publish ? "考试已保存并发布" : "考试已保存"); router.push("/exam/index"); } async function importPaperQuestions({ file }) { const formData = new FormData(); formData.append("file", file); const { data } = await examApi.importQuestions(formData); bank.value = [...data, ...bank.value]; for (const q of data) if (!isSelected(q.id)) form.questions.push({ questionId: q.id, score: 1 }); ElMessage.success(`导入并选中 ${data.length} 道题`); } async function downloadTemplate() { saveAs(await examApi.downloadQuestionTemplate(), "考试题库导入模板.docx"); } async function exportPaper() { saveAs(await examApi.exportExamPaper(id.value), `${form.name}试卷.docx`); } From 60a394a18c5dfcde27208f4659b7ad0edfb4b016 Mon Sep 17 00:00:00 2001 From: qinzhenpen Date: Mon, 28 Sep 2026 17:08:01 +0800 Subject: [PATCH 4/4] refactor(exam/editor): simplify score table by removing unnecessary score input fields Removed the score input fields from the score table and adjusted the table header accordingly. This change streamlines the exam editor interface, focusing on essential information while maintaining functionality for score calculations. Additionally, cleaned up related CSS styles to reflect the updated layout. --- src/views/teacherEnd/exam/editor.vue | 49 ++-------------------------- 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/src/views/teacherEnd/exam/editor.vue b/src/views/teacherEnd/exam/editor.vue index fc05062..577e50d 100644 --- a/src/views/teacherEnd/exam/editor.vue +++ b/src/views/teacherEnd/exam/editor.vue @@ -26,24 +26,12 @@
- + - @@ -130,10 +118,6 @@
{{ idx + 1 }} {{ typeOptions.find(t => t.value === question.type)?.label || question.type }} - - - 分 -

{{ question.stem }}

@@ -240,12 +224,11 @@ function isSelected(questionId) { return form.questions.some((item) => item.ques function selectedScore(questionId) { return form.questions.find((item) => item.questionId === questionId)?.score || 1; } function toggleQuestion(question, checked) { if (checked) form.questions.push({ questionId: question.id, score: typeScores[question.type] || 1 }); else form.questions = form.questions.filter((item) => item.questionId !== question.id); } function removeQuestionFromPreview(question) { toggleQuestion(question, false); } -function setScore(questionId, score) { const item = form.questions.find((row) => row.questionId === questionId); if (item) item.score = Number(score); } + function selectedByType(type) { const ids = new Set(bank.value.filter((item) => item.type === type).map((item) => item.id)); return form.questions.filter((item) => ids.has(item.questionId)).length; } function questionCount(type) { return bank.value.filter((item) => item.type === type).length; } function scoreByType(type) { return selectedQuestions(type).reduce((sum, item) => sum + Number(selectedScore(item.id)), 0); } function scorePerQuestion(type) { return Number(typeScores[type] || 1); } -function setTypeScore(type, score) { const value = Number(score); if (!Number.isFinite(value) || value < 0.5) return; typeScores[type] = value; const ids = new Set(bank.value.filter((item) => item.type === type).map((item) => item.id)); for (const item of form.questions) if (ids.has(item.questionId)) item.score = value; } function selectedQuestions(type) { const selected = new Set(form.questions.map((item) => item.questionId)); return bank.value.filter((item) => item.type === type && selected.has(item.id)); } async function nextStep() { if (activeStep.value === 0) { try { await formRef.value.validate(); const start = new Date(form.startTime.replace(" ", "T")); const end = new Date(form.endTime.replace(" ", "T")); if (end <= start) return ElMessage.warning("结束时间必须晚于开始时间"); duration.value = Math.round((end - start) / 60000); } catch { return; } } if (activeStep.value === 1 && !form.questions.length) return ElMessage.warning("至少选择一道试题"); activeStep.value++; } async function save(publish) { if (inProgress.value) { await examApi.updateExam(id.value, form); ElMessage.success("结束时间已更新"); return router.push("/exam/index"); } try { await formRef.value?.validate(); } catch { activeStep.value = 0; return; } if (!form.questions.length) { activeStep.value = 1; return ElMessage.warning("至少选择一道试题"); } const result = isNew.value ? await examApi.createExam(form) : await examApi.updateExam(id.value, form); const examId = result.data.id; if (publish) await examApi.publishExam(examId); ElMessage.success(publish ? "考试已保存并发布" : "考试已保存"); router.push("/exam/index"); } @@ -397,8 +380,6 @@ function toBoolean(value, fallback) { if (value === undefined || value === null tbody tr:hover td { background: #fafcff; } } - .type-score-input { width: 112px; } - .fixed-step-actions { display: flex; align-items: center; @@ -690,32 +671,6 @@ function toBoolean(value, fallback) { if (value === undefined || value === null font-weight: 500; } - .q-score-chip { - display: inline-flex; - align-items: center; - gap: var(--sp-1); - margin-left: auto; - padding: 2px var(--sp-3); - border-radius: var(--r-control); - background: #ffffff; - border: 1px solid var(--edu-primary); - font-size: var(--fs-tag); - color: var(--edu-primary-deep); - - span { color: var(--edu-primary-deep); font-weight: 500; } - - :deep(.el-input-number) { width: 88px; } - :deep(.el-input-number .el-input__wrapper) { - padding: 2px 6px; - box-shadow: none !important; - background: transparent; - } - :deep(.el-input-number__decrease), - :deep(.el-input-number__increase) { - display: none; - } - } - .q-card-stem { margin: 0 0 var(--sp-2); color: var(--text-strong);
题型题库题目数量选题数量每题分值小计
题型题库题目数量选题数量小计
{{ item.label }} {{ questionCount(item.value) }} {{ selectedByType(item.value) }} - - {{ scoreByType(item.value) }}