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

590 lines
23 KiB
Vue

This file contains ambiguous Unicode characters!

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

<!--
* @Author: qinzhenpen qzp1807@126.com
* @Date: 2025-05-13 16:22:46
* @LastEditors: qinzhenpen qzp1807@126.com
* @LastEditTime: 2025-05-21 16:04:21
* @FilePath: \e-commerce-internet\src\views\teacherEnd\school\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="teacher">
<div class="df">
<div class="online">
<span>教师姓名:</span>
<el-input style="width: 14rem" v-model="paramsquery.name" class="ml10"></el-input>
</div>
<div class="online">
<span>教师工号:</span>
<el-input style="width: 14rem" v-model="paramsquery.userName" class="ml10"></el-input>
<el-button @click="getList" class="ml10">
<el-icon>
<Search />
</el-icon>
搜索
</el-button>
</div>
</div>
<el-row class="mt10">
<el-col>
<div style="display: flex; gap: 10px">
<el-button v-if="isManagerTeacher" type="primary" @click="addschool">
<el-icon>
<Plus />
</el-icon>
新增
</el-button>
<el-upload v-if="isManagerTeacher" action="/api/user/uploadTeacherUserInfo"
:data="{ operatorId: currentUserId }" :show-file-list="false"
:on-success="(res, file) => handleSuccess(res, file)">
<el-button>
<el-icon>
<Upload />
</el-icon>
导入
</el-button>
</el-upload>
<el-button @click="exportTeacher">
<el-icon>
<Download />
</el-icon>
导出
</el-button>
<el-button v-if="isManagerTeacher" @click="downloadTemplate">
<el-icon>
<Document />
</el-icon>
导入模版下载
</el-button>
<el-button @click="refresh">
<el-icon>
<Refresh />
</el-icon>
刷新
</el-button>
</div>
</el-col>
</el-row>
<el-table :data="tableData" v-loading="loading" style="width: 100%; margin-top: 20px" :header-cell-style="{
color: '#1f2937',
fontSize: '12px',
fontWeight: '600',
backgroundColor: '#ffffff',
}">
<el-table-column label="教师姓名" prop="name" align="center">
<template #default="{ row }">
<div class="teacher-name-cell">
<span class="teacher-badge">{{ (row.name || "师").slice(0, 1) }}</span>
<span class="teacher-name">{{ row.name || "-" }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="工号" prop="username" align="center"></el-table-column>
<el-table-column label="名下班级" align="center" min-width="200">
<template #default="{ row }">
<div v-if="row.ownedClassNames?.length" class="owned-class-list"
:title="row.ownedClassNames.join('、')">
<span v-for="className in row.ownedClassNames" :key="className" class="owned-class-chip">{{
className }}</span>
</div>
<span v-else class="empty-summary">暂无班级</span>
</template>
</el-table-column>
<el-table-column label="学生数量" align="center" min-width="90">
<template #default="{ row }">
<strong class="student-count">{{ Number(row.ownedStudentCount || 0) }}<small> 人</small></strong>
</template>
</el-table-column>
<el-table-column label="创建日期" prop="createTime" align="center">
<template #default="{ row }">
<span>{{ formatDate(row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" min-width="200">
<template #default="{ row }">
<template v-if="isManagerTeacher">
<div class="action-btn-group">
<el-button type="primary" text :disabled="row.hasCreatedClass" @click="editSchool(row)">
<el-icon>
<Edit />
</el-icon>
编辑
</el-button>
<el-button type="danger" text :disabled="row.hasCreatedClass" @click="delSchool(row)">
<el-icon>
<Delete />
</el-icon>
删除
</el-button>
<el-button type="warning" text @click="initializeTeacherPassword(row)">
<el-icon>
<Key />
</el-icon>
密码初始化
</el-button>
</div>
<el-tag v-if="row.hasCreatedClass" type="warning" size="small">该教师已创建班级,不允许编辑或删除</el-tag>
</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>暂无教师</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="560px" :title="status ? '新增教师' : '编辑教师'" class="teacher-dialog"
@close="onClose">
<el-form :model="formInline" class="demo-form-inline" ref="formInlineref" label-width="100px"
style="width: 80%" :rules="rules">
<!-- 2026-10-12 停用:所属院系 / 所属专业的下拉选择(新增教师不再选择院系与专业)。
原始代码保留备用;恢复时打开下面这段注释,并删掉其后两个只读 el-form-item。
<el-form-item v-if="status || isOrganizationManagementEnabled" label="所属院系:" prop="schoolFacultyId">
<el-select v-model="formInline.schoolFacultyId" placeholder="" clearable @change="getMajorList"
style="width: 20rem" v-if="status">
<el-option v-for="school in facultyList" :key="school.schoolId"
:label="school.schoolFacultyName" :value="school.schoolFacultyId" />
</el-select>
<el-input v-else v-model="classInfo.name" style="width: 20rem" disabled></el-input>
</el-form-item>
<el-form-item v-if="status || isOrganizationManagementEnabled" label="所属专业:" prop="schoolMajorId">
<el-select v-model="formInline.schoolMajorId" placeholder="" clearable style="width: 20rem"
v-if="status">
<el-option v-for="major in majorList" :key="major.schoolMajorId" :label="major.schoolMajorName"
:value="major.schoolMajorId" />
</el-select>
<el-input v-else v-model="classInfo.value" placeholder="请输入所属专业:" style="width: 20rem"
disabled></el-input>
</el-form-item>
-->
<!-- 新增教师(status=true)不再展示院系/专业;编辑教师(status=false)仍保留只读展示 -->
<el-form-item v-if="!status && isOrganizationManagementEnabled" label="所属院系:" prop="schoolFacultyId">
<el-input v-model="classInfo.name" style="width: 20rem" disabled></el-input>
</el-form-item>
<el-form-item v-if="!status && isOrganizationManagementEnabled" label="所属专业:" prop="schoolMajorId">
<el-input v-model="classInfo.value" style="width: 20rem" disabled></el-input>
</el-form-item>
<el-form-item label="教师姓名:" prop="name">
<el-input v-model="formInline.name" style="width: 20rem" placeholder=""></el-input>
</el-form-item>
<el-form-item label="教师工号:" prop="userName">
<el-input v-model="formInline.userName" style="width: 20rem" placeholder=""></el-input>
</el-form-item>
<el-form-item label="电话号码:">
<el-input v-model="formInline.phone" style="width: 20rem" placeholder=""></el-input>
</el-form-item>
<el-form-item label="电子邮箱:">
<el-input v-model="formInline.email" style="width: 20rem" placeholder=""></el-input>
</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>
</div>
</template>
<script setup>
import { ElMessageBox } from "element-plus"
import { Search, Plus, Upload, Download, Document, Refresh, Edit, Delete, Key, Close, Check, FolderOpened } from "@element-plus/icons-vue"
const { proxy } = getCurrentInstance()
import useUserStore from "@/store/modules/user"
const userStore = useUserStore()
import * as indexApi from "@/api/teacher"
const dialogVisible = ref(false)
const loading = ref(false)
const currentUserId = computed(() => userStore.userInfo.userId)
const isManagerTeacher = computed(() => userStore.userInfo.teacherAdmin === true || String(userStore.userInfo.roleId || userStore.userInfo.role) === "1")
const legacyOrganizationManagementEnabled = computed(() => userStore.schoolProductConfig?.organizationMode !== "TEACHING_CLASS_ONLY")
const facultyManagementEnabled = computed(() => userStore.schoolProductConfig?.facultyManagementEnabled ?? legacyOrganizationManagementEnabled.value)
const majorManagementEnabled = computed(() => userStore.schoolProductConfig?.majorManagementEnabled ?? legacyOrganizationManagementEnabled.value)
const adminClassManagementEnabled = computed(() => userStore.schoolProductConfig?.adminClassManagementEnabled ?? legacyOrganizationManagementEnabled.value)
const isOrganizationManagementEnabled = computed(() => facultyManagementEnabled.value || majorManagementEnabled.value || adminClassManagementEnabled.value)
const formatDate = (cellValue) => {
const numeric = Number(cellValue)
if (!cellValue || !Number.isFinite(numeric)) {
return cellValue || ""
}
const date = new Date(numeric)
if (Number.isNaN(date.getTime())) {
return cellValue
}
const pad = (value) => String(value).padStart(2, "0")
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
const formInline = ref({
schoolFacultyId: "",
schoolId: userStore.userInfo.schoolId,
schoolMajorId: "",
schoolMajorName: "",
schoolName: "",
userName: "",
email: "",
phone: "",
})
const organizationRules = {
schoolFacultyId: [{ required: true, message: "请选择院系", trigger: "change" }],
schoolMajorId: [{ required: true, message: "请选择专业", trigger: "change" }],
}
const rules = computed(() => ({
...((status.value || isOrganizationManagementEnabled.value) ? organizationRules : {}),
name: [{ required: true, message: "请输入教师姓名", trigger: "blur" }],
userName: [{ required: true, message: "请输入教师工号", trigger: "blur" }],
}))
const paramsquery = ref({
index: 1,
size: 10,
schoolId: userStore.userInfo.schoolId,
})
const classInfo = ref({})
const schoolTotal = ref(0)
// 新增编辑状态
const status = ref(false)
// 院校list
const tableData = ref([])
// 获取院校列表
const getList = () => {
indexApi.getTeacherList({ ...paramsquery.value, operatorId: currentUserId.value }).then((res) => {
tableData.value = res.data.list
schoolTotal.value = res.data.total
})
}
// 删除院校
const delSchool = (id) => {
indexApi.deleteTeacher({ userId: id.userId, operatorId: currentUserId.value }).then((res) => {
proxy.$message.success("删除成功")
getList()
})
}
// 新增院校
const addschool = () => {
dialogVisible.value = true
status.value = true
}
const editSchool = (row) => {
let data = { ...row }
formInline.value = data
formInline.value.userName = row.username
dialogVisible.value = true
status.value = false
if (!isOrganizationManagementEnabled.value) {
return
}
indexApi.getFacultyAndMajorName({ schoolMajorId: row.schoolMajorId }).then((res) => {
classInfo.value.name = Object.keys(res.data)[0]
classInfo.value.value = Object.values(res.data)[0]
})
}
const add = () => {
// 院系/专业下拉停用后 schoolMajorId 可能为空,取不到专业名时不能抛错
const matchedMajor = (majorList.value || []).find((item) => item.schoolMajorId == formInline.value.schoolMajorId)
if (matchedMajor) formInline.value.schoolMajorName = matchedMajor.schoolMajorName
formInline.value.operatorId = currentUserId.value
indexApi.addTeacher(formInline.value).then((res) => {
proxy.$message.success("添加成功")
dialogVisible.value = false
getList()
})
}
const edit = () => {
formInline.value.status = true
formInline.value.operatorId = currentUserId.value
formInline.value.username = formInline.value.userName
indexApi.updateTeacher(formInline.value).then((res) => {
proxy.$message.success("编辑成功")
dialogVisible.value = false
getList()
})
}
// 提交
const submitForm = () => {
proxy.$refs.formInlineref.validate((valid) => {
if (valid) {
status.value ? add() : edit()
} else {
return false
}
})
}
const onClose = () => {
for (const key in formInline.value) {
formInline.value[key] = ""
}
dialogVisible.value = false
}
// 刷新
const refresh = () => {
getList()
}
const initializeTeacherPassword = (row) => {
ElMessageBox.confirm(
`确定将教师“${row.name || row.username}”的登录密码初始化为 123qwe 吗?`,
"密码初始化",
{
confirmButtonText: "确定初始化",
cancelButtonText: "取消",
type: "warning",
}
).then(() => {
indexApi
.initializeTeacherPassword({
userId: row.userId,
operatorId: currentUserId.value,
})
.then(() => proxy.$message.success("密码初始化成功"))
})
}
// 查询院系下拉列表
const facultyList = ref([])
const getFacultyList = () => {
indexApi.seleteSchoolFacultyList({ schoolId: userStore.userInfo.schoolId }).then((res) => {
facultyList.value = res.data
})
}
// 专业下拉框
const majorList = ref([])
const getMajorList = () => {
if (!formInline.value.schoolFacultyId) return
paramsquery.value.schoolMajorId = ""
indexApi.getMajorListBySchoolId({ schoolFacultyId: formInline.value.schoolFacultyId }).then((res) => {
majorList.value = res.data
})
}
const handleSuccess = (res, file) => {
getList()
proxy.$modal.msgSuccess("导入成功")
}
// 导出教师账号
const exportTeacher = () => {
indexApi.exportTeacherList({ schoolId: userStore.userInfo.schoolId }).then((res) => {
const blob = new Blob([res], { type: "application/vnd.ms-excel" })
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = url
link.setAttribute("download", "教师账号.xlsx")
link.click()
URL.revokeObjectURL(url)
})
}
// 导入模板下载
const downloadTemplate = async () => {
const XLSX = await import("xlsx")
const templateSheet = XLSX.utils.aoa_to_sheet([
["院校名称", "院系名称", "专业名称", "教师姓名", "工号", "电话", "邮箱"],
["请填写系统中的院校名称", "请填写系统中的院系名称", "请填写系统中的专业名称", "张老师", "20260001", "13800000000", "teacher@example.com"],
])
templateSheet["!cols"] = [
{ wch: 26 },
{ wch: 24 },
{ wch: 24 },
{ wch: 16 },
{ wch: 18 },
{ wch: 18 },
{ wch: 28 },
]
const guideSheet = XLSX.utils.aoa_to_sheet([
["填写说明"],
["1. 院校、院系和专业名称必须与系统中已维护的名称完全一致。"],
["2. 教师姓名和工号必填,工号只能填写数字且不能重复。"],
["3. 电话和邮箱可以留空;填写时必须符合手机号和邮箱格式。"],
["4. 默认密码为 123qwe,导入成功后请提醒教师及时修改密码。"],
])
guideSheet["!cols"] = [{ wch: 80 }]
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, templateSheet, "教师导入模板")
XLSX.utils.book_append_sheet(workbook, guideSheet, "填写说明")
XLSX.writeFile(workbook, "教师账号导入模板.xlsx")
}
onMounted(() => {
getList()
if (isOrganizationManagementEnabled.value) {
getFacultyList()
}
})
</script>
<style lang='scss' scoped>
.teacher {
/* ===== 筛选条(扁平化:无卡片底/边框,外观由 teacher-light.scss 覆盖)===== */
.df {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-3);
margin-bottom: var(--sp-4);
}
.online {
display: flex;
align-items: center;
gap: var(--sp-2);
>span {
font-size: var(--fs-body);
font-weight: 600;
color: var(--text-strong);
white-space: nowrap;
}
.el-input {
width: var(--field-w) !important;
}
.ml10 {
margin-left: 0;
}
}
/* ===== 操作按钮行 ===== */
.mt10 .el-col>div {
display: flex;
flex-wrap: wrap;
gap: var(--sp-2) !important; // 覆盖模板内联 10px
}
/* ===== 数据表格(纯白 + 灰线)===== */
.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);
}
}
/* ===== 名下班级芯片 / 学生数量(白底体系)===== */
.owned-class-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--sp-1);
max-height: 68px;
overflow: auto;
padding: var(--sp-1);
}
.owned-class-chip {
display: inline-flex;
align-items: center;
min-height: 26px;
padding: 0 var(--sp-2);
color: var(--edu-primary);
font-size: var(--fs-body);
line-height: 1;
border: 1px solid var(--border-color);
border-radius: 999px;
background: var(--edu-primary-soft);
}
.empty-summary {
color: var(--text-muted);
font-size: var(--fs-body);
}
/* ===== 教师姓名首字徽标(复刻班级管理页学术纸感)===== */
.teacher-name-cell {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
max-width: 100%;
.teacher-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;
}
.teacher-name {
color: var(--text-strong);
font-weight: 600;
}
}
/* ===== 自定义空状态 ===== */
.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);
}
}
.student-count {
color: var(--edu-primary);
font-size: var(--fs-card);
font-weight: 600;
font-variant-numeric: tabular-nums;
small {
color: var(--text-muted);
font-size: var(--fs-tag);
font-weight: 400;
}
}
/* ===== 弹窗内辅助布局(外观由 teacher-light.scss 全局接管)===== */
.dialog-footers {
display: flex;
gap: var(--sp-3);
justify-content: flex-end;
}
}
</style>