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/class/index.vue

1132 lines
42 KiB
Vue

<template>
<div class="class-page">
<el-tabs v-if="adminClassManagementEnabled" v-model="activeClassType" class="class-tabs"
@tab-change="handleTabChange">
<el-tab-pane :label="`行政班(${adminClassCount})`" name="ADMIN" />
<el-tab-pane :label="`教学班(${teachingClassCount})`" name="TEACHING" />
</el-tabs>
<div class="query-bar">
<el-select v-if="facultyManagementEnabled" v-model="paramsquery.schoolFacultyId" placeholder="所属院系"
clearable @change="getMajorList">
<el-option v-for="faculty in facultyList" :key="faculty.schoolFacultyId"
:label="faculty.schoolFacultyName" :value="faculty.schoolFacultyId" />
</el-select>
<el-select v-if="majorManagementEnabled" v-model="paramsquery.schoolMajorId" placeholder="所属专业" clearable>
<el-option v-for="major in majorList" :key="major.schoolMajorId" :label="major.schoolMajorName"
:value="major.schoolMajorId" />
</el-select>
<el-input v-model="paramsquery.className" placeholder="班级名称" clearable>
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<div class="query-divider"></div>
<el-button @click="getList">
<el-icon>
<Search />
</el-icon>
查询
</el-button>
<el-button @click="refresh">
<el-icon>
<Refresh />
</el-icon>
刷新
</el-button>
<div class="btn-group">
<el-button v-if="canCreateCurrentClass" type="primary" @click="openCreateDialog">
<el-icon>
<Plus />
</el-icon>
新增{{ activeClassType === "ADMIN" ? "行政班" : "教学班" }}
</el-button>
</div>
</div>
<el-table :data="filteredTableData" v-loading="loading" class="class-table"
:header-cell-style="tableHeaderStyle">
<el-table-column label="班级名称" align="center" min-width="180">
<template #default="{ row }">
<div class="class-name-cell">
<span class="class-badge">{{ (row.schoolClass?.className || "班").slice(0, 1) }}</span>
<span class="class-name">{{ row.schoolClass?.className || "-" }}</span>
</div>
</template>
</el-table-column>
<el-table-column v-if="facultyManagementEnabled" label="所属院系" align="center">
<template #default="{ row }">
<span>{{ row.schoolFacultyName || "-" }}</span>
</template>
</el-table-column>
<el-table-column v-if="majorManagementEnabled" label="所属专业" align="center">
<template #default="{ row }">
<span>{{ row.schoolMajorName || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="创建人" align="center">
<template #default="{ row }">
<span>{{ row.createdByName || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="数据来源" align="center" width="120">
<template #default="{ row }">
<el-tag :type="row.schoolClass.dataSource === 'ZHIYUN' ? 'warning' : 'success'">
{{ row.schoolClass.dataSource === "ZHIYUN" ? "智云" : "本系统" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="schoolClass.createTime" align="center" width="180">
<template #default="{ row }">
<span class="time-text">{{ formatDate(row.schoolClass.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="500">
<template #default="{ row }">
<el-button v-if="activeClassType === 'TEACHING'" type="primary" text
@click="openMemberDialog(row.schoolClass)">
<el-icon>
<User />
</el-icon>
成员管理
</el-button>
<template v-if="canOperateClass(row.schoolClass)">
<el-button type="primary" text @click="editClass(row)">
<el-icon>
<Edit />
</el-icon>
编辑
</el-button>
<el-button type="danger" text @click="deleteClass(row.schoolClass.schoolClassId)">
<el-icon>
<Delete />
</el-icon>
删除
</el-button>
<el-button v-if="activeClassType === 'TEACHING'" type="warning" text
@click="initializeClass(row.schoolClass)">
<el-icon>
<Refresh />
</el-icon>
数据初始化
</el-button>
</template>
<el-tag v-else type="info">仅查看</el-tag>
</template>
</el-table-column>
<template #empty>
<div class="table-empty">
<el-icon>
<FolderOpened />
</el-icon>
<p>暂无{{ activeClassType === "ADMIN" ? "行政班" : "教学班" }}</p>
<span>可通过右上角"新增"按钮创建</span>
</div>
</template>
</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="520px" :title="dialogTitle" class="teacher-dialog" @close="onClose">
<el-form ref="formInlineref" :model="formInline" label-width="96px" :rules="rules">
<el-form-item v-if="facultyManagementEnabled" label="所属院系" prop="schoolFacultyId">
<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 v-if="majorManagementEnabled" label="所属专业" prop="schoolMajorId">
<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 && adminClassManagementEnabled"
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-icon>
<Upload />
</el-icon>
导入名单创建
</el-button>
</el-upload>
<el-button @click="downloadTeachingClassImportTemplate">
<el-icon>
<Download />
</el-icon>
模板下载
</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footers">
<el-button @click="dialogVisible = false">
<el-icon>
<Close />
</el-icon>
<span>取消</span>
</el-button>
<el-button type="primary" @click="submitForm">
<el-icon>
<Check />
</el-icon>
<span>确定</span>
</el-button>
</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>
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-input v-model="memberParams.userName" placeholder="学号" clearable>
<template #prefix>
<el-icon>
<User />
</el-icon>
</template>
</el-input>
<el-button type="primary" @click="getMemberList">
<el-icon>
<Search />
</el-icon>
查询
</el-button>
<el-button @click="refreshMemberList">
<el-icon>
<Refresh />
</el-icon>
刷新
</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-icon>
<Upload />
</el-icon>
导入成员
</el-button>
</el-upload>
<el-button @click="downloadMemberImportTemplate">
<el-icon>
<Download />
</el-icon>
模板下载
</el-button>
</template>
</div>
<el-table :data="memberList" v-loading="memberLoading" class="member-table"
:header-cell-style="tableHeaderStyle">
<el-table-column label="学生姓名" align="center">
<template #default="{ row }">
<div class="member-name-cell">
<span class="member-badge">{{ (row.name || "学").slice(0, 1) }}</span>
<span class="member-name">{{ row.name || "-" }}</span>
</div>
</template>
</el-table-column>
<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="200">
<template #default="{ row }">
<template v-if="selectedTeachingClassCanOperate">
<el-button type="danger" text @click="removeMember(row)">
<el-icon>
<Delete />
</el-icon>
移除
</el-button>
<el-button type="primary" text @click="initializeMember(row)">
<el-icon>
<Refresh />
</el-icon>
数据初始化
</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>
4 months ago
</template>
4 months ago
<script setup>
import { Plus, Refresh, Search, Edit, Delete, User, Upload, Download, Close, Check, FolderOpened } from "@element-plus/icons-vue"
import { ElMessageBox } from "element-plus"
import { formatDate } from "@/utils/index"
import useUserStore from "@/store/modules/user"
import * as indexApi from "@/api/teacher"
const { proxy } = getCurrentInstance()
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 legacyAdminEnabled = computed(() => userStore.schoolProductConfig?.organizationMode !== "TEACHING_CLASS_ONLY")
const facultyManagementEnabled = computed(() => userStore.schoolProductConfig?.facultyManagementEnabled ?? legacyAdminEnabled.value)
const majorManagementEnabled = computed(() => userStore.schoolProductConfig?.majorManagementEnabled ?? legacyAdminEnabled.value)
const adminClassManagementEnabled = computed(() => userStore.schoolProductConfig?.adminClassManagementEnabled ?? legacyAdminEnabled.value)
const isTeacherRosterMode = computed(() => !facultyManagementEnabled.value && !majorManagementEnabled.value && !adminClassManagementEnabled.value)
const canCreateCurrentClass = computed(() => activeClassType.value === "ADMIN" || activeClassType.value === "TEACHING")
const paramsquery = ref({
index: 1,
size: 10,
schoolId: currentSchoolId.value,
schoolFacultyId: "",
schoolMajorId: "",
className: "",
})
const formInline = ref(defaultForm())
const memberParams = ref(defaultMemberParams())
const rules = computed(() => ({
...(facultyManagementEnabled.value ? { schoolFacultyId: [{ required: true, message: "请选择所属院系", trigger: "change" }] } : {}),
...(majorManagementEnabled.value ? { schoolMajorId: [{ required: true, message: "请选择所属专业", trigger: "change" }] } : {}),
className: [{ required: true, message: "请输入班级名称", trigger: "blur" }],
}))
const tableHeaderStyle = {
color: "#1f2937",
fontSize: "12px",
fontWeight: "600",
backgroundColor: "#ffffff",
}
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 adminClassCount = computed(() => allClassList.value.filter((schoolClass) => normalizeClassType(schoolClass) === "ADMIN").length)
const teachingClassCount = computed(() => allClassList.value.filter((schoolClass) => normalizeClassType(schoolClass) === "TEACHING").length)
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: "",
schoolId: currentSchoolId.value,
schoolMajorId: "",
className: "",
classType: activeClassType.value,
sourceClassIds: "",
}
}
function defaultMemberParams() {
return {
index: 1,
size: 10,
schoolId: currentSchoolId.value,
schoolClassId: "",
name: "",
userName: "",
}
}
function normalizeClassType(schoolClass) {
if (schoolClass?.classType) {
return schoolClass.classType
}
return schoolClass?.createdBy ? "TEACHING" : "ADMIN"
}
function canOperateClass(schoolClass) {
if (schoolClass?.dataSource === "ZHIYUN") {
return false
}
if (normalizeClassType(schoolClass) === "ADMIN") {
return !schoolClass?.schoolId || schoolClass.schoolId === currentSchoolId.value
}
return schoolClass?.createdBy === currentUserId.value
}
function getList(options = {}) {
const { showLoading = true } = options
if (showLoading) {
loading.value = true
}
indexApi
.getClassesList({
...paramsquery.value,
operatorId: currentUserId.value,
})
.then((res) => {
tableData.value = res.data?.list || []
schoolTotal.value = res.data?.total || 0
})
.finally(() => {
if (showLoading) {
loading.value = false
}
})
}
function handleTabChange() {
paramsquery.value.index = 1
getList()
}
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 = {
...schoolClass,
schoolFacultyId,
schoolMajorId,
classType: normalizeClassType(schoolClass),
userId: currentUserId.value,
}
sourceAdminClassIds.value = []
majorListForForm.value = []
dialogVisible.value = true
hydrateClassFormOptions(formInline.value)
}
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({ showLoading: false })
refreshAllClassList()
})
.catch((error) => {
if (!isMultiTeachingClassConflict(error)) return
ElMessageBox.confirm(multiTeachingClassMessage(error), "学生已在其他教学班", {
confirmButtonText: "继续创建",
cancelButtonText: "取消",
type: "warning",
}).then(() => addClass(true))
})
}
function updateClass() {
const payload = {
...formInline.value,
schoolId: currentSchoolId.value,
classType: activeClassType.value,
userId: currentUserId.value,
status: true,
}
indexApi.updateClass(payload).then(() => {
proxy.$message.success("编辑成功")
dialogVisible.value = false
getList({ showLoading: false })
refreshAllClassList()
})
}
function submitForm() {
proxy.$refs.formInlineref.validate((valid) => {
if (!valid) return
if (adminClassManagementEnabled.value && 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) {
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)
.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")
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"],
])
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: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
indexApi.deleteClass({ schoolClassId: id, userId: currentUserId.value }).then(() => {
proxy.$message.success("删除成功")
getList()
refreshAllClassList()
})
})
}
function initializeClass(schoolClass) {
ElMessageBox.confirm("确定初始化该教学班全班实训数据吗?该操作不可恢复。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
indexApi
.initializeTeachingClassTrainingData({
teachingClassId: schoolClass.schoolClassId,
operatorId: currentUserId.value,
})
.then(() => {
proxy.$message.success("初始化成功")
})
})
}
function refresh() {
getList()
}
function onClose() {
formInline.value = defaultForm()
sourceAdminClassIds.value = []
}
function getFacultyList() {
return indexApi.seleteSchoolFacultyList({ schoolId: currentSchoolId.value }).then((res) => {
facultyList.value = res.data || []
return facultyList.value
})
}
function getMajorList() {
paramsquery.value.schoolMajorId = ""
if (!paramsquery.value.schoolFacultyId) {
majorList.value = []
return
}
indexApi.getMajorListBySchoolId({ schoolFacultyId: paramsquery.value.schoolFacultyId }).then((res) => {
majorList.value = res.data || []
})
}
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 = ""
loadMajorListForForm(formInline.value.schoolFacultyId)
}
async function hydrateClassFormOptions(row) {
const schoolMajorId = row?.schoolMajorId
if (!schoolMajorId) {
return
}
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 || []
})
}
4 months ago
onMounted(() => {
if (!adminClassManagementEnabled.value) {
activeClassType.value = "TEACHING"
}
getList()
getAllClassList()
getFacultyList()
})
4 months ago
</script>
<style lang="scss" scoped>
.class-page {
/* ===== 行政班 / 教学班 标签页(极简文字式)===== */
.class-tabs {
margin-bottom: var(--sp-3);
:deep(.el-tabs__header) {
margin: 0 0 var(--sp-3);
}
:deep(.el-tabs__nav-wrap::after) {
background-color: var(--border-color);
height: 1px;
}
:deep(.el-tabs__item) {
font-size: var(--fs-body);
font-weight: 400;
color: var(--text-body);
height: 36px;
&:hover {
color: var(--text-strong);
}
&.is-active {
color: var(--edu-primary);
font-weight: 600;
}
}
:deep(.el-tabs__active-bar) {
background-color: var(--edu-primary);
height: 2px;
}
}
/* ===== 筛选 / 操作条(扁平化,与教师管理页一致)===== */
.query-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-3);
margin-bottom: var(--sp-4);
.el-select {
width: var(--field-w);
}
.el-input {
width: var(--field-w);
}
.query-divider {
width: 1px;
height: 24px;
background: var(--border-color);
margin: 0 var(--sp-1);
}
.btn-group {
margin-left: auto;
display: flex;
gap: var(--sp-2);
}
}
/* ===== 数据表格(纯白 + 灰线)===== */
.el-table {
background: #ffffff;
border: 1px solid var(--border-color);
border-radius: var(--r-card);
overflow: hidden;
:deep(.el-table__header-wrapper th) {
background: #ffffff;
color: var(--text-strong);
font-weight: 600;
font-size: var(--fs-body);
}
:deep(.el-table__body tr:hover > td) {
background: #f9fafb !important;
}
:deep(.el-table__cell) {
font-size: var(--fs-body);
color: var(--text-strong);
border-color: var(--border-color);
}
}
/* ===== 班级 / 成员名称徽标单元 ===== */
.class-name-cell {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
max-width: 100%;
.class-badge {
flex: 0 0 auto;
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--edu-primary-soft);
color: var(--edu-primary);
border-radius: var(--r-control);
font-size: var(--fs-body);
font-weight: 600;
}
.class-name {
color: var(--text-strong);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.member-name-cell {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
.member-badge {
flex: 0 0 auto;
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--edu-primary-soft);
color: var(--edu-primary);
border-radius: 50%;
font-size: var(--fs-tag);
font-weight: 600;
}
.member-name {
color: var(--text-strong);
}
}
.time-text {
color: var(--text-body);
}
/* ===== 空数据状态 ===== */
.table-empty {
padding: var(--sp-5) 0;
text-align: center;
.el-icon {
font-size: 28px;
color: var(--text-muted);
margin-bottom: var(--sp-2);
}
p {
margin: 0 0 var(--sp-1);
font-size: var(--fs-body);
font-weight: 600;
color: var(--text-body);
}
span {
font-size: var(--fs-tag);
color: var(--text-muted);
}
}
/* ===== 成员工具条(白卡)===== */
.member-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-3);
margin-bottom: var(--sp-4);
padding: var(--sp-4);
background: #ffffff;
border: 1px solid var(--border-color);
border-radius: var(--r-card);
.el-input {
width: var(--field-w);
}
}
/* ===== 弹窗内辅助布局(外观由 teacher-light.scss 全局接管)===== */
.dialog-footers {
display: flex;
gap: var(--sp-3);
justify-content: flex-end;
}
.form-control {
width: 100%;
}
.import-create-actions {
display: flex;
gap: var(--sp-2);
align-items: center;
}
4 months ago
}
</style>