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

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.

<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>
</template>
<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 || []
})
}
onMounted(() => {
if (!adminClassManagementEnabled.value) {
activeClassType.value = "TEACHING"
}
getList()
getAllClassList()
getFacultyList()
})
</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;
}
}
</style>