3
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
<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'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(false)
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const colleges = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const editingId = ref('')
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
keyword: '',
|
||||
collegeId: undefined as string | undefined,
|
||||
nature: undefined as string | undefined,
|
||||
isEnabled: undefined as boolean | undefined,
|
||||
})
|
||||
const form = reactive<Record<string, any>>({})
|
||||
|
||||
const canManage = computed(() =>
|
||||
auth.user?.roles.some((role) =>
|
||||
['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role),
|
||||
) ?? false,
|
||||
)
|
||||
const availableColleges = computed(() =>
|
||||
auth.user?.roles.includes('CollegeAdmin') && auth.user.collegeId
|
||||
? colleges.value.filter((item) => item.id === auth.user!.collegeId)
|
||||
: colleges.value,
|
||||
)
|
||||
const natureLabels: Record<string, string> = {
|
||||
GeneralRequired: '通识必修',
|
||||
MajorRequired: '专业必修',
|
||||
MajorElective: '专业选修',
|
||||
GeneralElective: '通识选修',
|
||||
Practice: '实践课程',
|
||||
}
|
||||
const assessmentLabels: Record<string, string> = {
|
||||
Examination: '考试',
|
||||
Assessment: '考查',
|
||||
}
|
||||
|
||||
function resetForm(row?: any) {
|
||||
Object.keys(form).forEach((key) => delete form[key])
|
||||
Object.assign(form, {
|
||||
code: '',
|
||||
name: '',
|
||||
englishName: '',
|
||||
collegeId: auth.user?.roles.includes('CollegeAdmin') ? auth.user.collegeId : undefined,
|
||||
credits: 2,
|
||||
totalHours: 32,
|
||||
lectureHours: 24,
|
||||
practiceHours: 8,
|
||||
nature: 'MajorRequired',
|
||||
assessmentMethod: 'Examination',
|
||||
description: '',
|
||||
isEnabled: true,
|
||||
sortOrder: 0,
|
||||
}, row ?? {})
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await http.get('/courses', {
|
||||
params: {
|
||||
page: query.page,
|
||||
pageSize: query.pageSize,
|
||||
keyword: query.keyword || undefined,
|
||||
collegeId: query.collegeId,
|
||||
nature: query.nature,
|
||||
isEnabled: query.isEnabled,
|
||||
},
|
||||
})
|
||||
rows.value = data.items
|
||||
total.value = data.total
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function resetFilters() {
|
||||
Object.assign(query, {
|
||||
page: 1,
|
||||
keyword: '',
|
||||
collegeId: auth.user?.roles.includes('CollegeAdmin') ? auth.user.collegeId : undefined,
|
||||
nature: undefined,
|
||||
isEnabled: undefined,
|
||||
})
|
||||
await 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() {
|
||||
if (!form.code?.trim() || !form.name?.trim() || !form.collegeId) {
|
||||
ElMessage.warning('请填写课程编码、名称和开课学院。')
|
||||
return
|
||||
}
|
||||
if (form.lectureHours + form.practiceHours > form.totalHours) {
|
||||
ElMessage.warning('讲授学时与实践学时之和不能超过总学时。')
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (editingId.value) await http.put(`/courses/${editingId.value}`, form)
|
||||
else await http.post('/courses', 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}”吗?已进入培养方案的课程将无法删除。`,
|
||||
'删除课程',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
|
||||
)
|
||||
await http.delete(`/courses/${row.id}`)
|
||||
ElMessage.success('课程已删除')
|
||||
await load()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
colleges.value = (await http.get('/base-data/colleges')).data
|
||||
if (auth.user?.roles.includes('CollegeAdmin') && auth.user.collegeId) {
|
||||
query.collegeId = auth.user.collegeId
|
||||
}
|
||||
await load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-stack">
|
||||
<section class="page-intro">
|
||||
<div>
|
||||
<span class="section-kicker">COURSE CATALOGUE</span>
|
||||
<h2>课程库</h2>
|
||||
<p>维护全校统一课程编码、学分学时和考核方式,供培养方案与教学任务引用。</p>
|
||||
</div>
|
||||
<el-button v-if="canManage" type="primary" :icon="Plus" @click="openCreate">新增课程</el-button>
|
||||
</section>
|
||||
|
||||
<section class="course-ledger">
|
||||
<div class="ledger-label">
|
||||
<span>课程目录</span>
|
||||
<b>{{ total }}</b>
|
||||
<small>门课程</small>
|
||||
</div>
|
||||
<div class="ledger-rule">
|
||||
<span>编码唯一</span>
|
||||
<span>学时守恒</span>
|
||||
<span>归属明确</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="data-card">
|
||||
<div class="filter-bar">
|
||||
<el-input
|
||||
v-model="query.keyword"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
placeholder="搜索课程编码、中英文名称"
|
||||
@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>
|
||||
<el-select v-model="query.nature" clearable placeholder="全部课程性质">
|
||||
<el-option v-for="(label, value) in natureLabels" :key="value" :label="label" :value="value" />
|
||||
</el-select>
|
||||
<el-select v-model="query.isEnabled" clearable placeholder="全部状态">
|
||||
<el-option label="启用" :value="true" />
|
||||
<el-option label="停用" :value="false" />
|
||||
</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 course-table">
|
||||
<el-table-column label="课程编码" width="130">
|
||||
<template #default="{ row }"><span class="course-code">{{ row.code }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="课程名称" min-width="210">
|
||||
<template #default="{ row }">
|
||||
<div class="course-name">
|
||||
<b>{{ row.name }}</b>
|
||||
<span>{{ row.englishName || '—' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="collegeName" label="开课学院" min-width="145" />
|
||||
<el-table-column label="性质" width="105">
|
||||
<template #default="{ row }">{{ natureLabels[row.nature] }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="学分/学时" width="120">
|
||||
<template #default="{ row }"><b>{{ row.credits }}</b> 学分 · {{ row.totalHours }} 学时</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="学时构成" width="135">
|
||||
<template #default="{ row }">讲授 {{ row.lectureHours }} / 实践 {{ row.practiceHours }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="考核" width="80">
|
||||
<template #default="{ row }">{{ assessmentLabels[row.assessmentMethod] }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="85">
|
||||
<template #default="{ row }">
|
||||
<span class="table-status" :class="{ off: !row.isEnabled }">{{ row.isEnabled ? '启用' : '停用' }}</span>
|
||||
</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 ? '编辑' : '新增'}课程`" width="660px">
|
||||
<el-form label-position="top" class="entity-form">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="课程编码" required><el-input v-model="form.code" placeholder="全校唯一" /></el-form-item>
|
||||
<el-form-item label="课程名称" required><el-input v-model="form.name" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item label="英文名称"><el-input v-model="form.englishName" /></el-form-item>
|
||||
<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-select v-model="form.nature">
|
||||
<el-option v-for="(label, value) in natureLabels" :key="value" :label="label" :value="value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-grid three">
|
||||
<el-form-item label="学分"><el-input-number v-model="form.credits" :min="0.1" :max="99" :step="0.5" /></el-form-item>
|
||||
<el-form-item label="总学时"><el-input-number v-model="form.totalHours" :min="1" /></el-form-item>
|
||||
<el-form-item label="考核方式">
|
||||
<el-select v-model="form.assessmentMethod">
|
||||
<el-option v-for="(label, value) in assessmentLabels" :key="value" :label="label" :value="value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="讲授学时"><el-input-number v-model="form.lectureHours" :min="0" /></el-form-item>
|
||||
<el-form-item label="实践学时"><el-input-number v-model="form.practiceHours" :min="0" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item label="课程简介"><el-input v-model="form.description" type="textarea" :rows="3" /></el-form-item>
|
||||
<div class="form-grid compact">
|
||||
<el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" /></el-form-item>
|
||||
<el-form-item label="状态"><el-switch v-model="form.isEnabled" active-text="启用" inactive-text="停用" /></el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存课程</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Calendar, Connection, OfficeBuilding, School } from '@element-plus/icons-vue'
|
||||
import { Collection, OfficeBuilding, School, UserFilled } from '@element-plus/icons-vue'
|
||||
import http from '../api/http'
|
||||
|
||||
interface DashboardData {
|
||||
@@ -14,10 +14,10 @@ const loading = ref(true)
|
||||
const data = ref<DashboardData>({ counts: {} })
|
||||
|
||||
const stats = computed(() => [
|
||||
{ label: '校区', value: data.value.counts.campuses ?? 0, unit: '个', icon: OfficeBuilding },
|
||||
{ label: '学院', value: data.value.counts.colleges ?? 0, unit: '个', icon: School },
|
||||
{ label: '专业', value: data.value.counts.majors ?? 0, unit: '个', icon: Connection },
|
||||
{ label: '行政班', value: data.value.counts.classes ?? 0, unit: '个', icon: Calendar },
|
||||
{ label: '教师档案', value: data.value.counts.teachers ?? 0, unit: '人', icon: UserFilled },
|
||||
{ label: '学生档案', value: data.value.counts.students ?? 0, unit: '人', icon: OfficeBuilding },
|
||||
{ label: '课程库', value: data.value.counts.courses ?? 0, unit: '门', icon: Collection },
|
||||
])
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -81,16 +81,21 @@ onMounted(async () => {
|
||||
<b>角色权限 + 学院数据范围</b>
|
||||
<i class="done">已建立</i>
|
||||
</div>
|
||||
<div>
|
||||
<span>人员课程</span>
|
||||
<b>教师档案 + 学生档案 + 课程库</b>
|
||||
<i class="done">已建立</i>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="work-card phase-card">
|
||||
<span class="section-kicker">NEXT MILESTONE</span>
|
||||
<h3>下一段业务链</h3>
|
||||
<p>基础数据确认后,将进入学生、教师、课程库和培养方案。</p>
|
||||
<p>人员与课程主数据已就绪,下一步进入培养方案、课程开设与教学任务。</p>
|
||||
<div class="phase-line">
|
||||
<span class="active">基础底座</span>
|
||||
<span>人员档案</span>
|
||||
<span class="active">人员档案</span>
|
||||
<span>培养方案</span>
|
||||
<span>教学运行</span>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user