You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/teacherEnd/student/index.vue

538 lines
22 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!--
* @Author: qinzhenpen qzp1807@126.com
* @Date: 2025-05-13 16:22:46
* @LastEditors: qinzhenpen qzp1807@126.com
* @LastEditTime: 2025-05-22 09:41:23
* @FilePath: \e-commerce-internet\src\views\teacherEnd\school\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="teacher app-container">
<div class="df">
<div class="online">
<span>班级:</span>
<el-select v-model="paramsquery.schoolClassId" placeholder="" style="width: 14rem" clearable class="ml10" @change="handleClassFilterChange">
<el-option v-for="school in studentClassOptions" :key="school.schoolClassId" :label="school.className" :value="school.schoolClassId" />
</el-select>
</div>
<div class="online">
<span>学生姓名:</span>
<el-input style="width: 14rem" v-model="paramsquery.name" class="ml10"></el-input>
</div>
<div class="online">
<span>学生学号:</span>
<el-input style="width: 14rem" v-model="paramsquery.userName" class="ml10"></el-input>
<el-button @click="getList" type="primary" class="ml10" :icon="Search">搜索</el-button>
</div>
</div>
<el-row class="mt10">
<el-col>
<div style="display: flex; gap: 10px">
<el-button type="primary" :icon="Plus" @click="addschool">新增</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>
<el-button type="primary" :icon="Refresh" @click="refresh">刷新</el-button>
</div>
</el-col>
</el-row>
<el-table
:data="tableData"
border
v-loading="loading"
style="width: 100%; margin-top: 20px"
:header-cell-style="{
color: '#fff',
fontSize: '14px',
backgroundColor: '#6A7DE9!important',
}"
>
<el-table-column label="所属班级" prop="className" align="center"></el-table-column>
<el-table-column label="学生姓名" prop="name" align="center"></el-table-column>
<el-table-column label="学号" prop="username" align="center"></el-table-column>
<el-table-column label="账号来源" prop="codeFrom" align="center"></el-table-column>
<el-table-column label="创建日期" prop="createTime" align="center">
<template #default="{ row }">
<span>{{ formatDate(row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="360">
<template #default="{ row }">
<div class="student-action-group">
<el-button type="info" text @click="editSchool(row)">编辑</el-button>
<el-button type="danger" text @click="delSchool(row)">删除</el-button>
<el-button type="success" text @click="initPassword(row, 0)">密码初始化</el-button>
<el-button type="warning" text @click="initPassword(row, 1)">数据初始化</el-button>
</div>
</template>
</el-table-column>
</el-table>
<pagination v-show="schoolTotal > 0" :total="schoolTotal" v-model:page="paramsquery.index" v-model:limit="paramsquery.size" @pagination="getList" />
<el-dialog v-model="dialogVisible" width="560px" :title="status ? '新增学生' : '编辑学生'" class="teacher-dialog" @close="onClose">
<el-form :model="formInline" class="demo-form-inline" ref="formInlineref" label-width="100px" style="width: 80%" :rules="studentFormRules">
<template v-if="!isTeacherRosterMode">
<el-form-item label="所属院系:" prop="schoolFacultyId">
<el-select v-model="formInline.schoolFacultyId" placeholder="" clearable @change="getMajorList" style="width: 20rem" v-if="status">
<el-option v-for="school in facultyList" :key="school.schoolId" :label="school.schoolFacultyName" :value="school.schoolFacultyId" />
</el-select>
<el-input v-else v-model="classInfo.name" style="width: 20rem" disabled></el-input>
</el-form-item>
<el-form-item label="所属专业:" prop="schoolMajorId">
<el-select v-model="formInline.schoolMajorId" placeholder="" clearable style="width: 20rem" v-if="status" @change="getClassByMajor">
<el-option v-for="major in majorList" :key="major.schoolMajorId" :label="major.schoolMajorName" :value="major.schoolMajorId" />
</el-select>
<el-input v-else v-model="classInfo.value" placeholder="请输入所属专业:" style="width: 20rem" disabled></el-input>
</el-form-item>
<el-form-item label="所属班级:" prop="schoolClassId">
<el-select v-model="formInline.schoolClassId" placeholder="" clearable style="width: 20rem" v-if="status">
<el-option v-for="major in classList1" :key="major.schoolClassId" :label="major.className" :value="major.schoolClassId" />
</el-select>
<el-input v-else v-model="classInfo.className" placeholder="请输入所属班级:" style="width: 20rem" disabled></el-input>
</el-form-item>
</template>
<el-form-item v-if="isTeacherRosterMode" label="所属教学班:" prop="teachingClassId">
<el-select v-model="formInline.teachingClassId" placeholder="请选择教学班" clearable style="width: 20rem">
<el-option v-for="schoolClass in teachingClassOptions" :key="schoolClass.schoolClassId" :label="schoolClass.className" :value="schoolClass.schoolClassId" />
</el-select>
</el-form-item>
<el-form-item label="学生姓名:" prop="name">
<el-input v-model="formInline.name" style="width: 20rem" placeholder=""></el-input>
</el-form-item>
<el-form-item label="学号:" prop="userName">
<el-input v-model="formInline.userName" style="width: 20rem" placeholder=""></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footers">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { Search, Plus, Refresh } from "@element-plus/icons-vue";
import { formatDate } from "@/utils/index";
import { ElMessageBox } from "element-plus";
const { proxy } = getCurrentInstance();
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
const currentUserId = computed(() => userStore.userInfo.userId);
const currentSchoolId = computed(() => userStore.userInfo.schoolId);
const isTeacherRosterMode = computed(() => userStore.schoolProductConfig?.studentRosterOwner === "TEACHER");
const isManagerTeacher = computed(() => userStore.userInfo.teacherAdmin === true);
import * as indexApi from "@/api/teacher";
const dialogVisible = ref(false);
const loading = ref(false);
const formInline = ref({
schoolFacultyId: "",
schoolId: userStore.userInfo.schoolId,
schoolMajorId: "",
schoolMajorName: "",
schoolClassId: "",
teachingClassId: "",
schoolName: "",
userName: "",
});
const createEmptyForm = () => ({
schoolFacultyId: "",
schoolId: currentSchoolId.value,
schoolMajorId: "",
schoolMajorName: "",
schoolClassId: "",
teachingClassId: "",
schoolName: "",
userName: "",
name: "",
});
const organizationRules = {
schoolFacultyId: [{ required: true, message: "请选择所属院系", trigger: "change" }],
schoolMajorId: [{ required: true, message: "请选择所属专业", trigger: "change" }],
schoolClassId: [{ required: true, message: "请选择所属班级", trigger: "change" }],
};
const studentIdentityRules = {
name: [{ required: true, message: "请输入学生姓名", trigger: "blur" }],
userName: [{ required: true, message: "请输入学号", trigger: "blur" }],
};
const teacherRosterRules = {
...studentIdentityRules,
teachingClassId: [{ required: true, message: "请选择所属教学班", trigger: "change" }],
};
const studentFormRules = computed(() => (isTeacherRosterMode.value ? teacherRosterRules : { ...organizationRules, ...studentIdentityRules }));
const paramsquery = ref({
index: 1,
size: 10,
schoolId: userStore.userInfo.schoolId,
});
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
const tableData = ref([]);
// 获取院校列表
const getList = () => {
indexApi.getStudentList({ ...paramsquery.value, operatorId: currentUserId.value }).then((res) => {
tableData.value = res.data.list;
schoolTotal.value = res.data.total;
});
};
// 删除院校
const delSchool = (id) => {
indexApi.deleteStudent({ userId: id.userId, operatorId: currentUserId.value, teachingClassId: resolveRowTeachingClassId(id) }).then((res) => {
proxy.$message.success("删除成功");
getList();
});
};
// 新增院校
const addschool = () => {
formInline.value = createEmptyForm();
classInfo.value = {};
classList1.value = [];
dialogVisible.value = true;
status.value = true;
};
const editSchool = (row) => {
console.log(row);
let data = { ...row };
formInline.value = data;
formInline.value.userName = row.username;
formInline.value.teachingClassId = isTeacherRosterMode.value ? (row.teachingClassId || currentTeachingClassId()) : "";
classInfo.value = {
className: row.className || row.schoolClassName || "",
};
dialogVisible.value = true;
status.value = false;
if (!isTeacherRosterMode.value && row.schoolMajorId) {
indexApi.getFacultyAndMajorName({ schoolMajorId: row.schoolMajorId }).then((res) => {
classInfo.value.name = Object.keys(res.data)[0];
classInfo.value.value = Object.values(res.data)[0];
});
}
};
const add = () => {
formInline.value.schoolId = currentSchoolId.value;
formInline.value.operatorId = currentUserId.value;
if (isTeacherRosterMode.value) {
indexApi.addTeacherRosterStudent(formInline.value).then((res) => {
proxy.$message.success("添加成功");
dialogVisible.value = false;
getList();
});
return;
}
formInline.value.schoolMajorName = majorList.value?.filter((item) => item.schoolMajorId == formInline.value.schoolMajorId)[0].schoolMajorName;
formInline.value.schoolClassName = classList1.value?.filter((item) => item.schoolClassId == formInline.value.schoolClassId)[0].className;
formInline.value.userId = currentUserId.value;
indexApi.addStudent(formInline.value).then((res) => {
proxy.$message.success("添加成功");
dialogVisible.value = false;
getList();
});
};
const edit = () => {
formInline.value.status = true;
formInline.value.operatorId = currentUserId.value;
if (isTeacherRosterMode.value) {
indexApi.updateTeacherRosterStudent(formInline.value).then((res) => {
proxy.$message.success("编辑成功");
dialogVisible.value = false;
getList();
});
return;
}
formInline.value.teachingClassId = currentTeachingClassId();
indexApi.updateStudent(formInline.value).then((res) => {
proxy.$message.success("编辑成功");
dialogVisible.value = false;
getList();
});
};
// 提交
const submitForm = () => {
proxy.$refs.formInlineref.validate((valid) => {
if (valid) {
status.value ? add() : edit();
} else {
return false;
}
});
};
const onClose = () => {
formInline.value = createEmptyForm();
classInfo.value = {};
classList1.value = [];
dialogVisible.value = false;
};
// 刷新
const refresh = () => {
loading.value = true;
setTimeout(() => {
loading.value = false;
getList();
}, 2000);
};
// 查询院系下拉列表
const facultyList = ref([]);
const getFacultyList = () => {
indexApi.seleteSchoolFacultyList({ schoolId: userStore.userInfo.schoolId }).then((res) => {
facultyList.value = res.data;
});
};
// 专业下拉框
const majorList = ref([]);
const getMajorList = () => {
if (!formInline.value.schoolFacultyId) return;
paramsquery.value.schoolMajorId = "";
indexApi.getMajorListBySchoolId({ schoolFacultyId: formInline.value.schoolFacultyId }).then((res) => {
majorList.value = res.data;
});
};
const uploadStudents = (options) => {
if (isTeacherRosterMode.value) {
const formData = new FormData();
formData.append("file", options.file);
return indexApi.importTeacherRosterStudents(formData).then((res) => {
handleSuccess(res);
});
}
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("导入成功");
};
// 导出教师账号
const exportTeacher = () => {
indexApi.exportStudentList({ schoolId: userStore.userInfo.schoolId, operatorId: currentUserId.value, schoolClassId: currentTeachingClassId() }).then((res) => {
const blob = new Blob([res], { type: "application/vnd.ms-excel" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "学生信息表格.xlsx");
link.click();
URL.revokeObjectURL(url);
});
};
// 导入模版下载
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");
if (isTeacherRosterMode.value) {
const exampleClassName = teachingClassOptions.value[0]?.className || "教学一班";
const templateSheet = XLSX.utils.aoa_to_sheet([
["学生姓名", "学号", "班级"],
["张三", "20260001", exampleClassName],
]);
templateSheet["!cols"] = [{ wch: 16 }, { wch: 18 }, { wch: 20 }];
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, templateSheet, "学生导入模板");
XLSX.writeFile(workbook, "学生账号导入模板.xlsx");
return;
}
const adminClassRows = await buildOwnedAdminClassRows();
const firstAdminClass = adminClassRows[0] || ["电子商务学院", "电子商务", "行政一班"];
const templateSheet = XLSX.utils.aoa_to_sheet([
["姓名", "学号", "性别", "所属院系", "所属专业", "所属班级"],
["张三", "20260001", "男", firstAdminClass[0], firstAdminClass[1], firstAdminClass[2]],
]);
templateSheet["!cols"] = [{ wch: 14 }, { wch: 18 }, { wch: 10 }, { wch: 24 }, { wch: 24 }, { wch: 20 }];
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;
return schoolClass?.createdBy ? "TEACHING" : "ADMIN";
};
const sameValue = (left, right) => String(left || "") === String(right || "");
const classCreateTime = (schoolClass) => {
if (!schoolClass?.createTime) return Number.MAX_SAFE_INTEGER;
const value = new Date(schoolClass.createTime).getTime();
return Number.isNaN(value) ? Number.MAX_SAFE_INTEGER : value;
};
const canUseAdminClassForStudent = (schoolClass) => {
if (normalizeClassType(schoolClass) !== "ADMIN") return false;
return !schoolClass?.schoolId || sameValue(schoolClass.schoolId, currentSchoolId.value);
};
const classMap = computed(() => new Map(classList.value.map((item) => [item.schoolClassId, item])));
const teachingClassOptions = computed(() =>
classList.value.filter(
(schoolClass) =>
normalizeClassType(schoolClass) === "TEACHING" &&
sameValue(schoolClass.schoolId, currentSchoolId.value) &&
(isManagerTeacher.value || sameValue(schoolClass.createdBy, currentUserId.value))
).sort((left, right) => classCreateTime(left) - classCreateTime(right))
);
const studentClassOptions = computed(() => teachingClassOptions.value);
const currentTeachingClassId = () => {
const selectedClass = classMap.value.get(paramsquery.value.schoolClassId);
return selectedClass && normalizeClassType(selectedClass) === "TEACHING" ? selectedClass.schoolClassId : "";
};
const resolveRowTeachingClassId = (row) => currentTeachingClassId() || row?.teachingClassId || "";
const beforeImportStudents = (file) => {
if (!/\.(xls|xlsx)$/i.test(file.name)) {
proxy.$message.warning("请选择xls或xlsx格式文件");
return false;
}
return true;
};
const getClassList = () => {
return indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId }).then((res) => {
classList.value = res.data;
});
};
const selectDefaultStudentClass = () => {
if (!paramsquery.value.schoolClassId) {
const ownTeachingClass = teachingClassOptions.value.find((schoolClass) => sameValue(schoolClass.createdBy, currentUserId.value));
paramsquery.value.schoolClassId = ownTeachingClass?.schoolClassId || "";
}
};
const handleClassFilterChange = () => {
paramsquery.value.index = 1;
getList();
};
const classList1 = ref([]);
// 根据专业查班级
const getClassByMajor = () => {
if (!formInline.value.schoolMajorId) return;
formInline.value.schoolClassId = "";
indexApi.getClassListByMajorId({ schoolMajorId: formInline.value.schoolMajorId }).then((res) => {
classList1.value = (res.data || []).filter((item) => canUseAdminClassForStudent(item));
});
};
//初始化密码
const initPassword = (row, status) => {
if (status == 0) {
ElMessageBox.confirm("确定初始化密码吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
indexApi.initStudentPassword({ userId: row.userId, operatorId: currentUserId.value, teachingClassId: resolveRowTeachingClassId(row) }).then((res) => {
proxy.$message.success("密码初始化成功");
getList();
});
})
.catch(() => {});
} else {
ElMessageBox.confirm("确定初始化当前学生数据吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
const teachingClassId = resolveRowTeachingClassId(row);
if (!teachingClassId) {
proxy.$message.warning("请先选择教学班");
return;
}
indexApi.initializeTeachingClassStudentTrainingData({
teachingClassId,
studentUserId: row.userId,
operatorId: currentUserId.value,
}).then((res) => {
proxy.$message.success("初始化成功");
getList();
});
})
.catch(() => {});
}
};
onMounted(async () => {
await userStore.ensureSchoolProductConfig().catch(() => null);
await getClassList();
selectDefaultStudentClass();
getList();
if (!isTeacherRosterMode.value) {
getFacultyList();
}
});
</script>
<style lang="scss" scoped>
.teacher {
background: #ffffff;
height: calc(100vh - 70px);
.df {
display: flex;
align-items: center;
gap: 20px;
}
:deep(.pagination-container) {
.el-pagination {
// 去掉定位
position: sticky !important;
}
}
.student-action-group {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
flex-wrap: nowrap;
white-space: nowrap;
:deep(.el-button + .el-button) {
margin-left: 0;
}
}
}
</style>