Files
Academic-Affairs-System/web/src/views/AccountActivationView.vue
T
biss 30c15f89e5 取消新增/Excel 导入档案时自动创建账号。
取消修改人员档案时同步登录账号。
Excel 人员模板恢复为不含“初始密码”。
登录页新增“学生首次登录?自助激活账号”入口。
公开激活地址:http://127.0.0.1:5255/activate
2026-07-25 07:55:49 +08:00

269 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>