From c2fcc259c70c24962d2c3ed9e2551112320f657b Mon Sep 17 00:00:00 2001 From: chenyuan Date: Thu, 2 Jul 2026 20:04:55 +0800 Subject: [PATCH] feat: open admin classes to school teachers --- src/api/teacher.js | 68 ++- src/views/teacherEnd/class/index.vue | 564 +++++++++++++++++++++++-- src/views/teacherEnd/student/index.vue | 109 ++++- 3 files changed, 684 insertions(+), 57 deletions(-) diff --git a/src/api/teacher.js b/src/api/teacher.js index e09de81..7f2b91a 100644 --- a/src/api/teacher.js +++ b/src/api/teacher.js @@ -338,6 +338,30 @@ export function addClass(params) { }); } // 班级管理-删除 +export function uploadTeachingClassStudentMembers(data) { + return request({ + url: "/api/user/uploadTeachingClassStudentMembers", + method: "POST", + data, + headers: { "Content-Type": "multipart/form-data" }, + }); +} +export function importTeacherStudents(data) { + return request({ + url: "/api/user/importTeacherStudents", + method: "POST", + data, + headers: { "Content-Type": "multipart/form-data" }, + }); +} +export function uploadTeachingClassByStudentMembers(data) { + return request({ + url: "/api/user/uploadTeachingClassByStudentMembers", + method: "POST", + data, + headers: { "Content-Type": "multipart/form-data" }, + }); +} export function deleteClass(params) { return request({ url: "/api/user/deleteSchoolClass", @@ -418,13 +442,33 @@ export function getStudentList(query) { }); } // 根据学校id查询班级 -export function getClassListBySchoolId(query) { - return request({ +const classListBySchoolIdCache = new Map(); +const CLASS_LIST_CACHE_TTL = 30 * 1000; + +export function clearClassListBySchoolIdCache(schoolId) { + if (schoolId) { + classListBySchoolIdCache.delete(schoolId); + return; + } + classListBySchoolIdCache.clear(); +} + +export function getClassListBySchoolId(query, options = {}) { + const schoolId = query?.schoolId || ""; + const cached = classListBySchoolIdCache.get(schoolId); + const now = Date.now(); + if (!options.force && cached && now - cached.time < CLASS_LIST_CACHE_TTL) { + return cached.promise; + } + const promise = request({ url: "/api/user/seleteSchoolClassListBySchoolId", method: "POST", params: query, }); + classListBySchoolIdCache.set(schoolId, { time: now, promise }); + return promise; } + // 根据专业id查询班级 export function getClassListByMajorId(query) { return request({ @@ -443,12 +487,14 @@ export function addStudent(params) { } // 学生管理 编辑 export function updateStudent(data) { + const { operatorId, teachingClassId, ...body } = data; return request({ url: "/api/user/updateStudent", method: "POST", - data, + data: body, params: { - operatorId: data.operatorId, + operatorId, + teachingClassId, }, }); } @@ -478,6 +524,20 @@ export function getTaskList(query) { params: query, }); } +export function getStudentTaskList(query) { + return request({ + url: "/api/taskAllocation/selectTaskAllocationByStudentUserId", + method: "POST", + params: query, + }); +} +export function getCurrentTeachingClass(query) { + return request({ + url: "/api/user/selectCurrentTeachingClass", + method: "POST", + params: query, + }); +} // 任务分配 export function addTask(data) { let { taskAllocationList, classId, schoolId, userId } = data; diff --git a/src/views/teacherEnd/class/index.vue b/src/views/teacherEnd/class/index.vue index 90702c5..fce376f 100644 --- a/src/views/teacherEnd/class/index.vue +++ b/src/views/teacherEnd/class/index.vue @@ -17,7 +17,7 @@ 刷新 -
+
新增{{ activeClassType === 'ADMIN' ? '行政班' : '教学班' }}
@@ -33,9 +33,9 @@ {{ row.schoolMajorName || "-" }} - + @@ -50,16 +50,21 @@ {{ formatDate(row.schoolClass.createTime) }} - + @@ -69,18 +74,37 @@ - + - + + + + + + + +
+ + 导入名单创建 + + 模板下载 +
+
+ + +
+ + + 查询 + 刷新 + +
+ + + + + + + + + + + + +
@@ -104,17 +173,24 @@ const userStore = useUserStore(); const activeClassType = ref("ADMIN"); const dialogVisible = ref(false); +const memberDialogVisible = ref(false); const loading = ref(false); +const memberLoading = ref(false); const isCreate = ref(true); const tableData = ref([]); +const memberList = ref([]); const schoolTotal = ref(0); +const memberTotal = ref(0); const facultyList = ref([]); const majorList = ref([]); const majorListForForm = ref([]); +const allClassList = ref([]); +const sourceAdminClassIds = ref([]); +const selectedTeachingClass = ref(null); const currentUserId = computed(() => userStore.userInfo.userId); const currentSchoolId = computed(() => userStore.userInfo.schoolId); -const isManagerTeacher = computed(() => userStore.userInfo.teacherAdmin === true || String(userStore.userInfo.roleId) === "1"); +const canCreateCurrentClass = computed(() => activeClassType.value === "ADMIN" || activeClassType.value === "TEACHING"); const paramsquery = ref({ index: 1, @@ -126,6 +202,7 @@ const paramsquery = ref({ }); const formInline = ref(defaultForm()); +const memberParams = ref(defaultMemberParams()); const rules = { schoolFacultyId: [{ required: true, message: "请选择所属院系", trigger: "change" }], @@ -140,11 +217,55 @@ const tableHeaderStyle = { }; const dialogTitle = computed(() => `${isCreate.value ? "新增" : "编辑"}${activeClassType.value === "ADMIN" ? "行政班" : "教学班"}`); +const memberDialogTitle = computed(() => `${selectedTeachingClass.value?.className || ""} 成员管理`); +const selectedTeachingClassCanOperate = computed(() => canOperateClass(selectedTeachingClass.value)); const filteredTableData = computed(() => { return tableData.value.filter((row) => normalizeClassType(row.schoolClass) === activeClassType.value); }); +const adminClassOptions = computed(() => { + return allClassList.value.filter((schoolClass) => { + return normalizeClassType(schoolClass) === "ADMIN" + && (!formInline.value.schoolMajorId || schoolClass.schoolMajorId === formInline.value.schoolMajorId); + }); +}); + +const importCreateUploadData = computed(() => ({ + schoolId: currentSchoolId.value, + schoolFacultyId: formInline.value.schoolFacultyId, + schoolMajorId: formInline.value.schoolMajorId, + className: formInline.value.className, + operatorId: currentUserId.value, + sourceClassIds: sourceAdminClassIds.value.join(","), +})); + +const memberUploadData = computed(() => ({ + teachingClassId: selectedTeachingClass.value?.schoolClassId || "", + operatorId: currentUserId.value, +})); + +function isMultiTeachingClassConflict(error) { + return error?.response?.data?.code === 409; +} + +function multiTeachingClassMessage(error) { + const body = error?.response?.data || {}; + const students = Array.isArray(body.data) ? body.data : []; + const details = students.length ? `\n${students.slice(0, 12).join("、")}${students.length > 12 ? " 等" : ""}` : ""; + return `${body.msg || "部分学生已在其他教学班,是否继续操作?"}${details}`; +} + +function buildUploadFormData(file, data, confirmMultiTeachingClass = false) { + const formData = new FormData(); + formData.append("file", file); + Object.keys(data).forEach((key) => { + formData.append(key, data[key] ?? ""); + }); + formData.append("confirmMultiTeachingClass", confirmMultiTeachingClass); + return formData; +} + function defaultForm() { return { schoolFacultyId: "", @@ -156,6 +277,17 @@ function defaultForm() { }; } +function defaultMemberParams() { + return { + index: 1, + size: 10, + schoolId: currentSchoolId.value, + schoolClassId: "", + name: "", + userName: "", + }; +} + function normalizeClassType(schoolClass) { if (schoolClass?.classType) { return schoolClass.classType; @@ -167,14 +299,17 @@ function canOperateClass(schoolClass) { if (schoolClass?.dataSource === "ZHIYUN") { return false; } - if (normalizeClassType(schoolClass) === "TEACHING") { - return schoolClass?.createdBy === currentUserId.value; + if (normalizeClassType(schoolClass) === "ADMIN") { + return !schoolClass?.schoolId || schoolClass.schoolId === currentSchoolId.value; } - return isManagerTeacher.value || schoolClass?.createdBy === currentUserId.value; + return schoolClass?.createdBy === currentUserId.value; } -function getList() { - loading.value = true; +function getList(options = {}) { + const { showLoading = true } = options; + if (showLoading) { + loading.value = true; + } indexApi .getClassesList(paramsquery.value) .then((res) => { @@ -182,7 +317,9 @@ function getList() { schoolTotal.value = res.data?.total || 0; }) .finally(() => { - loading.value = false; + if (showLoading) { + loading.value = false; + } }); } @@ -192,33 +329,59 @@ function handleTabChange() { } function openCreateDialog() { + if (!canCreateCurrentClass.value) { + proxy.$message.warning("普通教师不能新增行政班"); + return; + } isCreate.value = true; formInline.value = defaultForm(); + sourceAdminClassIds.value = []; dialogVisible.value = true; } function editClass(row) { + const schoolClass = row?.schoolClass || row; + const schoolFacultyId = schoolClass?.schoolFacultyId || row?.schoolFacultyId || ""; + const schoolMajorId = schoolClass?.schoolMajorId || row?.schoolMajorId || ""; isCreate.value = false; formInline.value = { - ...row, - classType: normalizeClassType(row), + ...schoolClass, + schoolFacultyId, + schoolMajorId, + classType: normalizeClassType(schoolClass), userId: currentUserId.value, }; + sourceAdminClassIds.value = []; + majorListForForm.value = []; dialogVisible.value = true; + hydrateClassFormOptions(formInline.value); } -function addClass() { +function addClass(confirmMultiTeachingClass = false) { const payload = { ...formInline.value, schoolId: currentSchoolId.value, classType: activeClassType.value, + sourceClassIds: activeClassType.value === "TEACHING" ? sourceAdminClassIds.value.join(",") : "", userId: currentUserId.value, + confirmMultiTeachingClass, }; - indexApi.addClass(payload).then(() => { - proxy.$message.success("添加成功"); - dialogVisible.value = false; - getList(); - }); + indexApi + .addClass(payload) + .then(() => { + proxy.$message.success("添加成功"); + dialogVisible.value = false; + getList({ showLoading: false }); + refreshAllClassList(); + }) + .catch((error) => { + if (!isMultiTeachingClassConflict(error)) return; + ElMessageBox.confirm(multiTeachingClassMessage(error), "学生已在其他教学班", { + confirmButtonText: "继续创建", + cancelButtonText: "取消", + type: "warning", + }).then(() => addClass(true)); + }); } function updateClass() { @@ -232,17 +395,249 @@ function updateClass() { indexApi.updateClass(payload).then(() => { proxy.$message.success("编辑成功"); dialogVisible.value = false; - getList(); + getList({ showLoading: false }); + refreshAllClassList(); }); } function submitForm() { proxy.$refs.formInlineref.validate((valid) => { if (!valid) return; + if (isCreate.value && activeClassType.value === "TEACHING" && sourceAdminClassIds.value.length === 0) { + proxy.$message.warning("请选择来源行政班"); + return; + } isCreate.value ? addClass() : updateClass(); }); } +async function beforeImportCreate(file) { + if (activeClassType.value !== "TEACHING") { + proxy.$message.warning("请切换到教学班"); + return false; + } + if (!formInline.value.schoolFacultyId || !formInline.value.schoolMajorId || !formInline.value.className) { + proxy.$message.warning("请先填写院系、专业和班级名称"); + return false; + } + if (sourceAdminClassIds.value.length === 0) { + proxy.$message.warning("请选择来源行政班"); + return false; + } + const total = await getSelectedSourceAdminStudentTotal(); + if (total <= 0) { + proxy.$message.warning("所选来源行政班暂无学生,请先在学生管理中导入行政班学生后再创建教学班"); + return false; + } + if (!/\.(xls|xlsx)$/i.test(file.name)) { + proxy.$message.warning("请选择xls或xlsx格式文件"); + return false; + } + return true; +} + +async function getSelectedSourceAdminStudentTotal() { + const results = await Promise.all(sourceAdminClassIds.value.map((schoolClassId) => { + return indexApi.getStudentList({ + index: 1, + size: 1, + schoolId: currentSchoolId.value, + schoolClassId, + }); + })); + return results.reduce((sum, res) => sum + Number(res.data?.total || 0), 0); +} + +function uploadImportCreate(options, confirmMultiTeachingClass = false) { + const formData = buildUploadFormData(options.file, importCreateUploadData.value, confirmMultiTeachingClass); + return indexApi + .uploadTeachingClassByStudentMembers(formData) + .then((res) => { + handleImportCreateSuccess(res); + }) + .catch((error) => { + if (!isMultiTeachingClassConflict(error)) return; + return ElMessageBox.confirm(multiTeachingClassMessage(error), "学生已在其他教学班", { + confirmButtonText: "继续导入创建", + cancelButtonText: "取消", + type: "warning", + }).then(() => uploadImportCreate(options, true)); + }); +} + +function handleImportCreateSuccess(res) { + if (res?.statusCode && res.statusCode !== "OK" && res.statusCode !== 200) { + proxy.$message.error(res?.message || "导入创建失败"); + return; + } + proxy.$message.success("导入创建成功"); + dialogVisible.value = false; + getList(); + refreshAllClassList(); +} + +function openMemberDialog(schoolClass) { + selectedTeachingClass.value = schoolClass; + memberParams.value = { + ...defaultMemberParams(), + schoolClassId: schoolClass.schoolClassId, + }; + memberDialogVisible.value = true; + getMemberList(); +} + +function closeMemberDialog() { + selectedTeachingClass.value = null; + memberList.value = []; + memberTotal.value = 0; + memberParams.value = defaultMemberParams(); +} + +function getMemberList() { + if (!memberParams.value.schoolClassId) { + return; + } + memberLoading.value = true; + indexApi + .getStudentList(memberParams.value) + .then((res) => { + memberList.value = res.data?.list || []; + memberTotal.value = res.data?.total || 0; + }) + .finally(() => { + memberLoading.value = false; + }); +} + +function refreshMemberList() { + memberParams.value.name = ""; + memberParams.value.userName = ""; + memberParams.value.index = 1; + getMemberList(); +} + +function beforeImportMembers(file) { + if (!selectedTeachingClass.value?.schoolClassId) { + proxy.$message.warning("请先选择教学班"); + return false; + } + if (!selectedTeachingClassCanOperate.value) { + proxy.$message.warning("只能维护自己创建的教学班成员"); + return false; + } + if (!/\.(xls|xlsx)$/i.test(file.name)) { + proxy.$message.warning("请选择xls或xlsx格式文件"); + return false; + } + return true; +} + +function uploadMemberImport(options, confirmMultiTeachingClass = false) { + const formData = buildUploadFormData(options.file, memberUploadData.value, confirmMultiTeachingClass); + return indexApi + .uploadTeachingClassStudentMembers(formData) + .then((res) => { + handleMemberImportSuccess(res); + }) + .catch((error) => { + if (!isMultiTeachingClassConflict(error)) return; + return ElMessageBox.confirm(multiTeachingClassMessage(error), "学生已在其他教学班", { + confirmButtonText: "继续导入", + cancelButtonText: "取消", + type: "warning", + }).then(() => uploadMemberImport(options, true)); + }); +} + +function handleMemberImportSuccess(res) { + if (res?.statusCode && res.statusCode !== "OK" && res.statusCode !== 200) { + proxy.$message.error(res?.message || "导入失败"); + return; + } + proxy.$message.success("导入成功"); + getMemberList(); +} + +function removeMember(row) { + ElMessageBox.confirm("确定将该学生从当前教学班移除吗?", "提示", { + confirmButtonText: "确定", + cancelButtonText: "取消", + type: "warning", + }).then(() => { + indexApi + .deleteStudent({ + userId: row.userId, + operatorId: currentUserId.value, + teachingClassId: selectedTeachingClass.value.schoolClassId, + }) + .then(() => { + proxy.$message.success("移除成功"); + getMemberList(); + }); + }); +} + +function initializeMember(row) { + ElMessageBox.confirm("确定初始化该学生在当前教学班的实训数据吗?该操作不可恢复。", "提示", { + confirmButtonText: "确定", + cancelButtonText: "取消", + type: "warning", + }).then(() => { + indexApi + .initializeTeachingClassStudentTrainingData({ + teachingClassId: selectedTeachingClass.value.schoolClassId, + studentUserId: row.userId, + operatorId: currentUserId.value, + }) + .then(() => { + proxy.$message.success("初始化成功"); + }); + }); +} + +async function downloadMemberImportTemplate() { + const XLSX = await import("xlsx"); + const templateSheet = XLSX.utils.aoa_to_sheet([ + ["所属院系", "所属专业", "行政班", "学号", "学生姓名", "性别", "手机号", "邮箱"], + ["商学院", "电子商务", "2026电子商务1班-行政班", "20260001", "张三", "男", "13800000000", "zhangsan@example.com"], + ]); + templateSheet["!cols"] = [{ wch: 18 }, { wch: 18 }, { wch: 28 }, { wch: 18 }, { wch: 16 }, { wch: 10 }, { wch: 18 }, { wch: 26 }]; + const guideSheet = XLSX.utils.aoa_to_sheet([ + ["填写说明"], + ["1. 本模板用于向当前教学班追加成员。"], + ["2. 不要填写任何数据ID,院系、专业、行政班请填写页面中的名称。"], + ["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"); +} + +async function downloadTeachingClassImportTemplate() { + const XLSX = await import("xlsx"); + const templateSheet = XLSX.utils.aoa_to_sheet([ + ["所属院系", "所属专业", "行政班", "学号", "学生姓名", "性别", "手机号", "邮箱"], + ["商学院", "电子商务", "2026电子商务1班-行政班", "20260001", "张三", "男", "13800000000", "zhangsan@example.com"], + ]); + templateSheet["!cols"] = [{ wch: 18 }, { wch: 18 }, { wch: 28 }, { wch: 18 }, { wch: 16 }, { wch: 10 }, { wch: 18 }, { wch: 26 }]; + const guideSheet = XLSX.utils.aoa_to_sheet([ + ["填写说明"], + ["1. 先在新增教学班弹窗中选择院系、专业并填写班级名称;来源行政班可以不选。"], + ["2. 不要填写任何数据ID,院系、专业、行政班请填写页面中的名称。"], + ["3. 学生必须已存在于对应行政班;如果行政班暂无学生,请先在学生管理中导入行政班学生。"], + ["4. 系统会按模板学生名单加入新教学班;学生若已在其他教学班,将自动转入新教学班。"], + ["5. 任意一行校验失败时,整批导入创建失败,教学班和成员关系都不会写入。"], + ]); + guideSheet["!cols"] = [{ wch: 96 }]; + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, templateSheet, "教学班名单创建模板"); + XLSX.utils.book_append_sheet(workbook, guideSheet, "填写说明"); + XLSX.writeFile(workbook, "教学班名单创建模板.xlsx"); +} + function deleteClass(id) { ElMessageBox.confirm("确定删除该班级吗?", "提示", { confirmButtonText: "确定", @@ -252,6 +647,7 @@ function deleteClass(id) { indexApi.deleteClass({ schoolClassId: id, userId: currentUserId.value }).then(() => { proxy.$message.success("删除成功"); getList(); + refreshAllClassList(); }); }); } @@ -279,11 +675,13 @@ function refresh() { function onClose() { formInline.value = defaultForm(); + sourceAdminClassIds.value = []; } function getFacultyList() { - indexApi.seleteSchoolFacultyList({ schoolId: currentSchoolId.value }).then((res) => { + return indexApi.seleteSchoolFacultyList({ schoolId: currentSchoolId.value }).then((res) => { facultyList.value = res.data || []; + return facultyList.value; }); } @@ -298,19 +696,79 @@ function getMajorList() { }); } +function loadMajorListForForm(schoolFacultyId) { + if (!schoolFacultyId) { + majorListForForm.value = []; + return Promise.resolve([]); + } + return indexApi.getMajorListBySchoolId({ schoolFacultyId }).then((res) => { + majorListForForm.value = res.data || []; + return majorListForForm.value; + }); +} + function getMajorListForForm() { + sourceAdminClassIds.value = []; formInline.value.schoolMajorId = ""; - if (!formInline.value.schoolFacultyId) { - majorListForForm.value = []; + loadMajorListForForm(formInline.value.schoolFacultyId); +} + +async function hydrateClassFormOptions(row) { + const schoolMajorId = row?.schoolMajorId; + if (!schoolMajorId) { return; } - indexApi.getMajorListBySchoolId({ schoolFacultyId: formInline.value.schoolFacultyId }).then((res) => { - majorListForForm.value = res.data || []; + if (row.schoolFacultyId) { + await loadMajorListForForm(row.schoolFacultyId); + return; + } + const faculties = facultyList.value.length ? facultyList.value : await getFacultyList(); + const results = await Promise.all( + faculties.map((faculty) => + indexApi + .getMajorListBySchoolId({ schoolFacultyId: faculty.schoolFacultyId }) + .then((res) => ({ + facultyId: faculty.schoolFacultyId, + majors: res.data || [], + })) + .catch(() => ({ + facultyId: faculty.schoolFacultyId, + majors: [], + })) + ) + ); + const matched = results.find((item) => item.majors.some((major) => major.schoolMajorId === schoolMajorId)); + if (!matched) { + return; + } + majorListForForm.value = matched.majors; + formInline.value = { + ...formInline.value, + schoolFacultyId: matched.facultyId, + schoolMajorId, + }; +} + +function handleFormMajorChange() { + sourceAdminClassIds.value = []; +} + +function getAllClassList() { + indexApi.getClassListBySchoolId({ schoolId: currentSchoolId.value }).then((res) => { + allClassList.value = res.data || []; + }); +} + +function refreshAllClassList() { + indexApi.clearClassListBySchoolIdCache(currentSchoolId.value); + indexApi.getClassListBySchoolId({ schoolId: currentSchoolId.value }, { force: true }).then((res) => { + allClassList.value = res.data || []; }); } onMounted(() => { getList(); + getAllClassList(); getFacultyList(); }); @@ -349,8 +807,46 @@ onMounted(() => { width: 100%; } + .action-buttons { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + flex-wrap: wrap; + + :deep(.el-button) { + margin-left: 0; + min-width: 56px; + padding: 6px 10px; + line-height: 1; + } + } + .form-control { width: 100%; } + + .import-create-actions { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + } + + .member-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-bottom: 12px; + } + + .member-query { + width: 180px; + } + + .member-table { + width: 100%; + } } diff --git a/src/views/teacherEnd/student/index.vue b/src/views/teacherEnd/student/index.vue index d4eb0a3..aff3eb3 100644 --- a/src/views/teacherEnd/student/index.vue +++ b/src/views/teacherEnd/student/index.vue @@ -29,8 +29,13 @@
新增 - - 导入 + + 导入 导出 导入模版下载 @@ -125,7 +130,6 @@ import useUserStore from "@/store/modules/user"; const userStore = useUserStore(); const currentUserId = computed(() => userStore.userInfo.userId); const currentSchoolId = computed(() => userStore.userInfo.schoolId); -const isManagerTeacher = computed(() => userStore.userInfo.teacherAdmin === true || String(userStore.userInfo.roleId || userStore.userInfo.role) === "1"); import * as indexApi from "@/api/teacher"; const dialogVisible = ref(false); const loading = ref(false); @@ -154,6 +158,12 @@ const paramsquery = ref({ }); const classInfo = ref({}); const schoolTotal = ref(0); +function buildStudentImportFormData(file) { + const formData = new FormData(); + formData.append("file", file); + formData.append("operatorId", currentUserId.value); + return formData; +} // 新增编辑状态 const status = ref(false); // 院校list @@ -167,7 +177,7 @@ const getList = () => { }; // 删除院校 const delSchool = (id) => { - indexApi.deleteStudent({ userId: id.userId, operatorId: currentUserId.value }).then((res) => { + indexApi.deleteStudent({ userId: id.userId, operatorId: currentUserId.value, teachingClassId: currentTeachingClassId() }).then((res) => { proxy.$message.success("删除成功"); getList(); }); @@ -202,6 +212,7 @@ const add = () => { const edit = () => { formInline.value.status = true; formInline.value.operatorId = currentUserId.value; + formInline.value.teachingClassId = currentTeachingClassId(); indexApi.updateStudent(formInline.value).then((res) => { proxy.$message.success("编辑成功"); dialogVisible.value = false; @@ -248,7 +259,17 @@ const getMajorList = () => { majorList.value = res.data; }); }; +const uploadStudents = (options) => { + const formData = buildStudentImportFormData(options.file); + return indexApi.importTeacherStudents(formData).then((res) => { + handleSuccess(res); + }); +}; const handleSuccess = (res, file) => { + if (res?.statusCode && res.statusCode !== "OK" && res.statusCode !== 200) { + proxy.$message.error(res?.message || "导入失败"); + return; + } getList(); proxy.$modal.msgSuccess("导入成功"); }; @@ -265,15 +286,54 @@ const exportTeacher = () => { }); }; // 导入模版下载 -const downloadTemplate = () => { - const a = document.createElement("a"); - a.href = `http://118.31.7.2:147/file/学生信息表.xlsx`; - a.download = "模板文件.xlsx"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); +const buildOwnedAdminClassRows = async () => { + const majorGroups = await Promise.all( + facultyList.value.map((faculty) => + indexApi + .getMajorListBySchoolId({ schoolFacultyId: faculty.schoolFacultyId }) + .then((res) => (res.data || []).map((major) => ({ ...major, schoolFacultyName: faculty.schoolFacultyName }))) + .catch(() => []) + ) + ); + const allMajors = majorGroups.flat(); + return classList.value + .filter((item) => canUseAdminClassForStudent(item)) + .map((item) => { + const major = allMajors.find((majorItem) => majorItem.schoolMajorId === item.schoolMajorId) || majorList.value.find((majorItem) => majorItem.schoolMajorId === item.schoolMajorId); + const faculty = facultyList.value.find((facultyItem) => facultyItem.schoolFacultyId === (major?.schoolFacultyId || item.schoolFacultyId)); + return [faculty?.schoolFacultyName || item.schoolFacultyName || "", major?.schoolMajorName || item.schoolMajorName || "", item.className || ""]; + }) + .filter((row) => row.every(Boolean)); +}; + +const downloadTemplate = async () => { + const XLSX = await import("xlsx"); + const adminClassRows = await buildOwnedAdminClassRows(); + const firstAdminClass = adminClassRows[0] || ["电子商务学院", "电子商务", "行政一班"]; + const templateSheet = XLSX.utils.aoa_to_sheet([ + ["姓名", "学号", "性别", "所属院系", "所属专业", "所属班级", "手机", "邮箱"], + ["张三", "20260001", "男", firstAdminClass[0], firstAdminClass[1], firstAdminClass[2], "13800000000", "student@example.com"], + ]); + templateSheet["!cols"] = [{ wch: 14 }, { wch: 18 }, { wch: 10 }, { wch: 24 }, { wch: 24 }, { wch: 20 }, { wch: 16 }, { wch: 28 }]; + const dataRows = adminClassRows.length ? adminClassRows : [["暂无可导入行政班", "请先维护专业", "请先创建行政班"]]; + const dataSheet = XLSX.utils.aoa_to_sheet([["所属院系", "所属专业", "所属班级"], ...dataRows]); + dataSheet["!cols"] = [{ wch: 28 }, { wch: 28 }, { wch: 24 }]; + const guideSheet = XLSX.utils.aoa_to_sheet([ + ["填写说明"], + ["1. 本模板用于学生账号导入,不要填写任何数据ID。"], + ["2. 所属院系、所属专业、所属班级请使用基础数据页中的名称。"], + ["3. 只能导入本校行政班,不能导入教学班或其他学校的班级。"], + ["4. 性别可填写男、女,也可留空。"], + ["5. 任意一行校验失败时,整批导入失败并返回失败原因。"], + ]); + guideSheet["!cols"] = [{ wch: 76 }]; + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, templateSheet, "学生导入模板"); + XLSX.utils.book_append_sheet(workbook, dataSheet, "基础数据"); + XLSX.utils.book_append_sheet(workbook, guideSheet, "填写说明"); + XLSX.writeFile(workbook, "学生账号导入模板.xlsx"); }; -// 查询班级下拉 + const classList = ref([]); const normalizeClassType = (schoolClass) => { if (schoolClass?.classType) return schoolClass.classType; @@ -282,18 +342,29 @@ const normalizeClassType = (schoolClass) => { const sameValue = (left, right) => String(left || "") === String(right || ""); const canUseAdminClassForStudent = (schoolClass) => { if (normalizeClassType(schoolClass) !== "ADMIN") return false; - if (isManagerTeacher.value) { - return !schoolClass?.schoolId || sameValue(schoolClass.schoolId, currentSchoolId.value); - } - return sameValue(schoolClass?.createdBy, currentUserId.value); + return !schoolClass?.schoolId || sameValue(schoolClass.schoolId, currentSchoolId.value); }; +const classMap = computed(() => new Map(classList.value.map((item) => [item.schoolClassId, item]))); const teachingClassList = computed(() => classList.value.filter((item) => normalizeClassType(item) === "TEACHING")); const classOwnerMap = computed(() => { return new Map(classList.value.map((item) => [item.schoolClassId, item.createdBy])); }); const canOperateStudent = (row) => { - const teachingClassId = paramsquery.value.schoolClassId || row.schoolClassId; - return sameValue(classOwnerMap.value.get(teachingClassId), currentUserId.value); + const teachingClassId = paramsquery.value.schoolClassId; + if (teachingClassId) { + return sameValue(classOwnerMap.value.get(teachingClassId), currentUserId.value); + } + const adminClass = classMap.value.get(row.schoolClassId); + return normalizeClassType(adminClass) === "ADMIN" + && (!adminClass?.schoolId || sameValue(adminClass.schoolId, currentSchoolId.value)); +}; +const currentTeachingClassId = () => paramsquery.value.schoolClassId || ""; +const beforeImportStudents = (file) => { + if (!/\.(xls|xlsx)$/i.test(file.name)) { + proxy.$message.warning("请选择xls或xlsx格式文件"); + return false; + } + return true; }; const getClassList = () => { indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId }).then((res) => { @@ -318,7 +389,7 @@ const initPassword = (row, status) => { type: "warning", }) .then(() => { - indexApi.initStudentPassword({ userId: row.userId, operatorId: currentUserId.value }).then((res) => { + indexApi.initStudentPassword({ userId: row.userId, operatorId: currentUserId.value, teachingClassId: currentTeachingClassId() }).then((res) => { proxy.$message.success("密码初始化成功"); getList(); });