取消新增/Excel 导入档案时自动创建账号。

取消修改人员档案时同步登录账号。
Excel 人员模板恢复为不含“初始密码”。
登录页新增“学生首次登录?自助激活账号”入口。
公开激活地址:http://127.0.0.1:5255/activate
This commit is contained in:
2026-07-25 07:55:49 +08:00 Unverified
parent 27c3944c28
commit 30c15f89e5
11 changed files with 434 additions and 287 deletions
+6
View File
@@ -17,6 +17,12 @@ const router = createRouter({
component: () => import('../views/TimetableView.vue'),
meta: { public: true },
},
{
path: '/activate',
name: 'activate-account',
component: () => import('../views/AccountActivationView.vue'),
meta: { public: true },
},
{
path: '/',
component: AdminLayout,
+1
View File
@@ -939,6 +939,7 @@ button { cursor: pointer; }
.login-form label { display: block; margin-bottom: 20px; }
.login-form label > span { display: block; margin-bottom: 8px; color: #525b6d; font-size: 12px; font-weight: 650; }
.public-timetable-link { display: block; margin: 14px 0 18px; color: #176b87; font-size: 13px; font-weight: 650; text-align: center; text-decoration: none; }
.account-activation-link { display: block; margin: -8px 0 18px; color: #315b73; font-size: 13px; font-weight: 650; text-align: center; text-decoration: none; }
.login-submit { width: 100%; margin-top: 6px; height: 46px; }
.dev-hint { margin-top: 24px; padding: 13px 15px; display: flex; justify-content: space-between; color: #767e8d; background: #f5f7fa; font-size: 11px; }
+268
View File
@@ -0,0 +1,268 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import http, { apiErrorMessage } from '../api/http'
const router = useRouter()
const loading = ref(false)
const submitting = ref(false)
const colleges = ref<any[]>([])
const majors = ref<any[]>([])
const classes = ref<any[]>([])
const form = reactive({
name: '',
studentNumber: '',
collegeId: '',
majorId: '',
grade: undefined as number | undefined,
administrativeClassId: '',
password: '',
confirmPassword: '',
})
const availableMajors = computed(() =>
majors.value.filter((item) => item.collegeId === form.collegeId),
)
const availableGrades = computed(() => {
const values = classes.value
.filter((item) => item.majorId === form.majorId)
.map((item) => Number(item.grade))
return [...new Set(values)].sort((a, b) => b - a)
})
const availableClasses = computed(() =>
classes.value.filter((item) =>
item.majorId === form.majorId && Number(item.grade) === form.grade,
),
)
watch(() => form.collegeId, () => {
form.majorId = ''
form.grade = undefined
form.administrativeClassId = ''
})
watch(() => form.majorId, () => {
form.grade = undefined
form.administrativeClassId = ''
})
watch(() => form.grade, () => {
form.administrativeClassId = ''
})
function validate() {
if (
!form.name.trim() ||
!form.studentNumber.trim() ||
!form.collegeId ||
!form.majorId ||
!form.grade ||
!form.administrativeClassId
) {
ElMessage.warning('请完整填写学生身份信息。')
return false
}
if (
form.password.length < 8 ||
!/[a-z]/.test(form.password) ||
!/[A-Z]/.test(form.password) ||
!/\d/.test(form.password) ||
!/[^A-Za-z0-9]/.test(form.password)
) {
ElMessage.warning('密码至少 8 位,并同时包含大小写字母、数字和特殊字符。')
return false
}
if (form.password !== form.confirmPassword) {
ElMessage.warning('两次输入的密码不一致。')
return false
}
return true
}
async function activate() {
if (!validate()) return
submitting.value = true
try {
const { data } = await http.post('/auth/activate-student', {
name: form.name.trim(),
studentNumber: form.studentNumber.trim(),
collegeId: form.collegeId,
majorId: form.majorId,
grade: form.grade,
administrativeClassId: form.administrativeClassId,
password: form.password,
})
ElMessage.success('账号激活成功,请使用学号和新密码登录。')
await router.replace({
name: 'login',
query: { userName: data.userName, activated: '1' },
})
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
submitting.value = false
}
}
onMounted(async () => {
loading.value = true
try {
const { data } = await http.get('/auth/activation-options')
colleges.value = data.colleges
majors.value = data.majors
classes.value = data.classes
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
loading.value = false
}
})
</script>
<template>
<main class="activation-page">
<header class="activation-header">
<router-link to="/login">明序教务</router-link>
<router-link to="/timetable">班级课表查询</router-link>
</header>
<section class="activation-card" v-loading="loading">
<div class="activation-intro">
<span>STUDENT ACCOUNT ACTIVATION</span>
<h1>学生账号自助激活</h1>
<p>请按学籍档案如实填写全部信息匹配后学号将成为登录账号</p>
</div>
<el-alert
type="info"
:closable="false"
title="每个学号只能激活一次。已激活或忘记密码的账号,请联系教务管理员处理。"
/>
<el-form label-position="top" class="activation-form" @submit.prevent="activate">
<div class="activation-grid">
<el-form-item label="姓名" required>
<el-input v-model="form.name" autocomplete="name" placeholder="与学籍档案一致" />
</el-form-item>
<el-form-item label="学号" required>
<el-input v-model="form.studentNumber" autocomplete="username" placeholder="完整学号" />
</el-form-item>
</div>
<div class="activation-grid">
<el-form-item label="学院" required>
<el-select v-model="form.collegeId" filterable placeholder="选择学院">
<el-option
v-for="item in colleges"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="专业" required>
<el-select
v-model="form.majorId"
filterable
:disabled="!form.collegeId"
placeholder="选择专业"
>
<el-option
v-for="item in availableMajors"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</div>
<div class="activation-grid">
<el-form-item label="年级" required>
<el-select
v-model="form.grade"
:disabled="!form.majorId"
placeholder="选择年级"
>
<el-option
v-for="grade in availableGrades"
:key="grade"
:label="`${grade} 级`"
:value="grade"
/>
</el-select>
</el-form-item>
<el-form-item label="行政班" required>
<el-select
v-model="form.administrativeClassId"
filterable
:disabled="!form.grade"
placeholder="选择行政班"
>
<el-option
v-for="item in availableClasses"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</div>
<div class="activation-grid">
<el-form-item label="设置密码" required>
<el-input
v-model="form.password"
type="password"
show-password
autocomplete="new-password"
/>
</el-form-item>
<el-form-item label="确认密码" required>
<el-input
v-model="form.confirmPassword"
type="password"
show-password
autocomplete="new-password"
@keyup.enter="activate"
/>
</el-form-item>
</div>
<p class="password-rule">至少 8 须包含大写字母小写字母数字和特殊字符</p>
<el-button
class="activation-submit"
type="primary"
size="large"
native-type="submit"
:loading="submitting"
>
核验信息并激活
</el-button>
</el-form>
<footer>
已有账号
<router-link to="/login">返回登录</router-link>
</footer>
</section>
</main>
</template>
<style scoped>
.activation-page { min-height: 100vh; padding: 0 24px 48px; background: #eef3f6; color: #17324d; }
.activation-header { max-width: 920px; height: 68px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #d7e0e7; }
.activation-header a { color: #17324d; font-weight: 700; text-decoration: none; }
.activation-header a:last-child { color: #176b87; font-size: 13px; }
.activation-card { max-width: 760px; margin: 44px auto 0; padding: 36px 42px; background: #fff; border: 1px solid #d7e0e7; box-shadow: 0 14px 32px rgba(23, 50, 77, .08); }
.activation-intro { margin-bottom: 22px; }
.activation-intro span { color: #176b87; font-size: 11px; font-weight: 750; letter-spacing: .12em; }
.activation-intro h1 { margin: 8px 0; font-size: 28px; }
.activation-intro p { margin: 0; color: #687b8c; }
.activation-form { margin-top: 24px; }
.activation-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.activation-form :deep(.el-select) { width: 100%; }
.password-rule { margin: -4px 0 18px; color: #738596; font-size: 12px; }
.activation-submit { width: 100%; }
footer { margin-top: 22px; color: #718191; font-size: 13px; text-align: center; }
footer a { color: #176b87; font-weight: 700; text-decoration: none; }
@media (max-width: 640px) {
.activation-page { padding: 0 14px 28px; }
.activation-card { margin-top: 22px; padding: 26px 20px; }
.activation-grid { grid-template-columns: 1fr; gap: 0; }
}
</style>
+5 -1
View File
@@ -8,7 +8,10 @@ const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const loading = ref(false)
const form = reactive({ userName: 'admin', password: 'Admin@123456' })
const form = reactive({
userName: String(route.query.userName ?? 'admin'),
password: route.query.activated ? '' : 'Admin@123456',
})
async function submit() {
loading.value = true
@@ -80,6 +83,7 @@ async function submit() {
进入工作台
</el-button>
<router-link class="public-timetable-link" to="/timetable">无需登录查询班级课表 </router-link>
<router-link class="account-activation-link" to="/activate">学生首次登录自助激活账号 </router-link>
<div class="dev-hint">
<b>本地开发账号</b>
<span>admin / Admin@123456</span>
+4 -34
View File
@@ -113,7 +113,6 @@ function resetForm(row?: any) {
phone: '',
email: '',
notes: '',
initialPassword: '',
}, row ?? {})
} else {
const currentYear = new Date().getFullYear()
@@ -129,7 +128,6 @@ function resetForm(row?: any) {
phone: '',
email: '',
notes: '',
initialPassword: '',
}, row ?? {})
}
}
@@ -268,22 +266,11 @@ async function save() {
ElMessage.warning(`请填写${numberLabel.value}和姓名。`)
return
}
if ((!editingId.value || !form.userId) && String(form.initialPassword ?? '').length < 8) {
ElMessage.warning('请设置至少 8 位初始密码,系统会同时创建同号登录账号。')
return
}
try {
const path = `/personnel/${active.value}`
const payload = { ...form }
if (editingId.value && form.userId) {
delete payload.initialPassword
}
if (editingId.value) {
await http.put(`${path}/${editingId.value}`, payload)
} else {
await http.post(path, payload)
}
ElMessage.success(editingId.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) {
@@ -423,7 +410,7 @@ watch(active, async () => {
<el-table-column label="登录账号" width="105">
<template #default="{ row }">
<span class="table-status" :class="{ off: !row.userId }">
{{ row.userId ? '已开通' : '未开通' }}
{{ row.userId ? '已激活' : '未激活' }}
</span>
</template>
</el-table-column>
@@ -521,23 +508,6 @@ watch(active, async () => {
<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-alert
v-if="!editingId || !form.userId"
type="info"
:closable="false"
:title="editingId
? '该历史档案尚未开通账号,本次保存会以工号或学号补建登录账号。'
: '保存档案后,系统会以工号或学号作为登录账号,并自动分配对应角色。'"
/>
<el-form-item v-if="!editingId || !form.userId" label="初始密码" required>
<el-input
v-model="form.initialPassword"
type="password"
show-password
autocomplete="new-password"
placeholder="至少 8 位,仅用于创建密码哈希"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>