|
|
|
|
@ -1,10 +1,5 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="school-admin-workbench">
|
|
|
|
|
<section class="admin-hero">
|
|
|
|
|
<div class="admin-badge">School Admin · Student</div>
|
|
|
|
|
<h1 class="admin-title">学生管理</h1>
|
|
|
|
|
<p class="admin-subtitle">管理本校学生账号,学生数据按班级归档并限制在当前学校。</p>
|
|
|
|
|
</section>
|
|
|
|
|
<section class="admin-panel">
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
<el-input v-model="query.name" clearable placeholder="学生姓名" class="filter-input" @keyup.enter="load" />
|
|
|
|
|
@ -14,8 +9,8 @@
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-button type="primary" @click="load">查询</el-button>
|
|
|
|
|
<el-button @click="reset">重置</el-button>
|
|
|
|
|
<el-button type="success" @click="openCreate">新增</el-button>
|
|
|
|
|
<el-button type="warning" @click="downloadStudentTemplate">下载模板</el-button>
|
|
|
|
|
<el-button type="primary" @click="openCreate">新增</el-button>
|
|
|
|
|
<el-button @click="downloadStudentTemplate">下载模板</el-button>
|
|
|
|
|
<el-button type="primary" :loading="importing" @click="triggerImport">Excel导入</el-button>
|
|
|
|
|
<input ref="importInputRef" type="file" accept=".xls,.xlsx" class="excel-import-input" @change="handleImportFile" />
|
|
|
|
|
</div>
|
|
|
|
|
@ -40,19 +35,9 @@
|
|
|
|
|
<el-form :model="form" label-width="86px">
|
|
|
|
|
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
|
|
|
|
|
<el-form-item label="学号"><el-input v-model="form.username" :disabled="Boolean(form.userId)" /></el-form-item>
|
|
|
|
|
<el-form-item label="所属院系">
|
|
|
|
|
<el-select v-model="form.schoolFacultyId" placeholder="请选择院系" style="width: 100%" @change="handleFacultyChange">
|
|
|
|
|
<el-option v-for="faculty in faculties" :key="faculty.schoolFacultyId" :label="faculty.schoolFacultyName" :value="faculty.schoolFacultyId" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="所属专业">
|
|
|
|
|
<el-select v-model="form.schoolMajorId" placeholder="请选择专业" style="width: 100%" :disabled="!form.schoolFacultyId" @change="handleMajorChange">
|
|
|
|
|
<el-option v-for="major in formMajors" :key="major.schoolMajorId" :label="major.schoolMajorName" :value="major.schoolMajorId" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="班级">
|
|
|
|
|
<el-select v-model="form.schoolClassId" placeholder="请选择班级" style="width: 100%" :disabled="!form.schoolMajorId">
|
|
|
|
|
<el-option v-for="klass in formClasses" :key="klass.schoolClassId" :label="klass.className" :value="klass.schoolClassId" />
|
|
|
|
|
<el-select v-model="form.schoolClassId" placeholder="请选择班级" style="width: 100%">
|
|
|
|
|
<el-option v-for="klass in classes" :key="klass.schoolClassId" :label="klass.className" :value="klass.schoolClassId" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="性别">
|
|
|
|
|
@ -73,7 +58,7 @@
|
|
|
|
|
<script setup>
|
|
|
|
|
import "@/views/schoolAdmin/admin-theme.scss";
|
|
|
|
|
import { ElMessage, ElMessageBox } from "element-plus";
|
|
|
|
|
import { addAdminStudent, deleteAdminStudent, getAdminClasses, getAdminFaculties, getAdminMajors, getAdminStudents, importAdminStudents, updateAdminStudent } from "@/api/schoolAdmin";
|
|
|
|
|
import { addAdminStudent, deleteAdminStudent, getAdminClasses, getAdminStudents, importAdminStudents, updateAdminStudent } from "@/api/schoolAdmin";
|
|
|
|
|
|
|
|
|
|
const query = reactive({ index: 1, size: 10, name: "", username: "", schoolClassId: "" });
|
|
|
|
|
const rows = ref([]);
|
|
|
|
|
@ -83,19 +68,13 @@ const importing = ref(false);
|
|
|
|
|
const dialogVisible = ref(false);
|
|
|
|
|
const importInputRef = ref(null);
|
|
|
|
|
const form = ref({});
|
|
|
|
|
const faculties = ref([]);
|
|
|
|
|
const majors = ref([]);
|
|
|
|
|
const classes = ref([]);
|
|
|
|
|
const studentTemplateHeaders = ["姓名", "学号", "性别", "所属院系", "所属专业", "所属班级"];
|
|
|
|
|
const studentTemplateHeaders = ["姓名", "学号", "性别", "所属班级"];
|
|
|
|
|
const sexOptions = [
|
|
|
|
|
{ label: "男", value: "男" },
|
|
|
|
|
{ label: "女", value: "女" },
|
|
|
|
|
];
|
|
|
|
|
const className = (id) => classes.value.find((klass) => klass.schoolClassId === id)?.className || "";
|
|
|
|
|
const classMajorId = (id) => classes.value.find((klass) => klass.schoolClassId === id)?.schoolMajorId || "";
|
|
|
|
|
const majorFacultyId = (id) => majors.value.find((major) => major.schoolMajorId === id)?.schoolFacultyId || "";
|
|
|
|
|
const formMajors = computed(() => majors.value.filter((major) => major.schoolFacultyId === form.value.schoolFacultyId));
|
|
|
|
|
const formClasses = computed(() => classes.value.filter((klass) => klass.schoolMajorId === form.value.schoolMajorId));
|
|
|
|
|
const trimValue = (value) => String(value ?? "").trim();
|
|
|
|
|
const normalizeSex = (sex) => {
|
|
|
|
|
const value = trimValue(sex);
|
|
|
|
|
@ -104,64 +83,37 @@ const normalizeSex = (sex) => {
|
|
|
|
|
return "";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadFaculties = async () => {
|
|
|
|
|
const res = await getAdminFaculties({ index: 1, size: 1000 });
|
|
|
|
|
faculties.value = (res.data?.list || []).map((row) => row.schoolFaculty || row);
|
|
|
|
|
};
|
|
|
|
|
const loadMajors = async () => {
|
|
|
|
|
const res = await getAdminMajors({ index: 1, size: 1000 });
|
|
|
|
|
majors.value = (res.data?.list || []).map((row) => ({
|
|
|
|
|
...(row.schoolMajor || row),
|
|
|
|
|
schoolFacultyId: row.schoolFacultyId || row.schoolMajor?.schoolFacultyId,
|
|
|
|
|
schoolMajorId: row.schoolMajorId || row.schoolMajor?.schoolMajorId,
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
const loadClasses = async () => {
|
|
|
|
|
const res = await getAdminClasses({ index: 1, size: 1000 });
|
|
|
|
|
classes.value = (res.data?.list || []).map((row) => row.schoolClass || row);
|
|
|
|
|
};
|
|
|
|
|
const ensureOrgData = async () => {
|
|
|
|
|
await Promise.all([loadFaculties(), loadMajors(), loadClasses()]);
|
|
|
|
|
};
|
|
|
|
|
const orgRows = () =>
|
|
|
|
|
classes.value.map((klass) => {
|
|
|
|
|
const major = majors.value.find((item) => item.schoolMajorId === klass.schoolMajorId) || {};
|
|
|
|
|
const faculty = faculties.value.find((item) => item.schoolFacultyId === major.schoolFacultyId) || {};
|
|
|
|
|
return {
|
|
|
|
|
所属院系: faculty.schoolFacultyName || "",
|
|
|
|
|
所属专业: major.schoolMajorName || "",
|
|
|
|
|
所属班级: klass.className || "",
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
const firstOrgRow = () => orgRows().find((row) => row.所属院系 && row.所属专业 && row.所属班级) || { 所属院系: "示例院系", 所属专业: "示例专业", 所属班级: "示例班级" };
|
|
|
|
|
const downloadStudentTemplate = async () => {
|
|
|
|
|
const XLSX = await import("xlsx");
|
|
|
|
|
await ensureOrgData();
|
|
|
|
|
const sampleOrg = firstOrgRow();
|
|
|
|
|
const templateRows = [
|
|
|
|
|
await loadClasses();
|
|
|
|
|
const classNames = classes.value.map((klass) => klass.className || "").filter(Boolean);
|
|
|
|
|
const templateSheet = XLSX.utils.aoa_to_sheet([
|
|
|
|
|
studentTemplateHeaders,
|
|
|
|
|
["张三", "20260001", "男", sampleOrg.所属院系, sampleOrg.所属专业, sampleOrg.所属班级],
|
|
|
|
|
];
|
|
|
|
|
const templateSheet = XLSX.utils.aoa_to_sheet(templateRows);
|
|
|
|
|
templateSheet["!cols"] = [{ wch: 14 }, { wch: 18 }, { wch: 10 }, { wch: 18 }, { wch: 18 }, { wch: 18 }];
|
|
|
|
|
templateSheet["!autofilter"] = { ref: "A1:F1" };
|
|
|
|
|
["张三", "20260001", "男", classNames[0] || "行政一班"],
|
|
|
|
|
]);
|
|
|
|
|
templateSheet["!cols"] = [{ wch: 14 }, { wch: 18 }, { wch: 10 }, { wch: 20 }];
|
|
|
|
|
templateSheet["!autofilter"] = { ref: "A1:D1" };
|
|
|
|
|
|
|
|
|
|
const orgSheet = XLSX.utils.json_to_sheet(orgRows().length ? orgRows() : [{ 所属院系: "请先维护院系", 所属专业: "请先维护专业", 所属班级: "请先维护班级" }]);
|
|
|
|
|
orgSheet["!cols"] = [{ wch: 22 }, { wch: 22 }, { wch: 22 }];
|
|
|
|
|
const classSheet = XLSX.utils.aoa_to_sheet([["所属班级"], ...classNames.map((name) => [name])]);
|
|
|
|
|
classSheet["!cols"] = [{ wch: 22 }];
|
|
|
|
|
|
|
|
|
|
const guideSheet = XLSX.utils.aoa_to_sheet([
|
|
|
|
|
["填写说明"],
|
|
|
|
|
["1. 请在“学生导入模板”中填写学生信息,第一行表头不要修改。"],
|
|
|
|
|
["2. 必填列:姓名、学号、所属院系、所属专业、所属班级。"],
|
|
|
|
|
["3. 性别请填写“男”或“女”,也兼容 1=男、2=女。"],
|
|
|
|
|
["4. 所属院系、所属专业、所属班级请填写名称,不要填写数据 ID。"],
|
|
|
|
|
["5. 可在“基础数据”表中查看当前学校可用的院系/专业/班级组合。"],
|
|
|
|
|
["2. 必填列:姓名、学号、所属班级。"],
|
|
|
|
|
["3. 性别请填写“男”或“女”,也兼容 1=男、2=女,可留空。"],
|
|
|
|
|
["4. 所属班级请填写名称,不要填写数据 ID;只能导入本校行政班。"],
|
|
|
|
|
["5. 可在“基础数据”表中查看当前学校可用的班级名称。"],
|
|
|
|
|
]);
|
|
|
|
|
guideSheet["!cols"] = [{ wch: 80 }];
|
|
|
|
|
|
|
|
|
|
const workbook = XLSX.utils.book_new();
|
|
|
|
|
XLSX.utils.book_append_sheet(workbook, templateSheet, "学生导入模板");
|
|
|
|
|
XLSX.utils.book_append_sheet(workbook, orgSheet, "基础数据");
|
|
|
|
|
XLSX.utils.book_append_sheet(workbook, classSheet, "基础数据");
|
|
|
|
|
XLSX.utils.book_append_sheet(workbook, guideSheet, "填写说明");
|
|
|
|
|
XLSX.writeFile(workbook, "学生导入模板.xlsx");
|
|
|
|
|
};
|
|
|
|
|
@ -212,26 +164,16 @@ const reset = () => {
|
|
|
|
|
load();
|
|
|
|
|
};
|
|
|
|
|
const openCreate = () => {
|
|
|
|
|
form.value = { schoolFacultyId: "", schoolMajorId: "", schoolClassId: "", sex: "" };
|
|
|
|
|
form.value = { schoolClassId: "", sex: "" };
|
|
|
|
|
dialogVisible.value = true;
|
|
|
|
|
};
|
|
|
|
|
const openEdit = (row) => {
|
|
|
|
|
const schoolMajorId = row.schoolMajorId || classMajorId(row.schoolClassId);
|
|
|
|
|
form.value = {
|
|
|
|
|
...row,
|
|
|
|
|
sex: normalizeSex(row.sex),
|
|
|
|
|
schoolMajorId,
|
|
|
|
|
schoolFacultyId: row.schoolFacultyId || majorFacultyId(schoolMajorId),
|
|
|
|
|
};
|
|
|
|
|
dialogVisible.value = true;
|
|
|
|
|
};
|
|
|
|
|
const handleFacultyChange = () => {
|
|
|
|
|
form.value.schoolMajorId = "";
|
|
|
|
|
form.value.schoolClassId = "";
|
|
|
|
|
};
|
|
|
|
|
const handleMajorChange = () => {
|
|
|
|
|
form.value.schoolClassId = "";
|
|
|
|
|
};
|
|
|
|
|
const submit = async () => {
|
|
|
|
|
if (form.value.userId) {
|
|
|
|
|
await updateAdminStudent(form.value.userId, form.value);
|
|
|
|
|
@ -250,8 +192,6 @@ const remove = async (row) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
loadFaculties();
|
|
|
|
|
loadMajors();
|
|
|
|
|
loadClasses();
|
|
|
|
|
load();
|
|
|
|
|
});
|
|
|
|
|
|