教师档案增加“激活账号”:管理员设置初始密码,服务端校验管理权限、学院数据范围、在职状态、工号冲突,并自动关联教师角色与档案。

已发布课表增加“发布后修改”:创建修订草稿,旧课表继续生效;修订版重新发布后再替换旧版本。
修复 390px 窄屏下课表页头按钮溢出。
This commit is contained in:
2026-07-25 10:09:05 +08:00 Unverified
parent ce65fe82d9
commit db91988264
5 changed files with 333 additions and 11 deletions
+2
View File
@@ -1070,6 +1070,8 @@ button { cursor: pointer; }
}
@media (max-width: 600px) {
.page-intro { align-items: flex-start; flex-direction: column; }
.page-actions { width: 100%; flex-wrap: wrap; }
.option-filter-grid.course,
.option-filter-grid.course.compact,
.option-filter-grid.classes { grid-template-columns: 1fr; }
+123 -2
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { Document, Download, Plus, Refresh, Search, Upload } from '@element-plus/icons-vue'
import { Document, Download, Key, 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'
@@ -25,6 +25,9 @@ const fileInput = ref<HTMLInputElement>()
const rows = ref<any[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const activationDialog = ref(false)
const activationLoading = ref(false)
const activatingTeacher = ref<any | null>(null)
const editingId = ref('')
const colleges = ref<any[]>([])
const majors = ref<any[]>([])
@@ -40,6 +43,10 @@ const query = reactive({
status: undefined as string | undefined,
})
const form = reactive<Record<string, any>>({})
const activationForm = reactive({
password: '',
confirmPassword: '',
})
const canManage = computed(() =>
auth.user?.roles.some((role) =>
@@ -293,6 +300,47 @@ async function remove(row: any) {
}
}
function openActivation(row: any) {
activatingTeacher.value = row
activationForm.password = ''
activationForm.confirmPassword = ''
activationDialog.value = true
}
async function activateTeacherAccount() {
if (!activationForm.password) {
ElMessage.warning('请设置教师的初始密码。')
return
}
if (activationForm.password.length < 8 ||
!/[a-z]/.test(activationForm.password) ||
!/[A-Z]/.test(activationForm.password) ||
!/\d/.test(activationForm.password) ||
!/[^A-Za-z0-9]/.test(activationForm.password)) {
ElMessage.warning('密码至少 8 位,并包含大写字母、小写字母、数字和特殊字符。')
return
}
if (activationForm.password !== activationForm.confirmPassword) {
ElMessage.warning('两次输入的密码不一致。')
return
}
activationLoading.value = true
try {
const { data } = await http.post(
`/personnel/teachers/${activatingTeacher.value.id}/activate-account`,
{ password: activationForm.password },
)
activationDialog.value = false
ElMessage.success(`教师账号 ${data.userName} 已激活`)
await load()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
activationLoading.value = false
}
}
onMounted(async () => {
await Promise.all([loadReferences(), load()])
})
@@ -417,8 +465,23 @@ watch(active, async () => {
<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">
<el-table-column
v-if="canManage"
label="操作"
:width="active === 'teachers' ? 225 : 145"
fixed="right"
>
<template #default="{ row }">
<el-button
v-if="active === 'teachers' && !row.userId"
link
type="success"
:icon="Key"
:disabled="row.status !== 'Active'"
@click="openActivation(row)"
>
激活账号
</el-button>
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
@@ -514,5 +577,63 @@ watch(active, async () => {
<el-button type="primary" @click="save">保存档案</el-button>
</template>
</el-dialog>
<el-dialog
v-model="activationDialog"
title="激活教师账号"
width="520px"
@closed="activatingTeacher = null"
>
<el-alert
title="账号激活后,教师可使用工号和初始密码登录。请通过安全方式将密码交给教师。"
type="info"
:closable="false"
show-icon
/>
<el-form v-if="activatingTeacher" label-position="top" class="entity-form activation-account-form">
<div class="form-grid">
<el-form-item label="教师">
<el-input :model-value="activatingTeacher.name" disabled />
</el-form-item>
<el-form-item label="登录账号">
<el-input :model-value="activatingTeacher.teacherNumber" disabled />
</el-form-item>
</div>
<el-form-item label="初始密码" required>
<el-input
v-model="activationForm.password"
type="password"
show-password
autocomplete="new-password"
placeholder="至少 8 位,含大小写字母、数字和特殊字符"
/>
</el-form-item>
<el-form-item label="确认初始密码" required>
<el-input
v-model="activationForm.confirmPassword"
type="password"
show-password
autocomplete="new-password"
@keyup.enter="activateTeacherAccount"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="activationDialog = false">取消</el-button>
<el-button
type="primary"
:loading="activationLoading"
@click="activateTeacherAccount"
>
确认激活
</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.activation-account-form {
margin-top: 20px;
}
</style>
+50 -8
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { CopyDocument, Plus, Promotion, Refresh, Search, Setting } from '@element-plus/icons-vue'
import { CopyDocument, EditPen, Plus, Promotion, Refresh, Search, Setting } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
const plans = ref<any[]>([])
@@ -29,6 +29,7 @@ const keyword = ref('')
const termId = ref<string | undefined>()
const planForm = reactive<Record<string, any>>({})
const cloneForm = reactive<Record<string, any>>({})
const revisingPublishedPlan = ref(false)
const entryForm = reactive<Record<string, any>>({})
const constraintForm = reactive<Record<string, any>>({})
const constraintBatchForm = reactive<Record<string, any>>({})
@@ -478,8 +479,9 @@ async function savePlan() {
}
function openClone() {
revisingPublishedPlan.value = selected.value.status === 'Published'
Object.assign(cloneForm, {
name: `${selected.value.name}调整版)`,
name: `${selected.value.name}修订版)`,
version: `V${plans.value.length + 1}`,
})
cloneDialog.value = true
@@ -487,10 +489,18 @@ function openClone() {
async function clonePlan() {
try {
await http.post(`/schedules/plans/${selected.value.id}/clone`, cloneForm)
const { data } = await http.post(
`/schedules/plans/${selected.value.id}/clone`,
cloneForm,
)
cloneDialog.value = false
ElMessage.success('已复制为可调整的草稿版本')
ElMessage.success(
revisingPublishedPlan.value
? '已建立修订草稿,原已发布课表继续生效'
: '已复制为可调整的草稿版本',
)
await loadPlans(false)
if (data.id && selected.value?.id !== data.id) await loadDetail(data.id)
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
@@ -499,7 +509,7 @@ async function clonePlan() {
async function publishPlan() {
try {
await ElMessageBox.confirm(
'系统将再次检查全部教师、行政班和教室冲突;发布后本版本锁定,并归档旧课表。',
'系统将再次检查全部教师、行政班和教室冲突;发布后本版本锁定并替换旧课表,后续仍可通过“发布后修改”建立修订版。',
'发布课表',
{ type: 'warning', confirmButtonText: '检查并发布', cancelButtonText: '取消' },
)
@@ -631,7 +641,23 @@ onBeforeUnmount(clearAutoSchedulePoll)
</div>
<div class="plan-actions">
<el-button v-if="isDraft" :disabled="autoLoading" @click="openPlan(selected)">编辑版本</el-button>
<el-button :icon="CopyDocument" :disabled="autoLoading" @click="openClone">复制调整</el-button>
<el-button
v-if="selected.status === 'Published'"
type="warning"
plain
:icon="EditPen"
@click="openClone"
>
发布后修改
</el-button>
<el-button
v-else
:icon="CopyDocument"
:disabled="autoLoading"
@click="openClone"
>
复制调整
</el-button>
<el-button
v-if="isDraft"
type="warning"
@@ -723,12 +749,28 @@ onBeforeUnmount(clearAutoSchedulePoll)
<template #footer><el-button @click="planDialog = false">取消</el-button><el-button type="primary" @click="savePlan">保存</el-button></template>
</el-dialog>
<el-dialog v-model="cloneDialog" title="复制排课版本" width="520px">
<el-dialog
v-model="cloneDialog"
:title="revisingPublishedPlan ? '发布后修改课表' : '复制排课版本'"
width="520px"
>
<el-alert
v-if="revisingPublishedPlan"
title="系统会建立一份可编辑的修订草稿;原课表在修订版重新发布前继续生效,不会影响教师和学生查课。"
type="info"
:closable="false"
show-icon
/>
<el-form label-position="top">
<el-form-item label="新版本名称" required><el-input v-model="cloneForm.name" /></el-form-item>
<el-form-item label="新版本号" required><el-input v-model="cloneForm.version" /></el-form-item>
</el-form>
<template #footer><el-button @click="cloneDialog = false">取消</el-button><el-button type="primary" @click="clonePlan">复制</el-button></template>
<template #footer>
<el-button @click="cloneDialog = false">取消</el-button>
<el-button type="primary" @click="clonePlan">
{{ revisingPublishedPlan ? '建立修订草稿' : '复制' }}
</el-button>
</template>
</el-dialog>
<el-dialog v-model="entryDialog" :title="editingEntryId ? '调整排课' : '添加排课'" width="680px">