完善报名系统

This commit is contained in:
LiuQAQQWQ 2025-12-04 09:39:51 +08:00
parent 43ccbf165c
commit 18be83519d
13 changed files with 1362 additions and 201 deletions

View File

@ -0,0 +1,26 @@
import request from '@/utils/request'
// 学生基本信息列表
export function stuBaseInfoLists(params?: Record<string, any>) {
return request.get({ url: '/stuBaseInfo/list', params })
}
// 学生基本信息详情
export function stuBaseInfoDetail(params: Record<string, any>) {
return request.get({ url: '/stuBaseInfo/detail', params })
}
// 学生基本信息新增
export function stuBaseInfoAdd(params: Record<string, any>) {
return request.post({ url: '/stuBaseInfo/add', params })
}
// 学生基本信息编辑
export function stuBaseInfoEdit(params: Record<string, any>) {
return request.post({ url: '/stuBaseInfo/edit', params })
}
// 学生基本信息删除
export function stuBaseInfoDelete(params: Record<string, any>) {
return request.post({ url: '/stuBaseInfo/del', params })
}

View File

@ -0,0 +1,333 @@
<template>
<div class="edit-popup">
<popup
ref="popupRef"
:title="popupTitle"
:async="true"
width="550px"
:clickModalClose="true"
@confirm="handleSubmit"
@close="handleClose"
>
<el-form ref="formRef" :model="formData" label-width="84px" :rules="formRules">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="formData.idCard" placeholder="请输入身份证号" />
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
class="flex-1 !flex"
v-model="formData.birthday"
type="datetime"
clearable
value-format="YYYY-MM-DD hh:mm:ss"
placeholder="请选择出生日期"
/>
</el-form-item>
<el-form-item label="民族" prop="nationality">
<el-input v-model="formData.nationality" placeholder="请输入民族" />
</el-form-item>
<el-form-item label="政治面貌" prop="politicalStatus">
<el-radio-group v-model="formData.politicalStatus" placeholder="请选择政治面貌">
<el-radio :label="0">群众</el-radio>
<el-radio :label="1">团员</el-radio>
<el-radio :label="2">党员</el-radio>
<el-radio :label="3">其他</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="手机号码" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入手机号码" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="紧急联系人" prop="emergencyContact">
<el-input v-model="formData.emergencyContact" placeholder="请输入紧急联系人" />
</el-form-item>
<el-form-item label="紧急联系电话" prop="emergencyPhone">
<el-input v-model="formData.emergencyPhone" placeholder="请输入紧急联系电话" />
</el-form-item>
<el-form-item label="与紧急联系人关系" prop="relationship">
<el-input v-model="formData.relationship" placeholder="请输入关系" />
</el-form-item>
<el-form-item label="家庭住址" prop="homeAddress">
<el-input v-model="formData.homeAddress" placeholder="请输入家庭住址" />
</el-form-item>
<el-form-item label="籍贯" prop="nativePlace">
<el-input v-model="formData.nativePlace" placeholder="请输入籍贯" />
</el-form-item>
<el-form-item label="邮政编码" prop="postalCode">
<el-input v-model="formData.postalCode" placeholder="请输入邮政编码" />
</el-form-item>
<el-form-item label="毕业学校" prop="previousSchool">
<el-input v-model="formData.previousSchool" placeholder="请输入毕业学校" />
</el-form-item>
<el-form-item label="学校类型" prop="schoolType">
<el-select
class="flex-1"
v-model="formData.schoolType"
placeholder="请选择学校类型"
>
<el-option label="普通高中" :value="1" />
<el-option label="职业高中" :value="2" />
<el-option label="中专" :value="3" />
<el-option label="大专" :value="4" />
<el-option label="本科" :value="5" />
<el-option label="其他" :value="6" />
</el-select>
</el-form-item>
<el-form-item label="学历" prop="academicQualification">
<el-select
class="flex-1"
v-model="formData.academicQualification"
placeholder="请选择学历"
>
<el-option label="初中" :value="1" />
<el-option label="高中" :value="2" />
<el-option label="中专" :value="3" />
<el-option label="大专" :value="4" />
<el-option label="本科" :value="5" />
<el-option label="硕士" :value="6" />
<el-option label="博士" :value="7" />
</el-select>
</el-form-item>
</el-form>
</popup>
</div>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import type { PropType } from 'vue'
import { stuBaseInfoAdd, stuBaseInfoDetail, stuBaseInfoEdit } from '@/api/stuBaseInfo'
import Popup from '@/components/popup/index.vue'
import feedback from '@/utils/feedback'
defineProps({
dictData: {
type: Object as PropType<Record<string, any[]>>,
default: () => ({})
}
})
const emit = defineEmits(['success', 'close'])
const formRef = shallowRef<FormInstance>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const mode = ref('add')
const popupTitle = computed(() => {
return mode.value == 'edit' ? '编辑学生基本信息' : '新增学生基本信息'
})
const formData = reactive({
id: '',
userId: '',
name: '',
gender: '',
idCard: '',
birthday: '',
nationality: '',
politicalStatus: '',
phone: '',
email: '',
emergencyContact: '',
emergencyPhone: '',
relationship: '',
homeAddress: '',
nativePlace: '',
postalCode: '',
previousSchool: '',
schoolType: '',
graduationYear: '',
academicQualification: ''
})
const formRules = {
id: [
{
required: true,
message: '请输入主键ID',
trigger: ['blur']
}
],
userId: [
{
required: true,
message: '请输入关联用户ID',
trigger: ['blur']
}
],
name: [
{
required: true,
message: '请输入姓名',
trigger: ['blur']
}
],
gender: [
{
required: true,
message: '请输入性别',
trigger: ['blur']
}
],
idCard: [
{
required: true,
message: '请输入身份证号',
trigger: ['blur']
}
],
birthday: [
{
required: true,
message: '请选择出生日期',
trigger: ['blur']
}
],
nationality: [
{
required: true,
message: '请输入民族',
trigger: ['blur']
}
],
politicalStatus: [
{
required: true,
message: '请选择政治面貌',
trigger: ['blur']
}
],
phone: [
{
required: true,
message: '请输入手机号码',
trigger: ['blur']
}
],
email: [
{
required: true,
message: '请输入邮箱',
trigger: ['blur']
}
],
emergencyContact: [
{
required: true,
message: '请输入紧急联系人',
trigger: ['blur']
}
],
emergencyPhone: [
{
required: true,
message: '请输入紧急联系电话',
trigger: ['blur']
}
],
relationship: [
{
required: true,
message: '请输入与紧急联系人关系',
trigger: ['blur']
}
],
homeAddress: [
{
required: true,
message: '请输入家庭住址',
trigger: ['blur']
}
],
nativePlace: [
{
required: true,
message: '请输入籍贯',
trigger: ['blur']
}
],
postalCode: [
{
required: true,
message: '请输入邮政编码',
trigger: ['blur']
}
],
previousSchool: [
{
required: true,
message: '请输入毕业学校',
trigger: ['blur']
}
],
schoolType: [
{
required: true,
message: '请选择学校类型',
trigger: ['blur']
}
],
graduationYear: [
{
required: true,
message: '请输入毕业年份',
trigger: ['blur']
}
],
academicQualification: [
{
required: true,
message: '请选择学历',
trigger: ['blur']
}
],
majorStudied: [
{
required: true,
message: '请输入原所学专业',
trigger: ['blur']
}
]
}
const handleSubmit = async () => {
await formRef.value?.validate()
const data: any = { ...formData }
mode.value == 'edit' ? await stuBaseInfoEdit(data) : await stuBaseInfoAdd(data)
popupRef.value?.close()
feedback.msgSuccess('操作成功')
emit('success')
}
const open = (type = 'add') => {
mode.value = type
popupRef.value?.open()
}
const setFormData = async (data: Record<string, any>) => {
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
//@ts-ignore
formData[key] = data[key]
}
}
}
const getDetail = async (row: Record<string, any>) => {
const data = await stuBaseInfoDetail({
id: row.id
})
setFormData(data)
}
const handleClose = () => {
emit('close')
}
defineExpose({
open,
setFormData,
getDetail
})
</script>

View File

@ -0,0 +1,230 @@
<template>
<div class="index-lists">
<el-card class="!border-none" shadow="never">
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item label="姓名" prop="name">
<el-input class="w-[280px]" v-model="queryParams.name" />
</el-form-item>
<el-form-item label="身份证号" prop="idCard">
<el-input class="w-[280px]" v-model="queryParams.idCard" />
</el-form-item>
<el-form-item label="民族" prop="nationality">
<el-input class="w-[280px]" v-model="queryParams.nationality" />
</el-form-item>
<el-form-item label="政治面貌" prop="politicalStatus">
<el-select v-model="queryParams.politicalStatus" class="w-[280px]" clearable>
<el-option label="群众" :value="0" />
<el-option label="团员" :value="1" />
<el-option label="党员" :value="2" />
<el-option label="其他" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="手机号码" prop="phone">
<el-input class="w-[280px]" v-model="queryParams.phone" />
</el-form-item>
<el-form-item label="籍贯" prop="nativePlace">
<el-input class="w-[280px]" v-model="queryParams.nativePlace" />
</el-form-item>
<el-form-item label="学校类型" prop="schoolType">
<el-select v-model="queryParams.schoolType" class="w-[280px]" clearable>
<el-option label="普通高中" :value="1" />
<el-option label="职业高中" :value="2" />
<el-option label="中专" :value="3" />
<el-option label="大专" :value="4" />
<el-option label="本科" :value="5" />
<el-option label="其他" :value="6" />
</el-select>
</el-form-item>
<el-form-item label="学历" prop="academicQualification">
<el-select
v-model="queryParams.academicQualification"
class="w-[280px]"
clearable
>
<el-option label="初中" :value="1" />
<el-option label="高中" :value="2" />
<el-option label="中专" :value="3" />
<el-option label="大专" :value="4" />
<el-option label="本科" :value="5" />
<el-option label="硕士" :value="6" />
<el-option label="博士" :value="7" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
<el-button v-perms="['stuBaseInfo:add']" type="primary" @click="handleAdd()">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
</div>
<el-table class="mt-4" size="large" v-loading="pager.loading" :data="pager.lists">
<el-table-column label="关联用户ID" prop="userId" min-width="100" />
<el-table-column label="姓名" prop="name" min-width="100" />
<el-table-column
label="性别"
prop="gender"
min-width="100"
:formatter="formatGender"
/>
<el-table-column label="身份证号" prop="idCard" min-width="100" />
<el-table-column label="出生日期" prop="birthday" min-width="100" />
<el-table-column label="民族" prop="nationality" min-width="100" />
<el-table-column
label="政治面貌"
prop="politicalStatus"
min-width="100"
:formatter="formatPoliticalStatus"
/>
<el-table-column label="手机号码" prop="phone" min-width="100" />
<el-table-column label="邮箱" prop="email" min-width="100" />
<el-table-column label="紧急联系人" prop="emergencyContact" min-width="100" />
<el-table-column label="紧急联系电话" prop="emergencyPhone" min-width="100" />
<el-table-column label="与紧急联系人关系" prop="relationship" min-width="100" />
<el-table-column label="家庭住址" prop="homeAddress" min-width="100" />
<el-table-column label="籍贯" prop="nativePlace" min-width="100" />
<el-table-column label="邮政编码" prop="postalCode" min-width="100" />
<el-table-column label="毕业学校" prop="previousSchool" min-width="100" />
<el-table-column
label="学校类型"
prop="schoolType"
min-width="100"
:formatter="formatSchoolType"
/>
<el-table-column label="毕业年份" prop="graduationYear" min-width="100" />
<el-table-column
label="学历"
prop="academicQualification"
min-width="100"
:formatter="formatAcademicQualification"
/>
<el-table-column label="创建时间" prop="createTime" min-width="100" />
<el-table-column label="更新时间" prop="updateTime" min-width="100" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['stuBaseInfo:edit']"
type="primary"
link
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-perms="['stuBaseInfo:del']"
type="danger"
link
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
<script lang="ts" setup name="stuBaseInfo">
import { stuBaseInfoDelete, stuBaseInfoLists } from '@/api/stuBaseInfo'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import EditPopup from './edit.vue'
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
const showEdit = ref(false)
const queryParams = reactive({
userId: '',
name: '',
gender: '',
idCard: '',
nationality: '',
politicalStatus: '',
phone: '',
nativePlace: '',
schoolType: '',
graduationYear: '',
academicQualification: ''
})
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: stuBaseInfoLists,
params: queryParams
})
const formatGender = (row: any, column: any, cellValue: any) => {
const genderMap: Record<string | number, string> = {
'0': '未知',
'1': '男',
'2': '女'
}
return genderMap[String(cellValue)] || cellValue
}
const formatPoliticalStatus = (row: any, column: any, cellValue: any) => {
const statusMap: Record<string | number, string> = {
'0': '群众',
'1': '团员',
'2': '党员',
'3': '其他'
}
return statusMap[String(cellValue)] || cellValue
}
const formatSchoolType = (row: any, column: any, cellValue: any) => {
const statusMap: Record<string | number, string> = {
'1': '普通高中',
'2': '职业高中',
'3': '中专',
'4': '大专',
'5': '本科',
'6': '其他'
}
return statusMap[String(cellValue)] || cellValue
}
const formatAcademicQualification = (row: any, column: any, cellValue: any) => {
const statusMap: Record<string | number, string> = {
'1': '初中',
'2': '高中',
'3': '中专',
'4': '大专',
'5': '本科',
'6': '硕士',
'7': '博士'
}
return statusMap[String(cellValue)] || cellValue
}
const handleAdd = async () => {
showEdit.value = true
await nextTick()
editRef.value?.open('add')
}
const handleEdit = async (data: any) => {
showEdit.value = true
await nextTick()
editRef.value?.open('edit')
editRef.value?.getDetail(data)
}
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除?')
await stuBaseInfoDelete({ id })
feedback.msgSuccess('删除成功')
getLists()
}
getLists()
</script>

View File

@ -18,10 +18,10 @@
</el-form-item>
<el-form-item label="年级" prop="grade">
<el-select class="flex-1" v-model="formData.grade" placeholder="请选择年级: ">
<el-option label="一年级" value="1" />
<el-option label="二年级" value="2" />
<el-option label="三年级" value="3" />
<el-option label="四年级" value="4" />
<el-option label="一年级" :value="1" />
<el-option label="二年级" :value="2" />
<el-option label="三年级" :value="3" />
<el-option label="四年级" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="班级" prop="className">
@ -36,10 +36,10 @@
v-model="formData.studentStatus"
placeholder="请选择学生状态: "
>
<el-option label="在读" value="1" />
<el-option label="休学" value="2" />
<el-option label="毕业" value="3" />
<el-option label="退学" value="4" />
<el-option label="在读" :value="1" />
<el-option label="休学" :value="2" />
<el-option label="毕业" :value="3" />
<el-option label="退学" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="身份证号" prop="idCard">
@ -51,9 +51,9 @@
v-model="formData.politicalStatus"
placeholder="请选择政治面貌: "
>
<el-option label="党员" value="1" />
<el-option label="团员" value="2" />
<el-option label="群众" value="3" />
<el-option label="党员" :value="1" />
<el-option label="团员" :value="2" />
<el-option label="群众" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="籍贯" prop="nativePlace">

View File

@ -3,24 +3,7 @@
* @return { Promise }
*/
export function getStudentBaseInfo() {
// 模拟后端数据
return new Promise((resolve) => {
setTimeout(() => {
resolve({
data: {
name: '张三',
gender: 'male',
idCard: '110101201001011234',
phone: '13800138000',
address: '北京市海淀区XX街道XX小区',
emergencyContact: '李四',
emergencyPhone: '13900139000'
}
})
}, 500) // 模拟网络延迟
})
// 真实接口(后端恢复后启用)
// return $request.get({ url: '/enrollment/studentInfo', params })
return $request.get({ url: '/enrollment/studentBaseInfo' })
}
/**
@ -29,19 +12,14 @@ export function getStudentBaseInfo() {
* @return { Promise }
*/
export function updateStudentBaseInfo(params: any) {
// 模拟后端数据
return new Promise((resolve) => {
setTimeout(() => {
resolve({
data: {
success: true,
message: '信息保存成功'
}
})
}, 500)
})
// 真实接口(后端恢复后启用)
// return $request.post({ url: '/enrollment/studentInfo', params })
return $request.post({ url: '/enrollment/studentBaseInfo', params })
}
/**
* @description "提交报名"
*/
export function submitEnrollmentInfo() {
return $request.post({ url: '/enrollment/submit' })
}
/**
@ -49,23 +27,5 @@ export function updateStudentBaseInfo(params: any) {
* @return { Promise }
*/
export function getEnrollmentProcessStatus() {
// 模拟后端数据
return new Promise((resolve) => {
setTimeout(() => {
resolve({
data: {
label: '审核完毕',
type: 'success',
steps: [
{ completed: true, time: '2025-05-01 10:30:00' },
{ completed: true, time: '2025-05-01 10:30:00' },
{ completed: false, time: '' },
{ completed: false, time: '' }
]
}
})
}, 500)
})
// 真实接口(后端恢复后启用)
// return $request.get({ url: '/enrollment/processStatus' })
return $request.get({ url: '/enrollment/processStatus' })
}

View File

@ -1,6 +1,5 @@
body {
@apply text-base text-tx-primary bg-page;
min-width: 1200px;
}
body,
html {

View File

@ -5,8 +5,11 @@ export const NAVBAR = [
},
{
name: '管理后台',
path: '/admin',
component: 'admin'
path: 'http://192.168.123.110:5173/workbench'
},
{
name: '接口文档',
path: 'http://192.168.123.111:8084/doc.html#/home'
}
]

View File

@ -1,21 +1,29 @@
<template>
<footer class="layout-footer text-center bg-[#222222] py-[30px]">
<div class="text-[#bebebe]">
<!-- <NuxtLink> 关于我们 </NuxtLink>
-->
<NuxtLink :to="`/policy/${PolicyAgreementEnum.SERVICE}`">
<footer
class="layout-footer text-center bg-[#222222] py-[30px] md:py-[30px] sm:py-[15px]"
>
<!-- 链接区域添加类名方便适配分隔符单独包裹 -->
<div class="text-[#bebebe] footer-links">
<NuxtLink
class="footer-link"
:to="`/policy/${PolicyAgreementEnum.SERVICE}`"
>
用户协议
</NuxtLink>
<NuxtLink :to="`/policy/${PolicyAgreementEnum.PRIVACY}`">
<span class="footer-divider"></span>
<NuxtLink
class="footer-link"
:to="`/policy/${PolicyAgreementEnum.PRIVACY}`"
>
隐私政策
</NuxtLink>
<NuxtLink to="/user/info"> 会员中心 </NuxtLink>
<span class="footer-divider"></span>
<NuxtLink class="footer-link" to="/user/info"> 会员中心 </NuxtLink>
</div>
<div class="mt-4 text-tx-secondary">
<!-- 版权信息调整间距类名 -->
<div class="mt-4 sm:mt-2 text-tx-secondary copyright">
<a
class="mx-1 hover:underline"
class="mx-1 sm:mx-2 sm:mb-2 hover:underline"
:href="item.value"
target="_blank"
v-for="item in appStore.getCopyrightConfig"
@ -26,10 +34,53 @@
</div>
</footer>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/stores/app'
import { PolicyAgreementEnum } from '@/enums/appEnums'
const appStore = useAppStore()
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.layout-footer {
.footer-links {
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
line-height: 1.5;
.footer-link {
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s;
&:hover {
background-color: rgba(255, 255, 255, 0.08);
}
}
}
.copyright {
font-size: 13px;
line-height: 1.6;
}
@media (max-width: 767px) {
.footer-links {
font-size: 12px; //
flex-wrap: wrap; //
gap: 6px 10px; // 6px10px
.footer-divider {
display: none;
}
.footer-link {
padding: 6px 12px;
}
}
.copyright {
font-size: 11px; //
a {
display: inline-block; // margin-bottom
margin-bottom: 6px; //
}
}
}
}
</style>

View File

@ -1,19 +1,130 @@
<template>
<header class="layout-header text-white bg-primary">
<div class="header-contain">
<Logo class="flex-none mr-4" />
<Navbar class="w-[600px]" />
<!-- 移动端菜单按钮 -->
<button
v-if="isMobile"
@click="showMobileMenu = !showMobileMenu"
class="md:hidden mr-2 p-2 rounded-lg hover:bg-primary-dark transition-colors"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-if="!showMobileMenu"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
<path
v-else
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<Logo class="flex-none mr-4" :class="{'mr-2': isMobile}" />
<!-- 桌面端导航栏 -->
<Navbar v-if="!isMobile" class="w-[600px]" />
<div class="flex-1"></div>
<Search class="mr-[40px] flex-none" />
<!-- 搜索框 - 移动端显示图标点击展开 -->
<div class="relative">
<!-- 移动端搜索图标 -->
<button
v-if="isMobile && !showSearch"
@click="showSearch = true"
class="md:hidden mr-4 p-2 rounded-lg hover:bg-primary-dark transition-colors"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</button>
<!-- 移动端搜索框 -->
<div
v-if="isMobile && showSearch"
class="md:hidden absolute right-0 top-0 z-10 bg-primary p-2 rounded-lg shadow-lg"
>
<Search class="w-[200px]" />
<button
@click="showSearch = false"
class="absolute -top-1 -right-1 p-1 bg-white text-primary rounded-full"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- 桌面端搜索框 -->
<Search v-if="!isMobile" class="mr-[40px] flex-none" />
</div>
<User class="flex-none" />
</div>
<!-- 移动端导航菜单 -->
<div
v-if="isMobile && showMobileMenu"
class="md:hidden absolute top-full left-0 right-0 bg-primary shadow-lg z-50"
>
<Navbar
class="w-full"
mode="vertical"
@item-click="showMobileMenu = false"
/>
</div>
</header>
</template>
<script lang="ts" setup>
import User from './user.vue'
import Search from './search.vue'
import Logo from './logo.vue'
import Navbar from './navbar.vue'
import { ref, onMounted, onUnmounted } from 'vue'
//
const isMobile = ref(false)
const showMobileMenu = ref(false)
const showSearch = ref(false)
const checkMobile = () => {
isMobile.value = window.outerWidth < 768 // Tailwind md
}
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
//
window.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
window.removeEventListener('resize', checkMobile)
window.removeEventListener('click', handleClickOutside)
})
//
const handleClickOutside = (event: MouseEvent) => {
const header = document.querySelector('.layout-header')
if (header && !header.contains(event.target as Node)) {
showMobileMenu.value = false
showSearch.value = false
}
}
//
const route = useRoute()
watch(() => route.path, () => {
showMobileMenu.value = false
showSearch.value = false
})
</script>
<style lang="scss" scoped>
@ -24,12 +135,19 @@ import Navbar from './navbar.vue'
top: 0;
width: 100%;
z-index: 1999;
.header-contain {
height: 100%;
display: flex;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
@media (min-width: 768px) {
padding: 0;
}
.navbar {
--el-menu-item-font-size: var(--el-font-size-large);
--el-menu-bg-color: var(--el-color-primary);

View File

@ -1,8 +1,21 @@
<template>
<main class="mx-auto w-[1200px] py-4">
<main class="mx-auto py-4 flex" style="min-width: 62vw; min-height: 100vh">
<!-- 移动端菜单按钮 -->
<button
v-if="sidebar.length && isMobile"
@click="showMobileMenu = true"
class="fixed left-4 top-[68px] z-50 md:hidden p-2 rounded-full bg-white shadow-lg transition-all duration-300"
:class="{ '-translate-x-full': showMobileMenu, 'translate-x-0': !showMobileMenu }"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<!-- 桌面端侧边栏 -->
<div
v-if="sidebar.length"
class="mr-4 bg-white rounded-[8px] overflow-hidden"
v-if="sidebar.length && !isMobile"
class="hidden md:block mr-4 bg-white rounded-[8px] overflow-hidden"
>
<Menu
:menu="sidebar"
@ -10,11 +23,42 @@
mode="vertical"
/>
</div>
<!-- 移动端抽屉式侧边栏 -->
<div
v-if="sidebar.length && isMobile"
class="fixed inset-0 z-40"
:class="{'pointer-events-none': !showMobileMenu}"
>
<!-- 遮罩层 -->
<div
v-if="showMobileMenu"
@click="showMobileMenu = false"
class="absolute inset-0 transition-opacity"
/>
<!-- 侧边栏内容 -->
<div
class="absolute top-[60px] left-0 h-full w-64 bg-white transform transition-transform duration-300"
:class="{'translate-x-0': showMobileMenu, '-translate-x-full': !showMobileMenu}"
>
<Menu
:menu="sidebar"
:default-active="activeMenu"
mode="vertical"
@click="showMobileMenu = false"
/>
</div>
</div>
<!-- 主内容区域 -->
<div
:class="[
'layout-page flex-1 min-w-0 rounded-[8px]',
'layout-page flex-1 min-w-0 rounded-[8px] transition-all duration-300',
{
'bg-body': hasSidebar
'bg-body': hasSidebar,
'md:ml-0': !isMobile || !sidebar.length,
'ml-0': isMobile && sidebar.length
}
]"
>
@ -22,9 +66,36 @@
</div>
</main>
</template>
<script lang="ts" setup>
import Menu from '../menu/index.vue'
import { ref, onMounted, onUnmounted } from 'vue'
const route = useRoute()
const activeMenu = computed<string>(() => route.meta.activeMenu ?? route.path)
const { sidebar, hasSidebar } = useMenu()
//
const isMobile = ref(false)
const showMobileMenu = ref(false)
const checkMobile = () => {
isMobile.value = window.outerWidth < 768 // 使 Tailwind md
}
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
})
onUnmounted(() => {
window.removeEventListener('resize', checkMobile)
})
//
watch(() => route.path, () => {
if (isMobile.value) {
showMobileMenu.value = false
}
})
</script>

View File

@ -1,5 +1,5 @@
<template>
<section class="layout-default min-w-[1200px]">
<section class="layout-default">
<LayoutHeader />
<div class="main-contain">
<LayoutMain class="flex-1 min-h-0 flex">
@ -22,7 +22,7 @@ const userStore = useUserStore();
</script>
<style lang="scss" scoped>
.main-contain {
min-height: calc(100vh - var(--header-height));
min-height: 100vh;
@apply flex flex-col;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="bg-white render-html p-[30px] w-[1200px] mx-auto min-h-screen">
<div class="bg-white render-html p-[30px] mx-auto min-h-screen">
<h1 class="text-center">{{ data.title }}</h1>
<div class="mx-auto" v-html="data.content"></div>
</div>

View File

@ -1,7 +1,42 @@
<template>
<div class="px-[30px] py-5 user-info min-h-full flex flex-col">
<div v-if="submission" class="border-b border-br pb-5">
<span class="text-2xl font-medium">注册状态</span>
</div>
<el-card
v-if="submission"
class="!border-none mb-4 flex-1"
shadow="never"
>
<div class="status-info mt-4">
<!-- 改为步骤条展示 -->
<div class="mb-6">
<div class="text-tx-secondary text-sm mb-4">状态进度</div>
<el-steps
:active="activeStep"
finish-status="success"
class="w-full"
>
<el-step
v-for="(step, index) in statusConfig.steps"
:key="index"
:title="stepTitles[index]"
:description="getStepDescription(index)"
:status="getStepStatus(index)"
/>
</el-steps>
</div>
<!-- 显示拒绝原因 -->
<div v-if="rejectionReason" class="text-danger mt-4">
<i class="el-icon-error mr-2"></i>
拒绝原因{{ rejectionReason }}
</div>
</div>
</el-card>
<div class="border-b border-br pb-5">
<span class="text-2xl font-medium">新生报名</span>
<span class="text-2xl font-medium">{{
submission ? '信息查看' : '新生报名'
}}</span>
</div>
<el-card class="!border-none mb-4" shadow="never">
<div class="lg:flex gap-6">
@ -17,26 +52,132 @@
<el-input
v-model="studentData.baseInfo.name"
placeholder="请输入姓名"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="性别">
<el-radio-group
v-model="studentData.baseInfo.gender"
:disabled="submission"
>
<el-radio label="male"></el-radio>
<el-radio label="female"></el-radio>
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="身份证号">
<el-input
v-model="studentData.baseInfo.idCard"
placeholder="请输入18位身份证号"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
v-model="studentData.baseInfo.birthday"
type="datetime"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请输入出生日期"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="联系方式">
<el-input
v-model="studentData.baseInfo.phone"
placeholder="请输入手机号码"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="电子邮箱">
<el-input
v-model="studentData.baseInfo.email"
placeholder="请输入电子邮箱"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="籍贯">
<el-input
v-model="studentData.baseInfo.nativePlace"
placeholder="请输入籍贯"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="民族">
<el-input
v-model="studentData.baseInfo.nationality"
placeholder="请输入民族"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="政治面貌">
<el-select
v-model="
studentData.baseInfo.politicalStatus
"
clearable
:disabled="submission"
>
<el-option label="群众" :value="0" />
<el-option label="团员" :value="1" />
<el-option label="党员" :value="2" />
<el-option label="其他" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="毕业年份">
<el-input-number
v-model="
studentData.baseInfo.graduationYear
"
placeholder="请输入毕业年份"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="毕业院校">
<el-input
v-model="
studentData.baseInfo.previousSchool
"
placeholder="请输入毕业院校"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="学校类型">
<el-select
v-model="studentData.baseInfo.schoolType"
clearable
:disabled="submission"
>
<el-option label="普通高中" :value="1" />
<el-option label="职业高中" :value="2" />
<el-option label="中专" :value="3" />
<el-option label="大专" :value="4" />
<el-option label="本科" :value="5" />
<el-option label="其他" :value="6" />
</el-select>
</el-form-item>
<el-form-item label="学历">
<el-select
v-model="
studentData.baseInfo
.academicQualification
"
clearable
:disabled="submission"
>
<el-option label="初中" :value="1" />
<el-option label="高中" :value="2" />
<el-option label="中专" :value="3" />
<el-option label="大专" :value="4" />
<el-option label="本科" :value="5" />
<el-option label="硕士" :value="6" />
<el-option label="博士" :value="7" />
</el-select>
</el-form-item>
<el-form-item label="邮政编码">
<el-input
v-model="studentData.baseInfo.postalCode"
placeholder="请输入邮政编码"
:disabled="submission"
/>
</el-form-item>
<el-form-item
@ -44,10 +185,11 @@
class="md:col-span-2"
>
<el-input
v-model="studentData.baseInfo.address"
v-model="studentData.baseInfo.homeAddress"
placeholder="请输入详细家庭住址"
type="textarea"
rows="3"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="紧急联系人">
@ -56,6 +198,7 @@
studentData.baseInfo.emergencyContact
"
placeholder="请输入紧急联系人姓名"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="紧急联系电话">
@ -64,6 +207,14 @@
studentData.baseInfo.emergencyPhone
"
placeholder="请输入紧急联系人电话"
:disabled="submission"
/>
</el-form-item>
<el-form-item label="与紧急联系人关系">
<el-input
v-model="studentData.baseInfo.relationship"
placeholder="请输入与紧急联系人的关系"
:disabled="submission"
/>
</el-form-item>
</div>
@ -73,83 +224,139 @@
</el-card>
<!-- 注册状态信息 -->
<el-card class="!border-none mb-4 flex-1" shadow="never">
<template #header>
<span>注册状态</span>
</template>
<div class="status-info mt-4">
<div class="mb-6">
<div class="text-tx-secondary text-sm mb-2">当前状态</div>
<el-tag :type="statusConfig.type">{{
statusConfig.label
}}</el-tag>
</div>
<div class="mb-6">
<div class="text-tx-secondary text-sm mb-2">状态进度</div>
<el-progress
:percentage="statusConfig.progress"
:stroke-width="6"
/>
</div>
<div class="space-y-3">
<div class="flex items-center">
<el-checkbox
v-model="statusConfig.steps[0].completed"
disabled
>
已报名
</el-checkbox>
<span> {{ statusConfig.steps[0].time }} </span>
</div>
<div class="flex items-center">
<el-checkbox
v-model="statusConfig.steps[1].completed"
disabled
>
材料审核
</el-checkbox>
<span>
{{ statusConfig.steps[1].time || '待完成' }}
</span>
</div>
<div class="flex items-center">
<el-checkbox
v-model="statusConfig.steps[2].completed"
disabled
>
已录取
</el-checkbox>
<span>
{{ statusConfig.steps[2].time || '待完成' }}
</span>
</div>
<div class="flex items-center">
<el-checkbox
v-model="statusConfig.steps[3].completed"
disabled
>
已入学
</el-checkbox>
<span>
{{ statusConfig.steps[3].time || '待完成' }}
</span>
</div>
</div>
</div>
<div class="mt-6">
<el-button type="primary" block @click="handleSave"
>保存信息</el-button
<div v-if="!submission" class="mt-6">
<el-button type="primary" block @click="handleSave">
保存信息
</el-button>
<el-button
class="ml-4"
type="success"
block
@click="handleShowConfirmDialog"
:disabled="statusConfig.steps[0].completed"
>
提交报名
</el-button>
</div>
</el-card>
<!-- 确认弹窗 -->
<el-dialog
title="确认提交"
v-model="showConfirmDialog"
width="600px"
:before-close="handleCloseDialog"
>
<div class="mb-4 text-warning">
<i class="el-icon-warning mr-2"></i>
提交信息后不可修改请检查后确认
</div>
<el-descriptions :column="1" border>
<el-descriptions-item label="学生姓名">{{
confirmInfo.name
}}</el-descriptions-item>
<el-descriptions-item label="性别">{{
confirmInfo.gender === 0 ? '男' : '女'
}}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{
confirmInfo.idCard
}}</el-descriptions-item>
<el-descriptions-item label="出生日期">{{
confirmInfo.birthday
}}</el-descriptions-item>
<el-descriptions-item label="民族">{{
confirmInfo.nationality
}}</el-descriptions-item>
<el-descriptions-item label="联系方式"
>电话{{ confirmInfo.phone }} 邮箱{{
confirmInfo.email
}}</el-descriptions-item
>
<el-descriptions-item label="籍贯">{{
confirmInfo.nativePlace
}}</el-descriptions-item>
<el-descriptions-item label="政治面貌">
{{
confirmInfo.politicalStatus === 0
? '群众'
: confirmInfo.politicalStatus === 1
? '团员'
: confirmInfo.politicalStatus === 2
? '党员'
: '其他'
}}
</el-descriptions-item>
<el-descriptions-item label="家庭住址">{{
confirmInfo.homeAddress
}}</el-descriptions-item>
<el-descriptions-item label="毕业年份">{{
confirmInfo.graduationYear
}}</el-descriptions-item>
<el-descriptions-item label="毕业院校">{{
confirmInfo.previousSchool
}}</el-descriptions-item>
<el-descriptions-item label="学校类型">
{{
confirmInfo.schoolType === 1
? '普通高中'
: confirmInfo.schoolType === 2
? '职业高中'
: confirmInfo.schoolType === 3
? '中专'
: confirmInfo.schoolType === 4
? '大专'
: confirmInfo.schoolType === 5
? '本科'
: '其他'
}}
</el-descriptions-item>
<el-descriptions-item label="学历">
{{
confirmInfo.academicQualification === 1
? '初中'
: confirmInfo.academicQualification === 2
? '高中'
: confirmInfo.academicQualification === 3
? '中专'
: confirmInfo.academicQualification === 4
? '大专'
: confirmInfo.academicQualification === 5
? '本科'
: confirmInfo.academicQualification === 6
? '硕士'
: '博士'
}}
</el-descriptions-item>
<el-descriptions-item label="邮政编码">{{
confirmInfo.postalCode
}}</el-descriptions-item>
<el-descriptions-item label="紧急联系人">{{
confirmInfo.emergencyContact
}}</el-descriptions-item>
<el-descriptions-item label="紧急联系电话">{{
confirmInfo.emergencyPhone
}}</el-descriptions-item>
<el-descriptions-item label="与紧急联系人关系">{{
confirmInfo.relationship
}}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="handleCloseDialog">取消</el-button>
<el-button type="primary" @click="handleConfirmSubmit">
确认提交
</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
import { ElButton } from 'element-plus'
import { reactive, computed, onMounted, ref } from 'vue'
import { ElButton, ElMessage } from 'element-plus'
import {
getStudentBaseInfo,
updateStudentBaseInfo,
getEnrollmentProcessStatus
getEnrollmentProcessStatus,
submitEnrollmentInfo
} from '@/api/enrollment'
definePageMeta({
@ -157,16 +364,54 @@ definePageMeta({
auth: true
})
//
const stepTitles = ['报名', '材料审核', '录取', '缴费']
const showConfirmDialog = ref(false)
const submission = ref(false) //
const rejectionReason = ref('') //
const confirmInfo = ref({
name: '',
gender: 0,
idCard: '',
birthday: '',
nationality: '',
phone: '',
email: '',
homeAddress: '',
nativePlace: '',
politicalStatus: 0,
previousSchool: '',
schoolType: null,
academicQualification: null,
postalCode: '',
graduationYear: 2025,
emergencyContact: '',
emergencyPhone: '',
relationship: ''
})
//
const studentData = reactive({
baseInfo: {
id: '',
name: '',
gender: 'male',
gender: 0,
idCard: '',
birthday: '',
nationality: '',
phone: '',
address: '',
email: '',
homeAddress: '',
nativePlace: '',
politicalStatus: 0,
previousSchool: '',
schoolType: '',
academicQualification: '',
postalCode: '',
graduationYear: 2025,
emergencyContact: '',
emergencyPhone: ''
emergencyPhone: '',
relationship: ''
},
additionalInfo: {
enrollYear: '',
@ -175,58 +420,183 @@ const studentData = reactive({
}
})
//
type TagType = '' | 'info' | 'success' | 'warning' | 'danger'
const statusConfig = reactive<{
label: string
type: TagType
progress: number
steps: { completed: boolean; time: string }[]
}>({
label: '未报名',
type: 'info',
get progress() {
const completedCount = this.steps.filter(
(step: { completed: any }) => step.completed
).length
return (completedCount / this.steps.length) * 100
},
// -
const statusConfig = reactive({
steps: [
{ completed: false, time: '' },
{ completed: false, time: '' },
{ completed: false, time: '' },
{ completed: false, time: '' }
]
{ completed: false, time: 0, status: 0 }, //
{ completed: false, time: 0, status: 0 }, //
{ completed: false, time: 0, status: 0 }, //
{ completed: false, time: 0, status: 0 } //
],
applicationNumber: '' //
})
const handleSave = async () => {
//
const formatTime = (timestamp: number) => {
if (!timestamp) return ''
const date = new Date(timestamp)
return date.toLocaleDateString()
}
//
const getStepDescription = (index: number) => {
const step = statusConfig.steps[index]
const timeStr = formatTime(step.time)
if (index === 0) {
return step.completed ? `已完成${timeStr}` : '状态异常'
} else if (index === 1) {
if (step.status === 0) return '待审核'
if (step.status === 1) return `审核通过 ${timeStr}`
if (step.status === 2) return `审核不通过 ${timeStr}`
} else if (index === 2) {
if (step.status === 0) return '待审核'
if (step.status === 1) return `已录取 ${timeStr}`
if (step.status === 2) return `未录取 ${timeStr}`
} else if (index === 3) {
if (step.status === 0) return '未缴费'
if (step.status === 1) return `已缴费 ${timeStr}`
}
return ''
}
//
const handleShowConfirmDialog = async () => {
try {
// APIstudentData.baseInfo
const res = await updateStudentBaseInfo(studentData.baseInfo)
//
if (res.data.success) {
ElMessage.success(res.data.message || '信息保存成功')
} else {
ElMessage.warning(res.data.message || '保存失败,请检查数据')
const info = await getStudentBaseInfo()
confirmInfo.value = info
showConfirmDialog.value = true
} catch (error) {
ElMessage.error('获取信息失败,请稍后重试')
console.error('获取确认信息失败:', error)
}
}
//
const handleCloseDialog = () => {
showConfirmDialog.value = false
}
//
const handleConfirmSubmit = async () => {
try {
await submitEnrollmentInfo()
showConfirmDialog.value = false
//
await loadProcessStatus()
submission.value = true
ElMessage.success('报名提交成功')
} catch (error) {
ElMessage.error('报名提交失败,请稍后重试')
console.error('提交报名信息失败:', error)
}
}
//
const loadProcessStatus = async () => {
try {
const statusRes = await getEnrollmentProcessStatus()
if (statusRes) {
submission.value = true
//
statusConfig.applicationNumber = statusRes.applicationNumber || ''
//
rejectionReason.value = statusRes.rejectionReason || ''
//
// 1:
statusConfig.steps[0] = {
completed: statusRes.applicationNumber,
time: statusRes.applicationTime,
status: statusRes.applicationTime > 0 ? 1 : 0
}
// 2:
statusConfig.steps[1] = {
completed: statusRes.approvalStatus === 1,
time: statusRes.applicationTime,
status: statusRes.approvalStatus
}
// 3:
statusConfig.steps[2] = {
completed: statusRes.admissionStatus === 1,
time: statusRes.admissionTime,
status: statusRes.admissionStatus
}
// 4:
statusConfig.steps[3] = {
completed: statusRes.paymentStatus === 1,
time: statusRes.paymentTime,
status: statusRes.paymentStatus
}
}
} catch (error) {
//
console.error('刷新状态失败:', error)
}
}
//
const activeStep = computed(() => {
//
if (
statusConfig.steps[1].status === 2 ||
statusConfig.steps[2].status === 2
) {
return statusConfig.steps[1].status === 2 ? 1 : 2
}
const count = completedStepCount.value
//
return count >= statusConfig.steps.length
? statusConfig.steps.length - 1
: count
})
//
const getStepStatus = (index: number) => {
//
if (statusConfig.steps[index].completed) {
return 'finish'
}
//
if (
(index === 1 && statusConfig.steps[index].status === 2) ||
(index === 2 && statusConfig.steps[index].status === 2)
) {
return 'error'
}
//
return index === activeStep.value ? 'process' : 'wait'
}
//
const completedStepCount = computed(() => {
return statusConfig.steps.filter((step) => step.completed).length
})
//
const handleSave = async () => {
try {
await updateStudentBaseInfo(studentData.baseInfo)
ElMessage.success('信息保存成功')
} catch (error) {
ElMessage.error('保存失败,请稍后重试')
console.error('更新信息失败:', error)
}
}
//
onMounted(async () => {
try {
// 1.
// 1.
const studentInfoRes = await getStudentBaseInfo()
Object.assign(studentData.baseInfo, studentInfoRes.data)
// 2.
const statusRes = await getEnrollmentProcessStatus()
// statusConfig
Object.assign(statusConfig, statusRes.data)
Object.assign(studentData.baseInfo, studentInfoRes)
if (studentInfoRes.id) {
studentData.baseInfo.id = studentInfoRes.id
}
// 2.
await loadProcessStatus()
} catch (error) {
//
ElMessage.error('数据加载失败,请稍后重试')
console.error('获取初始数据失败:', error)
}