diff --git a/src/api/teacher.js b/src/api/teacher.js index 6aed8b0..18b7c5c 100644 --- a/src/api/teacher.js +++ b/src/api/teacher.js @@ -346,6 +346,14 @@ export function uploadTeachingClassStudentMembers(data) { headers: { "Content-Type": "multipart/form-data" }, }); } +export function importTeacherRosterMembers(teachingClassId, data) { + return request({ + url: `/api/user/teaching-classes/${teachingClassId}/students/import`, + method: "POST", + data, + headers: { "Content-Type": "multipart/form-data" }, + }); +} export function importTeacherStudents(data) { return request({ url: "/api/user/importTeacherStudents", diff --git a/src/views/teacherEnd/class/index.vue b/src/views/teacherEnd/class/index.vue index cfbad32..8376ff9 100644 --- a/src/views/teacherEnd/class/index.vue +++ b/src/views/teacherEnd/class/index.vue @@ -316,7 +316,10 @@ function getList(options = {}) { loading.value = true; } indexApi - .getClassesList(paramsquery.value) + .getClassesList({ + ...paramsquery.value, + operatorId: currentUserId.value, + }) .then((res) => { tableData.value = res.data?.list || []; schoolTotal.value = res.data?.total || 0; @@ -538,6 +541,15 @@ function beforeImportMembers(file) { } function uploadMemberImport(options, confirmMultiTeachingClass = false) { + if (isTeacherRosterMode.value) { + const formData = new FormData(); + formData.append("file", options.file); + return indexApi + .importTeacherRosterMembers(selectedTeachingClass.value.schoolClassId, formData) + .then((res) => { + handleMemberImportSuccess(res); + }); + } const formData = buildUploadFormData(options.file, memberUploadData.value, confirmMultiTeachingClass); return indexApi .uploadTeachingClassStudentMembers(formData) @@ -602,6 +614,26 @@ function initializeMember(row) { async function downloadMemberImportTemplate() { const XLSX = await import("xlsx"); + if (isTeacherRosterMode.value) { + const templateSheet = XLSX.utils.aoa_to_sheet([ + ["学生姓名", "学号"], + ["张三", "20260001"], + ]); + templateSheet["!cols"] = [{ wch: 16 }, { wch: 18 }]; + const guideSheet = XLSX.utils.aoa_to_sheet([ + ["填写说明"], + ["1. 本模板用于向当前教学班添加成员。"], + ["2. 仅填写学生姓名和学号,不需要填写班级或其他个人信息。"], + ["3. 学号已存在时,系统会复用该学生并加入当前教学班。"], + ["4. 任意一行校验失败时,整批导入失败并返回失败原因。"], + ]); + guideSheet["!cols"] = [{ wch: 72 }]; + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, templateSheet, "教学班成员导入模板"); + XLSX.utils.book_append_sheet(workbook, guideSheet, "填写说明"); + XLSX.writeFile(workbook, "教学班成员导入模板.xlsx"); + return; + } const templateSheet = XLSX.utils.aoa_to_sheet([ ["所属院系", "所属专业", "行政班", "学号", "学生姓名", "性别", "手机号", "邮箱"], ["商学院", "电子商务", "2026电子商务1班-行政班", "20260001", "张三", "男", "13800000000", "zhangsan@example.com"], diff --git a/tests/school-default-task.static.test.cjs b/tests/school-default-task.static.test.cjs index d941326..eafbe0e 100644 --- a/tests/school-default-task.static.test.cjs +++ b/tests/school-default-task.static.test.cjs @@ -11,7 +11,7 @@ const apiSource = read("src/api/teacher.js"); const pageSource = read("src/views/teacherEnd/defaultTask/index.vue"); assert.match(routerSource, /requiresManagerTeacher:\s*true/); -assert.match(sidebarSource, /route\.meta\?\.requiresManagerTeacher/); +assert.match(sidebarSource, /capability\.requiresManagerTeacher && userStore\.userInfo\.teacherAdmin !== true/); assert.match(apiSource, /\/api\/school-default-tasks/); assert.match(pageSource, /正在初始化,稍后刷新/); assert.match(pageSource, /立即重试/); diff --git a/tests/teacher-owned-class-roster.static.test.cjs b/tests/teacher-owned-class-roster.static.test.cjs new file mode 100644 index 0000000..2be6ab0 --- /dev/null +++ b/tests/teacher-owned-class-roster.static.test.cjs @@ -0,0 +1,21 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const pagePath = path.join(__dirname, "..", "src", "views", "teacherEnd", "class", "index.vue"); +const apiPath = path.join(__dirname, "..", "src", "api", "teacher.js"); +const page = fs.readFileSync(pagePath, "utf8"); +const api = fs.readFileSync(apiPath, "utf8"); + +assert.match(page, /getClassesList\(\{\s*\.\.\.paramsquery\.value,\s*operatorId: currentUserId\.value,?\s*\}\)/, + "班级查询必须提交当前操作教师,以便后端限制为本人教学班"); +assert.match(api, /export function importTeacherRosterMembers\(teachingClassId, data\)/, + "前端 API 层必须提供教师教学班成员导入接口"); +assert.match(api, /url: `\/api\/user\/teaching-classes\/\$\{teachingClassId\}\/students\/import`/, + "教师教学班成员导入必须使用专用端点"); +assert.match(page, /if \(isTeacherRosterMode\.value\) \{[\s\S]*importTeacherRosterMembers/, + "教学班专属模式必须走教师名单导入端点"); +assert.match(page, /\["学生姓名", "学号"\]/, + "教学班专属模式模板必须只包含姓名和学号"); + +console.log("teacher owned class roster static checks passed");