This commit is contained in:
2026-07-24 13:11:09 +08:00 Unverified
parent 6c3021ea8d
commit 30d793773c
23 changed files with 3479 additions and 79 deletions
+447
View File
@@ -0,0 +1,447 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
type Registry = 'teachers' | 'students'
interface PageResult {
items: any[]
total: number
page: number
pageSize: number
}
const auth = useAuthStore()
const active = ref<Registry>('teachers')
const loading = ref(false)
const rows = ref<any[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const editingId = ref('')
const colleges = ref<any[]>([])
const majors = ref<any[]>([])
const classes = ref<any[]>([])
const query = reactive({
page: 1,
pageSize: 20,
keyword: '',
collegeId: undefined as string | undefined,
majorId: undefined as string | undefined,
classId: undefined as string | undefined,
enrollmentYear: undefined as number | undefined,
status: undefined as string | undefined,
})
const form = reactive<Record<string, any>>({})
const canManage = computed(() =>
auth.user?.roles.some((role) =>
['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role),
) ?? false,
)
const scopedCollegeId = computed(() =>
auth.user?.roles.some((role) => ['CollegeAdmin', 'Counselor'].includes(role))
? auth.user.collegeId
: undefined,
)
const availableColleges = computed(() =>
scopedCollegeId.value
? colleges.value.filter((item) => item.id === scopedCollegeId.value)
: colleges.value,
)
const availableMajors = computed(() =>
scopedCollegeId.value
? majors.value.filter((item) => item.collegeId === scopedCollegeId.value)
: majors.value,
)
const availableClasses = computed(() => {
const majorIds = new Set(availableMajors.value.map((item) => item.id))
return classes.value.filter((item) => majorIds.has(item.majorId))
})
const pageTitle = computed(() => active.value === 'teachers' ? '教师档案' : '学生档案')
const numberLabel = computed(() => active.value === 'teachers' ? '工号' : '学号')
const filteredMajors = computed(() =>
query.collegeId
? availableMajors.value.filter((item) => item.collegeId === query.collegeId)
: availableMajors.value,
)
const filteredClasses = computed(() => {
if (query.majorId) {
return availableClasses.value.filter((item) => item.majorId === query.majorId)
}
if (query.collegeId) {
const majorIds = new Set(
availableMajors.value
.filter((item) => item.collegeId === query.collegeId)
.map((item) => item.id),
)
return availableClasses.value.filter((item) => majorIds.has(item.majorId))
}
return availableClasses.value
})
const formClasses = computed(() => availableClasses.value)
const genderLabels: Record<string, string> = {
Unknown: '未设置', Male: '男', Female: '女',
}
const teacherStatusLabels: Record<string, string> = {
Active: '在职', OnLeave: '离岗', Retired: '退休', Departed: '离职',
}
const studentStatusLabels: Record<string, string> = {
Active: '在籍', Suspended: '休学', Graduated: '毕业', Withdrawn: '退学',
}
function resetForm(row?: any) {
Object.keys(form).forEach((key) => delete form[key])
if (active.value === 'teachers') {
Object.assign(form, {
teacherNumber: '',
name: '',
gender: 'Unknown',
collegeId: scopedCollegeId.value,
title: '',
status: 'Active',
hireDate: '',
isExternal: false,
phone: '',
email: '',
notes: '',
}, row ?? {})
} else {
const currentYear = new Date().getFullYear()
Object.assign(form, {
studentNumber: '',
name: '',
gender: 'Unknown',
administrativeClassId: undefined,
enrollmentYear: currentYear,
enrollmentDate: `${currentYear}-09-01`,
status: 'Active',
dateOfBirth: '',
phone: '',
email: '',
notes: '',
}, row ?? {})
}
}
async function load() {
loading.value = true
try {
const params: Record<string, any> = {
page: query.page,
pageSize: query.pageSize,
keyword: query.keyword || undefined,
collegeId: query.collegeId,
}
if (active.value === 'teachers') {
params.teacherStatus = query.status
} else {
Object.assign(params, {
majorId: query.majorId,
classId: query.classId,
enrollmentYear: query.enrollmentYear,
studentStatus: query.status,
})
}
const { data } = await http.get<PageResult>(`/personnel/${active.value}`, { params })
rows.value = data.items
total.value = data.total
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
loading.value = false
}
}
async function loadReferences() {
const [collegeRes, majorRes, classRes] = await Promise.all([
http.get('/base-data/colleges'),
http.get('/base-data/majors'),
http.get('/base-data/classes'),
])
colleges.value = collegeRes.data
majors.value = majorRes.data
classes.value = classRes.data
if (scopedCollegeId.value) query.collegeId = scopedCollegeId.value
}
function switchRegistry(kind: Registry) {
active.value = kind
Object.assign(query, {
page: 1,
keyword: '',
collegeId: scopedCollegeId.value,
majorId: undefined,
classId: undefined,
enrollmentYear: undefined,
status: undefined,
})
load()
}
function resetFilters() {
Object.assign(query, {
page: 1,
keyword: '',
collegeId: scopedCollegeId.value,
majorId: undefined,
classId: undefined,
enrollmentYear: undefined,
status: undefined,
})
load()
}
function openCreate() {
editingId.value = ''
resetForm()
dialogVisible.value = true
}
function openEdit(row: any) {
editingId.value = row.id
resetForm(row)
dialogVisible.value = true
}
async function save() {
const number = active.value === 'teachers' ? form.teacherNumber : form.studentNumber
if (!number?.trim() || !form.name?.trim()) {
ElMessage.warning(`请填写${numberLabel.value}和姓名。`)
return
}
try {
const path = `/personnel/${active.value}`
if (editingId.value) await http.put(`${path}/${editingId.value}`, form)
else await http.post(path, form)
ElMessage.success(editingId.value ? '档案已更新' : '档案已建立')
dialogVisible.value = false
await load()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
async function remove(row: any) {
try {
await ElMessageBox.confirm(
`确定删除“${row.name}”的${pageTitle.value}吗?`,
'删除档案',
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
)
await http.delete(`/personnel/${active.value}/${row.id}`)
ElMessage.success('档案已删除')
await load()
} catch (error: any) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
}
}
onMounted(async () => {
await Promise.all([loadReferences(), load()])
})
</script>
<template>
<div class="page-stack">
<section class="page-intro registry-intro">
<div>
<span class="section-kicker">ACADEMIC REGISTRY</span>
<h2>人员档案</h2>
<p>以学校统一编号管理教师和学生的归属状态与联系信息</p>
</div>
<el-button v-if="canManage" type="primary" :icon="Plus" @click="openCreate">
新增{{ pageTitle }}
</el-button>
</section>
<section class="registry-switch">
<button
type="button"
:class="{ active: active === 'teachers' }"
@click="switchRegistry('teachers')"
>
<span>TEACHING STAFF</span>
<b>教师档案</b>
<small>工号职称与任职状态</small>
</button>
<button
type="button"
:class="{ active: active === 'students' }"
@click="switchRegistry('students')"
>
<span>STUDENT ROLL</span>
<b>学生档案</b>
<small>学号班级与学籍状态</small>
</button>
<div class="registry-total">
<span>当前结果</span>
<strong>{{ total }}</strong>
<small>条档案</small>
</div>
</section>
<section class="data-card">
<div class="filter-bar">
<el-input
v-model="query.keyword"
:prefix-icon="Search"
clearable
:placeholder="`搜索${numberLabel}、姓名${active === 'teachers' ? '或职称' : ''}`"
@keyup.enter="query.page = 1; load()"
/>
<el-select v-model="query.collegeId" clearable placeholder="全部学院">
<el-option v-for="item in availableColleges" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<template v-if="active === 'students'">
<el-select v-model="query.majorId" clearable filterable placeholder="全部专业">
<el-option v-for="item in filteredMajors" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-select v-model="query.classId" clearable filterable placeholder="全部班级">
<el-option v-for="item in filteredClasses" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</template>
<el-select v-model="query.status" clearable placeholder="全部状态">
<el-option
v-for="(label, value) in active === 'teachers' ? teacherStatusLabels : studentStatusLabels"
:key="value"
:label="label"
:value="value"
/>
</el-select>
<el-button type="primary" @click="query.page = 1; load()">查询</el-button>
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
</div>
<el-table v-loading="loading" :data="rows" class="data-table registry-table">
<el-table-column :label="numberLabel" width="135">
<template #default="{ row }"><span class="registry-number">{{ active === 'teachers' ? row.teacherNumber : row.studentNumber }}</span></template>
</el-table-column>
<el-table-column label="姓名" min-width="120">
<template #default="{ row }">
<div class="record-name">
<b>{{ row.name }}</b>
<span>{{ genderLabels[row.gender] }}</span>
</div>
</template>
</el-table-column>
<el-table-column v-if="active === 'teachers'" prop="collegeName" label="所属学院" min-width="150" />
<el-table-column v-if="active === 'teachers'" prop="title" label="职称" width="110">
<template #default="{ row }">{{ row.title || '—' }}</template>
</el-table-column>
<el-table-column v-if="active === 'teachers'" label="教师类别" width="105">
<template #default="{ row }">{{ row.isExternal ? '外聘' : '校内' }}</template>
</el-table-column>
<el-table-column v-if="active === 'students'" prop="className" label="行政班" min-width="155" />
<el-table-column v-if="active === 'students'" prop="majorName" label="专业" min-width="170" />
<el-table-column v-if="active === 'students'" prop="enrollmentYear" label="入学年级" width="105" />
<el-table-column label="状态" width="95">
<template #default="{ row }">
<span class="table-status" :class="{ off: row.status !== 'Active' }">
{{ (active === 'teachers' ? teacherStatusLabels : studentStatusLabels)[row.status] }}
</span>
</template>
</el-table-column>
<el-table-column label="联系方式" min-width="170">
<template #default="{ row }">{{ row.phone || row.email || '—' }}</template>
</el-table-column>
<el-table-column v-if="canManage" label="操作" width="145" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
<template #empty><el-empty description="没有符合条件的档案" /></template>
</el-table>
<div class="pagination-bar">
<span> {{ total }} </span>
<el-pagination
v-model:current-page="query.page"
:page-size="query.pageSize"
:total="total"
layout="prev, pager, next"
@current-change="load"
/>
</div>
</section>
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}${pageTitle}`" width="620px">
<el-form label-position="top" class="entity-form">
<template v-if="active === 'teachers'">
<div class="form-grid">
<el-form-item label="工号" required><el-input v-model="form.teacherNumber" /></el-form-item>
<el-form-item label="姓名" required><el-input v-model="form.name" /></el-form-item>
</div>
<div class="form-grid">
<el-form-item label="所属学院" required>
<el-select v-model="form.collegeId">
<el-option v-for="item in availableColleges" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="职称"><el-input v-model="form.title" placeholder="教授、副教授、讲师等" /></el-form-item>
</div>
<div class="form-grid">
<el-form-item label="性别">
<el-select v-model="form.gender">
<el-option v-for="(label, value) in genderLabels" :key="value" :label="label" :value="value" />
</el-select>
</el-form-item>
<el-form-item label="任职状态">
<el-select v-model="form.status">
<el-option v-for="(label, value) in teacherStatusLabels" :key="value" :label="label" :value="value" />
</el-select>
</el-form-item>
</div>
<div class="form-grid">
<el-form-item label="入职日期"><el-date-picker v-model="form.hireDate" value-format="YYYY-MM-DD" /></el-form-item>
<el-form-item label="教师类别"><el-switch v-model="form.isExternal" active-text="外聘" inactive-text="校内" /></el-form-item>
</div>
</template>
<template v-else>
<div class="form-grid">
<el-form-item label="学号" required><el-input v-model="form.studentNumber" /></el-form-item>
<el-form-item label="姓名" required><el-input v-model="form.name" /></el-form-item>
</div>
<el-form-item label="行政班" required>
<el-select v-model="form.administrativeClassId" filterable>
<el-option
v-for="item in formClasses"
:key="item.id"
:label="`${item.collegeName} · ${item.majorName} · ${item.name}`"
:value="item.id"
/>
</el-select>
</el-form-item>
<div class="form-grid">
<el-form-item label="入学年级"><el-input-number v-model="form.enrollmentYear" :min="2000" :max="2200" /></el-form-item>
<el-form-item label="入学日期"><el-date-picker v-model="form.enrollmentDate" value-format="YYYY-MM-DD" /></el-form-item>
</div>
<div class="form-grid">
<el-form-item label="性别">
<el-select v-model="form.gender">
<el-option v-for="(label, value) in genderLabels" :key="value" :label="label" :value="value" />
</el-select>
</el-form-item>
<el-form-item label="学籍状态">
<el-select v-model="form.status">
<el-option v-for="(label, value) in studentStatusLabels" :key="value" :label="label" :value="value" />
</el-select>
</el-form-item>
</div>
<el-form-item label="出生日期"><el-date-picker v-model="form.dateOfBirth" value-format="YYYY-MM-DD" /></el-form-item>
</template>
<div class="form-grid">
<el-form-item label="联系电话"><el-input v-model="form.phone" /></el-form-item>
<el-form-item label="电子邮箱"><el-input v-model="form.email" /></el-form-item>
</div>
<el-form-item label="备注"><el-input v-model="form.notes" type="textarea" :rows="3" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">保存档案</el-button>
</template>
</el-dialog>
</div>
</template>