已完成“组织与权限”优化:
基础数据拆分为:组织机构、学年学期、教学场所。 人员档案拆分为:教师档案、学生档案。 基础数据、教师、学生、用户权限均支持 Excel 模板下载、批量导入和导出。 导入按编码/工号/学号/账号新增或更新;整批校验,失败不写入,限制 2000 条、10 MB。 用户列表新增密码重置,包含双重确认、最低 8 位校验和服务端权限保护。 导入严格执行角色及学院数据范围校验,并保护当前超级管理员账号。 保留旧地址重定向,原有入口不会失效。
This commit is contained in:
+102
-38
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Document, Download, Plus, Refresh, Search, Upload } from '@element-plus/icons-vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { downloadApiFile, importExcel } from '../api/excel'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
type Registry = 'teachers' | 'students'
|
||||
@@ -13,8 +15,13 @@ interface PageResult {
|
||||
}
|
||||
|
||||
const auth = useAuthStore()
|
||||
const active = ref<Registry>('teachers')
|
||||
const route = useRoute()
|
||||
const active = computed<Registry>(() =>
|
||||
route.meta.registry === 'students' ? 'students' : 'teachers',
|
||||
)
|
||||
const loading = ref(false)
|
||||
const importing = ref(false)
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
@@ -166,8 +173,7 @@ async function loadReferences() {
|
||||
if (scopedCollegeId.value) query.collegeId = scopedCollegeId.value
|
||||
}
|
||||
|
||||
function switchRegistry(kind: Registry) {
|
||||
active.value = kind
|
||||
function resetFilters() {
|
||||
Object.assign(query, {
|
||||
page: 1,
|
||||
keyword: '',
|
||||
@@ -180,17 +186,66 @@ function switchRegistry(kind: Registry) {
|
||||
load()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
Object.assign(query, {
|
||||
page: 1,
|
||||
keyword: '',
|
||||
collegeId: scopedCollegeId.value,
|
||||
majorId: undefined,
|
||||
classId: undefined,
|
||||
enrollmentYear: undefined,
|
||||
status: undefined,
|
||||
})
|
||||
load()
|
||||
function exportParams() {
|
||||
const params: Record<string, any> = {
|
||||
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,
|
||||
})
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
async function downloadTemplate() {
|
||||
try {
|
||||
await downloadApiFile(
|
||||
`/personnel/${active.value}/template`,
|
||||
`${pageTitle.value}导入模板.xlsx`,
|
||||
)
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function exportRows() {
|
||||
try {
|
||||
await downloadApiFile(
|
||||
`/personnel/${active.value}/export`,
|
||||
`${pageTitle.value}.xlsx`,
|
||||
{ params: exportParams() },
|
||||
)
|
||||
ElMessage.success(`${pageTitle.value}已导出`)
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
function chooseImportFile() {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
async function handleImport(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
input.value = ''
|
||||
if (!file) return
|
||||
importing.value = true
|
||||
try {
|
||||
const { data } = await importExcel(`/personnel/${active.value}/import`, file)
|
||||
ElMessage.success(`导入完成:新增 ${data.created} 条,更新 ${data.updated} 条`)
|
||||
await load()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
@@ -241,40 +296,37 @@ async function remove(row: any) {
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadReferences(), load()])
|
||||
})
|
||||
|
||||
watch(active, async () => {
|
||||
resetFilters()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-stack">
|
||||
<section class="page-intro registry-intro">
|
||||
<div>
|
||||
<span class="section-kicker">ACADEMIC REGISTRY</span>
|
||||
<h2>人员档案</h2>
|
||||
<p>以学校统一编号管理教师和学生的归属、状态与联系信息。</p>
|
||||
<span class="section-kicker">
|
||||
{{ active === 'teachers' ? 'TEACHING STAFF' : 'STUDENT ROLL' }}
|
||||
</span>
|
||||
<h2>{{ pageTitle }}</h2>
|
||||
<p>
|
||||
{{ active === 'teachers'
|
||||
? '以工号维护教师归属、职称、任职状态与联系信息。'
|
||||
: '以学号维护学生班级归属、入学信息与学籍状态。' }}
|
||||
</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>
|
||||
<section class="registry-banner" :class="{ student: active === 'students' }">
|
||||
<div>
|
||||
<span>{{ active === 'teachers' ? 'STAFF DIRECTORY' : 'STUDENT DIRECTORY' }}</span>
|
||||
<b>{{ active === 'teachers' ? '工号是教师身份与教学任务的唯一索引' : '学号贯穿学籍、选课、成绩与毕业业务' }}</b>
|
||||
<small>当前筛选结果会原样用于 Excel 导出</small>
|
||||
</div>
|
||||
<div class="registry-total">
|
||||
<span>当前结果</span>
|
||||
<strong>{{ total }}</strong>
|
||||
@@ -312,6 +364,18 @@ onMounted(async () => {
|
||||
</el-select>
|
||||
<el-button type="primary" @click="query.page = 1; load()">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
<template v-if="canManage">
|
||||
<el-button :icon="Document" @click="downloadTemplate">下载模板</el-button>
|
||||
<el-button :icon="Upload" :loading="importing" @click="chooseImportFile">Excel 导入</el-button>
|
||||
<input
|
||||
ref="fileInput"
|
||||
class="visually-hidden"
|
||||
type="file"
|
||||
accept=".xlsx"
|
||||
@change="handleImport"
|
||||
/>
|
||||
</template>
|
||||
<el-button :icon="Download" @click="exportRows">导出结果</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" class="data-table registry-table">
|
||||
|
||||
Reference in New Issue
Block a user