feat: open admin classes to school teachers

dev-QQq
chenyuan 2 months ago
parent fd0ebc668b
commit c2fcc259c7

@ -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;

@ -17,7 +17,7 @@
<el-button :icon="Refresh" @click="refresh"></el-button>
</div>
<div class="table-actions">
<div v-if="canCreateCurrentClass" class="table-actions">
<el-button type="primary" :icon="Plus" @click="openCreateDialog">{{ activeClassType === 'ADMIN' ? '' : '' }}</el-button>
</div>
@ -33,9 +33,9 @@
<span>{{ row.schoolMajorName || "-" }}</span>
</template>
</el-table-column>
<el-table-column v-if="activeClassType === 'TEACHING'" label="创建人" align="center">
<el-table-column label="创建人" align="center">
<template #default="{ row }">
<span>{{ row.schoolClass.createdByName || row.schoolClass.createdBy || "-" }}</span>
<span>{{ row.createdByName || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="数据来源" align="center" width="120">
@ -50,16 +50,21 @@
<span>{{ formatDate(row.schoolClass.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="260">
<el-table-column label="操作" align="center" width="420">
<template #default="{ row }">
<template v-if="canOperateClass(row.schoolClass)">
<el-button type="primary" text @click="editClass(row.schoolClass)"></el-button>
<el-button type="danger" text @click="deleteClass(row.schoolClass.schoolClassId)"></el-button>
<el-button v-if="activeClassType === 'TEACHING'" type="warning" text @click="initializeClass(row.schoolClass)">
数据初始化
<div class="action-buttons">
<el-button v-if="activeClassType === 'TEACHING'" type="primary" text @click="openMemberDialog(row.schoolClass)">
成员管理
</el-button>
</template>
<el-tag v-else type="info">仅查看</el-tag>
<template v-if="canOperateClass(row.schoolClass)">
<el-button type="primary" text @click="editClass(row)"></el-button>
<el-button type="danger" text @click="deleteClass(row.schoolClass.schoolClassId)"></el-button>
<el-button v-if="activeClassType === 'TEACHING'" type="warning" text @click="initializeClass(row.schoolClass)">
数据初始化
</el-button>
</template>
<el-tag v-else type="info">仅查看</el-tag>
</div>
</template>
</el-table-column>
</el-table>
@ -69,18 +74,37 @@
<el-dialog v-model="dialogVisible" width="520px" :title="dialogTitle" class="teacher-dialog" @close="onClose">
<el-form ref="formInlineref" :model="formInline" label-width="96px" :rules="rules">
<el-form-item label="所属院系" prop="schoolFacultyId">
<el-select v-model="formInline.schoolFacultyId" placeholder="请选择院系" clearable class="form-control" @change="getMajorListForForm" :disabled="!isCreate">
<el-select v-model="formInline.schoolFacultyId" placeholder="请选择院系" clearable class="form-control" @change="getMajorListForForm">
<el-option v-for="faculty in facultyList" :key="faculty.schoolFacultyId" :label="faculty.schoolFacultyName" :value="faculty.schoolFacultyId" />
</el-select>
</el-form-item>
<el-form-item label="所属专业" prop="schoolMajorId">
<el-select v-model="formInline.schoolMajorId" placeholder="请选择专业" clearable class="form-control" :disabled="!isCreate">
<el-select v-model="formInline.schoolMajorId" placeholder="请选择专业" clearable class="form-control" @change="handleFormMajorChange">
<el-option v-for="major in majorListForForm" :key="major.schoolMajorId" :label="major.schoolMajorName" :value="major.schoolMajorId" />
</el-select>
</el-form-item>
<el-form-item v-if="activeClassType === 'TEACHING' && isCreate" label="来源行政班" prop="sourceClassIds">
<el-select v-model="sourceAdminClassIds" placeholder="请选择来源行政班" clearable multiple collapse-tags collapse-tags-tooltip class="form-control">
<el-option v-for="schoolClass in adminClassOptions" :key="schoolClass.schoolClassId" :label="schoolClass.className" :value="schoolClass.schoolClassId" />
</el-select>
</el-form-item>
<el-form-item label="班级名称" prop="className">
<el-input v-model="formInline.className" placeholder="请输入班级名称" class="form-control" />
</el-form-item>
<el-form-item v-if="activeClassType === 'TEACHING' && isCreate" label="名单创建">
<div class="import-create-actions">
<el-upload
action="/api/user/uploadTeachingClassByStudentMembers"
:data="importCreateUploadData"
:show-file-list="false"
:before-upload="beforeImportCreate"
:http-request="uploadImportCreate"
>
<el-button type="primary">导入名单创建</el-button>
</el-upload>
<el-button @click="downloadTeachingClassImportTemplate"></el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
@ -89,6 +113,51 @@
</span>
</template>
</el-dialog>
<el-dialog v-model="memberDialogVisible" width="900px" :title="memberDialogTitle" class="teacher-dialog" @close="closeMemberDialog">
<div class="member-toolbar">
<el-input v-model="memberParams.name" placeholder="学生姓名" clearable class="member-query" />
<el-input v-model="memberParams.userName" placeholder="学号" clearable class="member-query" />
<el-button type="primary" :icon="Search" @click="getMemberList"></el-button>
<el-button :icon="Refresh" @click="refreshMemberList"></el-button>
<template v-if="selectedTeachingClassCanOperate">
<el-upload
action="/api/user/uploadTeachingClassStudentMembers"
:data="memberUploadData"
:show-file-list="false"
:before-upload="beforeImportMembers"
:http-request="uploadMemberImport"
>
<el-button type="primary">导入成员</el-button>
</el-upload>
<el-button @click="downloadMemberImportTemplate"></el-button>
</template>
</div>
<el-table :data="memberList" border v-loading="memberLoading" class="member-table" :header-cell-style="tableHeaderStyle">
<el-table-column label="学生姓名" prop="name" align="center" />
<el-table-column label="学号" prop="username" align="center" />
<el-table-column label="所属班级" prop="className" align="center" />
<el-table-column label="电话" prop="phone" align="center" />
<el-table-column label="邮箱" prop="email" align="center" />
<el-table-column label="账号来源" prop="codeFrom" align="center" />
<el-table-column label="操作" align="center" width="210">
<template #default="{ row }">
<template v-if="selectedTeachingClassCanOperate">
<el-button type="danger" text @click="removeMember(row)"></el-button>
<el-button type="primary" text @click="initializeMember(row)"></el-button>
</template>
<el-tag v-else type="info">仅查看</el-tag>
</template>
</el-table-column>
</el-table>
<pagination
v-show="memberTotal > 0"
:total="memberTotal"
v-model:page="memberParams.index"
v-model:limit="memberParams.size"
@pagination="getMemberList"
/>
</el-dialog>
</div>
</template>
@ -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();
});
</script>
@ -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%;
}
}
</style>

@ -29,8 +29,13 @@
<el-col>
<div style="display: flex; gap: 10px">
<el-button type="primary" :icon="Plus" @click="addschool"></el-button>
<el-upload action="/api/user/uploadStudentUserInfo" :data="{ userId: currentUserId }" :show-file-list="false" :on-success="(res, file) => handleSuccess(res, file)">
<el-button type="primary" @click="editSchool(row)"></el-button>
<el-upload
action="/api/user/importTeacherStudents"
:show-file-list="false"
:before-upload="beforeImportStudents"
:http-request="uploadStudents"
>
<el-button type="primary">导入</el-button>
</el-upload>
<el-button type="warning" @click="exportTeacher"></el-button>
<el-button type="warning" @click="downloadTemplate"></el-button>
@ -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();
});

Loading…
Cancel
Save