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