From b087a72a1ea268bf1c6d0bec85155d61254b6e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=B2=85?= <907037276@qq.com> Date: Sat, 5 Sep 2026 11:33:06 +0800 Subject: [PATCH] feat: complete teacher task review workflow --- src/api/studentTrainingAnswer.js | 5 + src/api/trainingTask.js | 11 ++ src/router/index.js | 9 +- src/utils/request.js | 12 +- src/utils/trainingReviewPreview.js | 53 ++++++++ .../teacherEnd/score/TaskReviewPanel.vue | 116 +++++++++--------- .../taskReview/StudentTaskReadonlyPreview.vue | 73 +++++++++++ .../taskReview/studentTaskPreviewMap.js | 30 +++++ src/views/training/GenericTrainingPage.vue | 5 +- tests/task-review-navigation.test.mjs | 2 +- tests/task-review-preview.test.mjs | 69 +++++++++++ 11 files changed, 324 insertions(+), 61 deletions(-) create mode 100644 src/utils/trainingReviewPreview.js create mode 100644 src/views/teacherEnd/taskReview/StudentTaskReadonlyPreview.vue create mode 100644 src/views/teacherEnd/taskReview/studentTaskPreviewMap.js create mode 100644 tests/task-review-preview.test.mjs diff --git a/src/api/studentTrainingAnswer.js b/src/api/studentTrainingAnswer.js index 7d19707..1987b39 100644 --- a/src/api/studentTrainingAnswer.js +++ b/src/api/studentTrainingAnswer.js @@ -1,5 +1,6 @@ import request from "@/utils/request"; import { getDemoAnswerKey, getStudentDemoSession, isStudentDemo } from "@/utils/studentDemo"; +import { getTrainingReviewPreviewContext } from "@/utils/trainingReviewPreview"; const newProductSurveyStepOneAnswers = { "selling-point-1": "核心层", @@ -20,6 +21,10 @@ function demoResult(taskKey) { } export function getStudentTrainingAnswer(taskKey, params) { + const preview = getTrainingReviewPreviewContext(); + if (preview) { + return Promise.resolve({ code: 200, data: preview.taskKey === taskKey ? preview.answer || null : null }); + } if (isStudentDemo()) return Promise.resolve({ code: 200, data: demoResult(taskKey) }); return request({ url: `/api/student-training-answers/${taskKey}`, diff --git a/src/api/trainingTask.js b/src/api/trainingTask.js index 67a0468..df9776e 100644 --- a/src/api/trainingTask.js +++ b/src/api/trainingTask.js @@ -1,5 +1,6 @@ import request from "@/utils/request"; import { getUserInfo } from "@/utils/auth"; +import { getTrainingReviewPreviewContext } from "@/utils/trainingReviewPreview"; // 学生页面和全局“补充步骤”导航会在同一轮路由渲染中读取同一任务配置。 // 仅合并进行中的请求:教师保存后下一次读取仍会获得最新配置,不引入陈旧缓存。 @@ -67,6 +68,16 @@ export function loadStudentTrainingTasks() { export function getTrainingTaskByKey(taskKey) { const normalizedTaskKey = String(taskKey || "").trim(); + const preview = getTrainingReviewPreviewContext(); + if (preview) { + if (preview.taskKey !== normalizedTaskKey) { + return Promise.resolve({ code: 200, data: null }); + } + if (!preview.taskConfig) { + return Promise.reject(new Error("评阅案例配置未加载,不能展示通用案例")); + } + return Promise.resolve({ code: 200, data: preview.taskConfig }); + } if (pendingTaskRequests.has(normalizedTaskKey)) { return pendingTaskRequests.get(normalizedTaskKey); } diff --git a/src/router/index.js b/src/router/index.js index f718612..76a41b6 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -499,7 +499,14 @@ export const dynamicRoutes = [ ]; // 教师端路由 export const teacherRoutes = [ - { + { + path: "/task-review-preview", + component: () => import("@/views/teacherEnd/taskReview/StudentTaskReadonlyPreview.vue"), + roles: ["teacher"], + hidden: true, + meta: { title: "学生任务预览", affix: false }, + }, + { path: "/school", component: Layout, redirect: "/school/index", diff --git a/src/utils/request.js b/src/utils/request.js index cc4b383..680a56a 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -5,7 +5,8 @@ import errorCode from "@/utils/errorCode"; import { tansParams, blobValidate } from "@/utils/ruoyi"; import cache from "@/plugins/cache"; import { saveAs } from "file-saver"; -import useUserStore from "@/store/modules/user"; +import useUserStore from "@/store/modules/user"; +import { isTrainingReviewPreviewRequest } from "@/utils/trainingReviewPreview"; let downloadLoadingInstance; // 是否显示重新登录 @@ -23,8 +24,13 @@ const service = axios.create({ }); // request拦截器 service.interceptors.request.use( - (config) => { - // 是否需要设置 token + (config) => { + if (isTrainingReviewPreviewRequest(config)) { + const error = new Error("任务评阅预览为只读模式,不能修改学生作答"); + error.config = config; + return Promise.reject(error); + } + // 是否需要设置 token const isToken = (config.headers || {}).isToken === false; // 是否需要防止数据重复提交 const isRepeatSubmit = (config.headers || {}).repeatSubmit === false; diff --git a/src/utils/trainingReviewPreview.js b/src/utils/trainingReviewPreview.js new file mode 100644 index 0000000..a1c8d72 --- /dev/null +++ b/src/utils/trainingReviewPreview.js @@ -0,0 +1,53 @@ +const PREVIEW_STORAGE_PREFIX = "teacher-task-review-preview:"; + +function currentLocation() { + return typeof window === "undefined" ? null : window.location; +} + +function previewKeyFromLocation() { + const location = currentLocation(); + if (!location) return ""; + const directKey = new URLSearchParams(location.search).get("reviewPreviewKey"); + if (directKey) return directKey; + const hashQueryIndex = location.hash.indexOf("?"); + if (hashQueryIndex < 0) return ""; + return new URLSearchParams(location.hash.slice(hashQueryIndex + 1)).get("reviewPreviewKey") || ""; +} + +export function getTrainingReviewPreviewKey() { + return previewKeyFromLocation(); +} + +export function writeTrainingReviewPreview(previewKey, value) { + if (!previewKey || typeof sessionStorage === "undefined") return; + sessionStorage.setItem(`${PREVIEW_STORAGE_PREFIX}${previewKey}`, JSON.stringify(value)); +} + +export function readTrainingReviewPreview(previewKey) { + if (!previewKey || typeof sessionStorage === "undefined") return null; + try { + return JSON.parse(sessionStorage.getItem(`${PREVIEW_STORAGE_PREFIX}${previewKey}`) || "null"); + } catch (_) { + return null; + } +} + +export function clearTrainingReviewPreview(previewKey) { + if (previewKey && typeof sessionStorage !== "undefined") { + sessionStorage.removeItem(`${PREVIEW_STORAGE_PREFIX}${previewKey}`); + } +} + +export function getTrainingReviewPreviewContext() { + return readTrainingReviewPreview(getTrainingReviewPreviewKey()); +} + +export function isTrainingReviewPreviewRequest(config = {}) { + const method = String(config.method || "get").toLowerCase(); + const url = String(config.url || "").split("?")[0]; + return Boolean(getTrainingReviewPreviewKey()) + && ["post", "put", "patch", "delete"].includes(method) + && (url.startsWith("/api/student-training-answers/") + || url.startsWith("/api/student/project-study-time/") + || url === "/common/upload"); +} diff --git a/src/views/teacherEnd/score/TaskReviewPanel.vue b/src/views/teacherEnd/score/TaskReviewPanel.vue index 02e352a..12f7c12 100644 --- a/src/views/teacherEnd/score/TaskReviewPanel.vue +++ b/src/views/teacherEnd/score/TaskReviewPanel.vue @@ -25,6 +25,7 @@ - - - - + + + + - +