补充新生报名页面stu

This commit is contained in:
XiuHe 2026-03-05 14:53:39 +08:00
parent 2fe6a590e8
commit d85ebeb7f0
32 changed files with 1869 additions and 0 deletions

24
stu/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
stu/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
stu/README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

5
stu/global.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="vite/client" />
import { Request } from './src/utils/http/request'
declare global {
const $request: Request
}

13
stu/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>stu</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

34
stu/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "stu",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.13.2",
"element-plus": "^2.11.9",
"ofetch": "^1.5.1",
"vue": "^3.5.24",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"autoprefixer": "^8.0.0",
"postcss": "^8.5.6",
"postcss-loader": "^8.2.0",
"sass-embedded": "^1.93.3",
"tailwindcss": "^3.4.17",
"typescript": "~5.9.3",
"vite": "npm:rolldown-vite@7.2.5",
"vue-tsc": "^3.1.4"
},
"overrides": {
"vite": "npm:rolldown-vite@7.2.5"
}
}

6
stu/postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
stu/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

204
stu/src/App.vue Normal file
View File

@ -0,0 +1,204 @@
<!-- App.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const isMenuOpen = ref(false); //
</script>
<template>
<!-- 导航栏固定在页面顶端 -->
<header class="navbar">
<div class="navbar-container">
<!-- 品牌标识 -->
<div class="navbar-brand">
<router-link to="/">新生报名系统</router-link>
</div>
<!-- 桌面端导航 -->
<nav class="navbar-nav">
<router-link
to="/apply"
:class="{ active: route.path === '/apply' }"
>
报名注册
</router-link>
<router-link
to="/query"
:class="{ active: route.path === '/query' }"
>
状态查询
</router-link>
</nav>
<!-- 移动端菜单按钮 -->
<button class="menu-toggle" @click="isMenuOpen = !isMenuOpen">
<span v-if="!isMenuOpen"></span>
<span v-else></span>
</button>
</div>
<!-- 移动端导航菜单 -->
<div class="mobile-menu" v-if="isMenuOpen">
<router-link
to="/apply"
:class="{ active: route.path === '/apply' }"
@click="isMenuOpen = false"
>
报名注册
</router-link>
<router-link
to="/query"
:class="{ active: route.path === '/query' }"
@click="isMenuOpen = false"
>
状态查询
</router-link>
</div>
</header>
<main><router-view /></main>
</template>
<style scoped>
/* 重置页面默认边距,确保导航栏紧贴顶端 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
overflow-x: hidden; /* 防止横向滚动 */
}
/* 导航栏固定在顶端z-index设为最高确保不被遮挡 */
.navbar {
background-color: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: fixed; /* 改为fixed确保始终固定在顶端 */
top: 0;
left: 0;
right: 0;
z-index: 10; /* 提高层级,避免被其他元素覆盖 */
width: 100%;
}
.navbar-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
}
.navbar-brand a {
color: #165DFF;
font-size: 1.2rem;
font-weight: bold;
text-decoration: none;
}
.navbar-nav {
display: flex;
gap: 30px;
}
.navbar-nav a {
color: #333333;
text-decoration: none;
padding: 8px 0;
position: relative;
transition: color 0.3s;
}
.navbar-nav a:hover {
color: #165DFF;
}
.navbar-nav a.active {
color: #165DFF;
}
.navbar-nav a.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background-color: #165DFF;
}
.menu-toggle {
display: none;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #333333;
padding: 5px;
}
.mobile-menu {
display: none;
padding: 15px 20px;
background-color: #ffffff;
border-top: 1px solid #f0f0f0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.mobile-menu a {
display: block;
padding: 10px 0;
color: #333333;
text-decoration: none;
transition: color 0.3s;
}
.mobile-menu a:hover {
color: #165DFF;
}
.mobile-menu a.active {
color: #165DFF;
font-weight: bold;
}
/* 主内容区添加顶部内边距,避免被固定导航栏遮挡 */
main {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 80px 20px 20px; /* 顶部padding=导航栏高度(60px)+20px间距 */
}
.app {
padding: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.navbar-nav {
display: none;
}
.menu-toggle {
display: block;
}
.mobile-menu {
display: block;
}
/* 移动端主内容区顶部padding调整考虑展开的移动端菜单 */
main {
padding-top: 80px;
}
}
</style>

10
stu/src/api/enrollment.ts Normal file
View File

@ -0,0 +1,10 @@
// api/enrollment.ts
import request from './request'
export function submitEnrollmentInfo(params: any) {
return request.post('enrollment/submit', params)
}
export function getEnrollmentProcessStatus(params: any) {
return request.get('enrollment/processStatus', params)
}

145
stu/src/api/request.ts Normal file
View File

@ -0,0 +1,145 @@
// api/request.ts
export class Request {
private baseURL: string
private interceptors: {
request: Array<(config: any) => any>
response: Array<(response: any) => any>
}
constructor(baseURL: string = '') {
this.baseURL = baseURL
this.interceptors = {
request: [],
response: []
}
}
// 添加请求拦截器
useRequestInterceptor(interceptor: (config: any) => any) {
this.interceptors.request.push(interceptor)
}
// 添加响应拦截器
useResponseInterceptor(interceptor: (response: any) => any) {
this.interceptors.response.push(interceptor)
}
async request(method: string, url: string, data?: any) {
let config = {
method,
url: this.baseURL + url,
data,
headers: {},
// 添加params字段用于区分GET参数
params: method === 'GET' || method === 'HEAD' ? data : undefined
}
// 执行请求拦截器
for (const interceptor of this.interceptors.request) {
config = await interceptor(config)
}
// 构建完整的URL处理GET参数
let fullUrl = config.url
if (config.params && Object.keys(config.params).length > 0) {
const queryString = new URLSearchParams(config.params).toString()
fullUrl += (fullUrl.includes('?') ? '&' : '?') + queryString
}
const fetchConfig: RequestInit = {
method: config.method,
headers: {
'Content-Type': 'application/json',
...config.headers
}
}
// 只有非GET/HEAD请求才设置body
if (config.method !== 'GET' && config.method !== 'HEAD' && config.data) {
fetchConfig.body = JSON.stringify(config.data)
}
try {
const response = await fetch(fullUrl, fetchConfig)
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`)
}
const result = await response.json()
let finalResult = { ...result, status: response.status }
for (const interceptor of this.interceptors.response) {
finalResult = await interceptor(finalResult)
}
return finalResult
} catch (error) {
// 错误处理
console.error('请求失败:', error)
throw error
}
}
get(url: string, params?: any) {
return this.request('GET', url, params)
}
post(url: string, data?: any) {
return this.request('POST', url, data)
}
put(url: string, data?: any) {
return this.request('PUT', url, data)
}
delete(url: string, data?: any) {
return this.request('DELETE', url, data)
}
}
// 创建实例
const request = new Request('http://192.168.123.111:8083/api/')
// 添加全局请求拦截器
request.useRequestInterceptor(async (config) => {
// 添加认证token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
if (config.url.includes('/enrollment/') && config.params?.idCard) {
const idCardRegex = /(^\d{18}$)|(^\d{17}(\d|X|x)$)/
if (!idCardRegex.test(config.params.idCard)) {
throw new Error('身份证号格式不正确')
}
}
return config
})
// 添加全局响应拦截器
request.useResponseInterceptor(async (response) => {
// 根据你的后端返回结构调整
if (response.code && response.code !== 200 && response.code !== 0 && response.code !== 1) {
// 处理业务错误
switch (response.code) {
case 401:
window.location.href = '/login'
break
case 403:
throw new Error('权限不足,无法访问')
default:
throw new Error(response.message || '请求失败')
}
}
return response
})
// 添加错误处理拦截器
request.useResponseInterceptor(async (response) => {
return response
})
export default request

1
stu/src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,223 @@
<template>
<!-- 报名表单区域 -->
<div class="border-b border-br pb-5">
<span class="text-2xl font-medium">新生报名</span>
</div>
<el-card class="!border-none mb-4" shadow="never">
<div class="lg:flex gap-6">
<!-- 基本信息表单 -->
<el-card class="!border-none flex-1" shadow="never">
<el-form
layout="vertical"
:model="studentData.baseInfo"
class="mt-4"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item label="学生姓名">
<el-input
v-model="studentData.baseInfo.name"
placeholder="请输入姓名"
/>
</el-form-item>
<el-form-item label="性别">
<el-radio-group
v-model="studentData.baseInfo.gender"
>
<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位身份证号"
/>
</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="请输入出生日期"
/>
</el-form-item>
<el-form-item label="联系方式">
<el-input
v-model="studentData.baseInfo.phone"
placeholder="请输入手机号码"
/>
</el-form-item>
<el-form-item label="电子邮箱">
<el-input
v-model="studentData.baseInfo.email"
placeholder="请输入电子邮箱"
/>
</el-form-item>
<el-form-item label="籍贯">
<el-input
v-model="studentData.baseInfo.nativePlace"
placeholder="请输入籍贯"
/>
</el-form-item>
<el-form-item label="民族">
<el-input
v-model="studentData.baseInfo.nationality"
placeholder="请输入民族"
/>
</el-form-item>
<el-form-item label="政治面貌">
<el-select
v-model="studentData.baseInfo.politicalStatus"
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="毕业年份">
<el-input-number
v-model="studentData.baseInfo.graduationYear"
placeholder="请输入毕业年份"
/>
</el-form-item>
<el-form-item label="毕业院校">
<el-input
v-model="studentData.baseInfo.previousSchool"
placeholder="请输入毕业院校"
/>
</el-form-item>
<el-form-item label="学校类型">
<el-select
v-model="studentData.baseInfo.schoolType"
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="家庭住址"
class="md:col-span-2"
>
<el-input
v-model="studentData.baseInfo.homeAddress"
placeholder="请输入详细家庭住址"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item label="紧急联系人">
<el-input
v-model="studentData.baseInfo.emergencyContact"
placeholder="请输入紧急联系人姓名"
/>
</el-form-item>
<el-form-item label="紧急联系电话">
<el-input
v-model="studentData.baseInfo.emergencyPhone"
placeholder="请输入紧急联系人电话"
/>
</el-form-item>
<el-form-item label="与紧急联系人关系">
<el-input
v-model="studentData.baseInfo.relationship"
placeholder="请输入与紧急联系人的关系"
/>
</el-form-item>
</div>
</el-form>
</el-card>
</div>
</el-card>
<!-- 提交按钮 -->
<el-card class="!border-none mb-4 flex-1" shadow="never">
<div class="mt-6">
<el-button
class="ml-4"
type="primary"
block
@click="handleConfirmSubmit"
>
提交报名
</el-button>
</div>
</el-card>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
import { ElButton, ElMessage, ElForm, ElFormItem, ElInput } from 'element-plus'
import {
submitEnrollmentInfo
} from '@/api/enrollment'
import { useRouter } from 'vue-router'
const router = useRouter()
//
const studentData = reactive({
baseInfo: {
name: '',
gender: 0,
idCard: '',
birthday: '',
nationality: '',
phone: '',
email: '',
homeAddress: '',
nativePlace: '',
invitationCode: '',
politicalStatus: 0,
previousSchool: '',
schoolType: '',
graduationYear: 2025,
emergencyContact: '',
emergencyPhone: '',
relationship: ''
}
})
//
const handleConfirmSubmit = async () => {
try {
//
if (!/(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(studentData.baseInfo.idCard)) {
ElMessage.warning('请输入有效的18位身份证号')
return
}
const data = await submitEnrollmentInfo(studentData.baseInfo)
console.log(data)
if(data.code === 1){
ElMessage.success('报名提交成功')
router.push({
path: '/query',
query: { idCard: studentData.baseInfo.idCard }
})
}
else ElMessage.error(data.msg)
} catch (error) {
ElMessage.error('报名提交失败,请稍后重试')
console.error('提交报名信息失败:', error)
}
}
</script>
<style lang="scss" scoped>
.el-dialog__body {
padding: 20px;
}
.el-form-item {
margin-bottom: 20px;
}
.el-tabs {
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,383 @@
<template>
<div class="px-[30px] py-5 user-info min-h-full flex flex-col">
<!-- 标签页切换 -->
<el-form>
<!-- 查询进度区域 -->
<div class="border-b border-br pb-5">
<span class="text-2xl font-medium">查询报名进度</span>
</div>
<!-- 查询表单 -->
<el-card class="!border-none mb-6" shadow="never">
<el-form
:model="queryForm"
:rules="queryRules"
ref="queryFormRef"
label-width="100px"
class="mt-4"
>
<el-form-item label="身份证号" prop="idCard">
<el-input
v-model="queryForm.idCard"
placeholder="请输入18位身份证号"
maxlength="18"
@input="handleIdCardInput"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuerySubmit" :loading="isQueryLoading">
查询
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 进度展示区域 -->
<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"
style="text-align: left"
>
<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>
</el-form>
<!-- 查询进度弹窗保持全局可访问 -->
<el-dialog
v-model="isQueryDialogOpen"
title="查询报名进度"
width="500px"
:close-on-click-modal="false"
>
<el-form
:model="queryForm"
:rules="queryRules"
ref="queryFormRef"
label-width="100px"
class="mt-4"
>
<el-form-item label="身份证号" prop="idCard">
<el-input
v-model="queryForm.idCard"
placeholder="请输入18位身份证号"
maxlength="18"
@input="handleIdCardInput"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="isQueryDialogOpen = false">取消</el-button>
<el-button type="primary" @click="handleQuerySubmit" :loading="isQueryLoading">
查询
</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { reactive, computed, onMounted, ref, unref } from 'vue'
import { ElButton, ElMessage, ElForm, ElFormItem, ElInput, ElDialog, ElTabs, ElTabPane } from 'element-plus'
import {
getEnrollmentProcessStatus
} from '@/api/enrollment'
import { useRoute } from 'vue-router'
const activeTab = ref('enroll') //
const route = useRoute()
//
const stepTitles = ['报名', '缴费', '等待录取']
const submission = ref(false) //
const rejectionReason = ref('') //
//
const isQueryDialogOpen = ref(false) //
const isQueryLoading = ref(false) //
const queryFormRef = ref<InstanceType<typeof ElForm> | null>(null) //
//
const queryForm = reactive({
idCard: ''
})
//
const queryRules = reactive({
idCard: [
{ required: true, message: '请输入身份证号', trigger: 'blur' },
{
pattern: /(^\d{18}$)|(^\d{17}(\d|X|x)$)/,
message: '请输入有效的18位身份证号',
trigger: 'blur'
}
]
})
//
const studentData = reactive({
baseInfo: {
name: '',
gender: 0,
idCard: '',
birthday: '',
nationality: '',
phone: '',
email: '',
homeAddress: '',
nativePlace: '',
invitationCode: '',
politicalStatus: 0,
previousSchool: '',
schoolType: '',
graduationYear: 2025,
emergencyContact: '',
emergencyPhone: '',
relationship: ''
}
})
//
const statusConfig = reactive({
steps: [
{ completed: false, time: 0, status: 0 }, //
{ completed: false, time: 0, status: 0 }, //
{ completed: false, time: 0, status: 0 }, //
],
applicationNumber: '' //
})
//
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}`
} else if (index === 2) {
if (step.status === 0) return '等待录取通知'
if (step.status === 1) return `录取通知已发送 ${timeStr}`
if (step.status === 2) return `未录取 ${timeStr}`
}
return ''
}
//
const handleIdCardInput = () => {
let idCardStr = String(queryForm.idCard || '')
idCardStr = idCardStr.replace(/[^0-9Xx]/g, '')
idCardStr = idCardStr.toUpperCase()
if (idCardStr.length > 18) {
idCardStr = idCardStr.slice(0, 18)
}
queryForm.idCard = idCardStr
}
//
const handleQuerySubmit = async () => {
const formRef = unref(queryFormRef)
if (!formRef) return
try {
await formRef.validate()
isQueryLoading.value = true
const data = await getEnrollmentProcessStatus(queryForm)
const statusRes = data.data
if (statusRes) {
isQueryDialogOpen.value = false
submission.value = true
statusConfig.applicationNumber = statusRes.applicationNumber || ''
rejectionReason.value = statusRes.rejectionReason || ''
statusConfig.steps[0] = {
completed: statusRes.applicationNumber ? true : false,
time: statusRes.applicationTime || 0,
status: statusRes.applicationTime > 0 ? 1 : 0
}
statusConfig.steps[1] = {
completed: statusRes.approvalStatus === 1,
time: statusRes.approvalTime || 0,
status: statusRes.approvalStatus || 0
}
statusConfig.steps[2] = {
completed: statusRes.admissionStatus === 6,
time: statusRes.admissionTime || 0,
status: statusRes.admissionStatus || 0
}
ElMessage.success('查询成功')
} else {
ElMessage.info('未查询到相关报名记录')
resetStatusConfig()
}
} catch (error) {
console.error('查询进度失败:', error)
ElMessage.error('查询失败,请稍后重试')
} finally {
isQueryLoading.value = false
}
}
//
const loadProcessStatus = async (idCard?: string) => {
const cardNo = idCard || studentData.baseInfo.idCard
if (!cardNo || typeof cardNo !== 'string') return
try {
const statusRes = await getEnrollmentProcessStatus(cardNo)
if (statusRes) {
submission.value = true
statusConfig.applicationNumber = statusRes.data.applicationNumber || ''
rejectionReason.value = statusRes.data.rejectionReason || ''
statusConfig.steps[0] = {
completed: statusRes.data.applicationNumber ? true : false,
time: statusRes.data.applicationTime || 0,
status: statusRes.data.applicationTime > 0 ? 1 : 0
}
statusConfig.steps[1] = {
completed: statusRes.data.approvalStatus === 1,
time: statusRes.data.approvalTime || 0,
status: statusRes.data.approvalStatus || 0
}
statusConfig.steps[2] = {
completed: statusRes.data.admissionStatus === 1,
time: statusRes.data.admissionTime || 0,
status: statusRes.data.admissionStatus || 0
}
}
} catch (error) {
console.error('刷新状态失败:', error)
}
}
//
const resetStatusConfig = () => {
statusConfig.steps = [
{ completed: false, time: 0, status: 0 },
{ completed: false, time: 0, status: 0 },
{ completed: false, time: 0, status: 0 }
]
statusConfig.applicationNumber = ''
rejectionReason.value = ''
submission.value = false
const graduationYear = studentData.baseInfo.graduationYear
Object.assign(studentData.baseInfo, {
name: '',
gender: 0,
idCard: '',
birthday: '',
nationality: '',
phone: '',
email: '',
homeAddress: '',
nativePlace: '',
politicalStatus: 0,
previousSchool: '',
schoolType: '',
graduationYear,
emergencyContact: '',
emergencyPhone: '',
relationship: ''
})
}
//
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
})
//
onMounted(() => {
//
const idCardFromRoute = route.query.idCard as string
if (idCardFromRoute) {
queryForm.idCard = idCardFromRoute
//
setTimeout(() => {
handleQuerySubmit()
}, 100)
} else {
const idCard = studentData.baseInfo.idCard
if (idCard && typeof idCard === 'string') {
loadProcessStatus(idCard)
}
}
})
</script>
<style lang="scss" scoped>
.el-dialog__body {
padding: 20px;
}
.el-form-item {
margin-bottom: 20px;
}
.el-tabs {
margin-bottom: 20px;
}
</style>

33
stu/src/enums/appEnums.ts Normal file
View File

@ -0,0 +1,33 @@
//菜单主题类型
export enum ThemeEnum {
LIGHT = 'light',
DARK = 'dark'
}
// 菜单类型
export enum MenuEnum {
CATALOGUE = 'M',
MENU = 'C',
BUTTON = 'A'
}
// 屏幕
export enum ScreenEnum {
SM = 640,
MD = 768,
LG = 1024,
XL = 1280,
'2XL' = 1536
}
export enum SMSEnum {
LOGIN = 'YZMDL',
BIND_MOBILE = 'BDSJHM',
CHANGE_MOBILE = 'BGSJHM',
FIND_PASSWORD = 'ZHDLMM'
}
export enum PolicyAgreementEnum {
SERVICE = 'service',
PRIVACY = 'privacy'
}

View File

@ -0,0 +1,8 @@
// 本地缓冲key
//token
export const TOKEN_KEY = 'token'
//账号
export const ACCOUNT_KEY = 'account'
//设置
export const SETTING_KEY = 'setting'

View File

@ -0,0 +1,7 @@
export enum PageEnum {
//登录页面
LOGIN = '/login',
//无权限页面
ERROR_403 = '/403',
INDEX = '/'
}

View File

@ -0,0 +1,19 @@
export enum ContentTypeEnum {
// json
JSON = 'application/json;charset=UTF-8',
// form-data 上传资源(图片,视频)
FORM_DATA = 'multipart/form-data'
}
export enum RequestMethodsEnum {
GET = 'GET',
POST = 'POST'
}
export enum RequestCodeEnum {
NOT_INSTALL = -2,
LOGIN_FAILURE = -1,
FAIL = 0,
SUCCESS = 1,
OPEN_NEW_PAGE = 2
}

11
stu/src/main.ts Normal file
View File

@ -0,0 +1,11 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router'
createApp(App)
.use(ElementPlus)
.use(router)
.mount('#app')

47
stu/src/router/index.ts Normal file
View File

@ -0,0 +1,47 @@
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// 导入组件(支持懒加载)
import apply from '@/components/apply.vue'
import query from '@/components/query.vue'
// 1. 定义路由数组:用 RouteRecordRaw 约束类型
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/apply'
},
{
path: '/apply',
name: 'apply', // 路由名称(可选,用于编程式导航)
component: apply,
meta: {
title: '申请', // 自定义元信息(可扩展类型)
requiresAuth: false // 是否需要登录
}
},
{
path: '/query',
name: 'query',
component: query,
meta: {
title: '查询',
requiresAuth: false
}
}
]
// 2. 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), // HTML5 模式(无 #
routes // 传入路由数组
})
// 3. 路由守卫(可选,示例:修改页面标题)
router.beforeEach((to, from, next) => {
if (to.meta.title) {
document.title = to.meta.title as string // TS 类型断言(因 meta 已扩展,可优化)
}
next()
})
export default router

83
stu/src/style.css Normal file
View File

@ -0,0 +1,83 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

29
stu/src/utils/env.ts Normal file
View File

@ -0,0 +1,29 @@
/**
* @description
*/
export function getClient() {
return useRuntimeConfig().public.client
}
/**
* @description
*/
export function getVersion() {
return useRuntimeConfig().public.version
}
/**
* @description
*/
export function getApiUrl() {
return (
useRuntimeConfig().public.apiUrl || 'http://192.168.123.111:8084/api/'
)
}
/**
* @description
*/
export function getApiPrefix() {
return useRuntimeConfig().public.apiPrefix
}

95
stu/src/utils/feedback.ts Normal file
View File

@ -0,0 +1,95 @@
import {
ElMessage,
ElMessageBox,
ElNotification,
ElLoading,
type ElMessageBoxOptions
} from 'element-plus'
import type { LoadingInstance } from 'element-plus/es/components/loading/src/loading'
export class Feedback {
private loadingInstance: LoadingInstance | null = null
static instance: Feedback | null = null
static getInstance() {
return this.instance ?? (this.instance = new Feedback())
}
// 消息提示
msg(msg: string) {
ElMessage.info(msg)
}
// 错误消息
msgError(msg: string) {
ElMessage.error(msg)
}
// 成功消息
msgSuccess(msg: string) {
ElMessage.success(msg)
}
// 警告消息
msgWarning(msg: string) {
ElMessage.warning(msg)
}
// 弹出提示
alert(msg: string) {
ElMessageBox.alert(msg, '系统提示')
}
// 错误提示
alertError(msg: string) {
ElMessageBox.alert(msg, '系统提示', { type: 'error' })
}
// 成功提示
alertSuccess(msg: string) {
ElMessageBox.alert(msg, '系统提示', { type: 'success' })
}
// 警告提示
alertWarning(msg: string) {
ElMessageBox.alert(msg, '系统提示', { type: 'warning' })
}
// 通知提示
notify(msg: string) {
ElNotification.info(msg)
}
// 错误通知
notifyError(msg: string) {
ElNotification.error(msg)
}
// 成功通知
notifySuccess(msg: string) {
ElNotification.success(msg)
}
// 警告通知
notifyWarning(msg: string) {
ElNotification.warning(msg)
}
// 确认窗体
confirm(msg: string) {
return ElMessageBox.confirm(msg, '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
}
// 提交内容
prompt(content: string, title: string, options?: ElMessageBoxOptions) {
return ElMessageBox.prompt(content, title, {
confirmButtonText: '确定',
cancelButtonText: '取消',
...options
})
}
// 打开全局loading
loading(msg: string) {
this.loadingInstance = ElLoading.service({
lock: true,
text: msg
})
}
// 关闭全局loading
closeLoading() {
this.loadingInstance?.close()
}
}
const feedback = Feedback.getInstance()
export default feedback

View File

@ -0,0 +1,93 @@
import { FetchOptions } from 'ofetch'
import { RequestCodeEnum, RequestMethodsEnum } from '@/enums/requestEnums'
import feedback from '@/utils/feedback'
import { merge } from 'lodash-es'
import { Request } from './request'
import { getApiPrefix, getApiUrl, getVersion } from '../env'
import { useUserStore } from '@/stores/user'
export function createRequest(opt?: Partial<FetchOptions>) {
const userStore = useUserStore()
// const { setPopupType, toggleShowPopup } = useAccount()
const defaultOptions: FetchOptions = {
// 基础接口地址
baseURL: getApiUrl(),
//请求头
headers: {
version: getVersion()
},
retry: 2,
requestOptions: {
apiPrefix: getApiPrefix(),
isTransformResponse: true,
isReturnDefaultResponse: false,
withToken: true,
isParamsToData: true,
requestInterceptorsHook(options) {
const { apiPrefix, isParamsToData, withToken } =
options.requestOptions
// 拼接请求前缀
if (apiPrefix) {
options.url = `${apiPrefix}${options.url}`
}
const params = options.params || {}
// POST请求下如果无data则将params视为data
if (
isParamsToData &&
!Reflect.has(options, 'body') &&
options.method?.toUpperCase() === RequestMethodsEnum.POST
) {
options.body = params
options.params = {}
}
const headers = options.headers || {}
if (withToken) {
const token = userStore.token
headers['token'] = token
}
options.headers = headers
return options
},
async responseInterceptorsHook(response, options) {
const { isTransformResponse, isReturnDefaultResponse } =
options.requestOptions
//返回默认响应,当需要获取响应头及其他数据时可使用
if (isReturnDefaultResponse) {
return response
}
// 是否需要对数据进行处理
if (!isTransformResponse) {
return response._data
}
const { code, data, show, msg } = response._data
switch (code) {
case RequestCodeEnum.SUCCESS:
if (show) {
msg && feedback.msgSuccess(msg)
}
return data
case RequestCodeEnum.FAIL:
if (show) {
msg && feedback.msgError(msg)
}
return Promise.reject(msg)
case RequestCodeEnum.LOGIN_FAILURE:
userStore.logout()
return Promise.reject(data)
case RequestCodeEnum.NOT_INSTALL:
window.location.replace('/install/install.php')
break
default:
return data
}
},
responseInterceptorsCatchHook(err) {
return err
}
}
}
return new Request(
// 深度合并
merge(defaultOptions, opt || {})
)
}

View File

@ -0,0 +1,126 @@
import {
FetchOptions,
$fetch,
$Fetch,
FetchResponse,
RequestOptions,
FileParams,
RequestEventStreamOptions
} from 'ofetch'
import { merge } from 'lodash-es'
import { isFunction } from '../validate'
import { RequestMethodsEnum } from '@/enums/requestEnums'
import { objectToQuery } from '../util'
export class Request {
private requestOptions: RequestOptions
private fetchInstance: $Fetch
constructor(private fetchOptions: FetchOptions) {
this.fetchInstance = $fetch.create(fetchOptions)
this.requestOptions = fetchOptions.requestOptions
}
getInstance() {
return this.fetchInstance
}
/**
* @description get请求
*/
get(fetchOptions: FetchOptions, requestOptions?: Partial<RequestOptions>) {
return this.request(
{ ...fetchOptions, method: RequestMethodsEnum.GET },
requestOptions
)
}
/**
* @description post请求
*/
post(fetchOptions: FetchOptions, requestOptions?: Partial<RequestOptions>) {
return this.request(
{ ...fetchOptions, method: RequestMethodsEnum.POST },
requestOptions
)
}
/**
* @description:
*/
uploadFile(options: FetchOptions, params: FileParams) {
const formData = new FormData()
const customFilename = params.name || 'file'
formData.append(customFilename, params.file)
if (params.data) {
Object.keys(params.data).forEach((key) => {
const value = params.data![key]
if (Array.isArray(value)) {
value.forEach((item) => {
formData.append(`${key}[]`, item)
})
return
}
formData.append(key, params.data![key])
})
}
return this.request({
...options,
method: RequestMethodsEnum.POST,
body: formData
})
}
/**
* @description
*/
request(
fetchOptions: FetchOptions,
requestOptions?: Partial<RequestOptions>
): Promise<any> {
let mergeOptions = merge({}, this.fetchOptions, fetchOptions)
mergeOptions.requestOptions = merge(
{},
this.requestOptions,
requestOptions
)
const {
requestInterceptorsHook,
responseInterceptorsHook,
responseInterceptorsCatchHook
} = this.requestOptions
if (requestInterceptorsHook && isFunction(requestInterceptorsHook)) {
mergeOptions = requestInterceptorsHook(mergeOptions)
}
return new Promise((resolve, reject) => {
return this.fetchInstance
.raw(mergeOptions.url, mergeOptions)
.then(async (response: FetchResponse<any>) => {
if (
responseInterceptorsHook &&
isFunction(responseInterceptorsHook)
) {
try {
response = await responseInterceptorsHook(
response,
mergeOptions
)
resolve(response)
} catch (error) {
reject(error)
}
return
}
resolve(response)
})
.catch((err) => {
if (
responseInterceptorsCatchHook &&
isFunction(responseInterceptorsCatchHook)
) {
reject(responseInterceptorsCatchHook(err))
return
}
reject(err)
})
})
}
}

63
stu/src/utils/util.ts Normal file
View File

@ -0,0 +1,63 @@
/**
* @description
* @param {String | Number} value 100
* @param {String} unit px em rem
*/
export const addUnit = (value: string | number, unit = 'px') => {
return !Object.is(Number(value), NaN) ? `${value}${unit}` : value
}
/**
* @description 广
* @param {Array} data
* @param {Object} props `{ children: 'children' }`
*/
export const treeToArray = (data: any[], props = { children: 'children' }) => {
data = JSON.parse(JSON.stringify(data))
const { children } = props
const newData = []
const queue: any[] = []
data.forEach((child: any) => queue.push(child))
while (queue.length) {
const item: any = queue.shift()
if (item[children]) {
item[children].forEach((child: any) => queue.push(child))
delete item[children]
}
newData.push(item)
}
return newData
}
/**
* @description
* @param {String} path
*/
export function getNormalPath(path: string) {
if (path.length === 0 || !path || path == 'undefined') {
return path
}
const newPath = path.replace('//', '/')
const length = newPath.length
if (newPath[length - 1] === '/') {
return newPath.slice(0, length - 1)
}
return newPath
}
/**
* @description对象格式化为Query语法
* @param { Object } params
* @return {string} Query语法
*/
export function objectToQuery(params: Record<string, any>): string {
let query = ''
for (const props of Object.keys(params)) {
const value = params[props]
if (!isEmpty(value)) {
query += props + '=' + value + '&'
}
}
return query.slice(0, -1)
}

50
stu/src/utils/validate.ts Normal file
View File

@ -0,0 +1,50 @@
export {
isArray,
isBoolean,
isDate,
isObject,
isFunction,
isString,
isNumber,
isNull
} from 'lodash-es'
import { isObject } from 'lodash-es'
/**
* @description http
*/
export function isExternal(path: string) {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
* @description http
*/
export const isLinkHttp = (link: string): boolean =>
/^(https?:)?\/\//.test(link)
/**
* @description
*/
export const isLinkTel = (link: string): boolean => /^tel:/.test(link)
/**
* @description
*/
export const isLinkMailto = (link: string): boolean => /^mailto:/.test(link)
/**
* @description
* @param {unknown} value
* @return {Boolean}
*/
export const isEmpty = (value: unknown) => {
return value !== null && value !== '' && typeof value !== 'undefined'
}
/**
* @description
* @param {Object} value
* @return {Boolean}
*/
export const isEmptyObject = (target: object) => {
return isObject(target) && !Object.keys(target).length
}

71
stu/tailwind.config.js Normal file
View File

@ -0,0 +1,71 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./index.html',
'./src/**/*.{vue,js}'
],
theme: {
colors: {
white: 'var(--color-white)',
black: 'var(--el-color-black)',
primary: {
DEFAULT: 'var(--el-color-primary)',
'light-3': 'var(--el-color-primary-light-3)',
'light-5': 'var(--el-color-primary-light-5)',
'light-7': 'var(--el-color-primary-light-7)',
'light-8': 'var(--el-color-primary-light-8)',
'light-9': 'var(--el-color-primary-light-9)',
'dark-2': 'var(--el-color-primary-dark-2)'
},
success: 'var(--el-color-success)',
warning: 'var(--el-color-warning)',
danger: 'var(--el-color-danger)',
error: 'var(--el-color-error)',
info: 'var(--el-color-info)',
body: 'var(--el-bg-color)',
page: 'var(--el-bg-color-page)',
'tx-primary': 'var(--el-text-color-primary)',
'tx-regular': 'var(--el-text-color-regular)',
'tx-secondary': 'var(--el-text-color-secondary)',
'tx-placeholder': 'var(--el-text-color-placeholder)',
'tx-disabled': 'var(--el-text-color-disabled)',
br: 'var(--el-border-color)',
'br-light': 'var(--el-border-color-light)',
'br-extra-light': 'var(--el-border-color-extra-light)',
'br-dark': 'var( --el-border-color-dark)',
fill: 'var(--el-fill-color)',
mask: 'var(--el-mask-color)',
overlay: 'var(--el-overlay-color-light)'
},
fontFamily: {
sans: [
'PingFang SC',
'Arial',
'Hiragino Sans GB',
'Microsoft YaHei',
'sans-serif'
]
},
boxShadow: {
DEFAULT: 'var(--el-box-shadow)',
light: 'var(--el-box-shadow-light)',
lighter: 'var(--el-box-shadow-lighter)',
dark: 'var(--el-box-shadow-dark)'
},
fontSize: {
xs: 'var(--el-font-size-extra-small)',
sm: 'var( --el-font-size-small)',
base: 'var( --el-font-size-base)',
lg: 'var( --el-font-size-medium)',
xl: 'var( --el-font-size-large)',
'2xl': 'var( --el-font-size-extra-large)',
'3xl': '20px',
'4xl': '24px',
'5xl': '28px',
'6xl': '30px',
'7xl': '36px',
'8xl': '48px',
'9xl': '60px'
}
}
}

20
stu/tsconfig.app.json Normal file
View File

@ -0,0 +1,20 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"strict": true,
"composite":true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"verbatimModuleSyntax": false,
"baseUrl": ".", //
"paths": { "@/*": ["src/*"] } // Vite
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

14
stu/tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

26
stu/tsconfig.node.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

17
stu/vite.config.ts Normal file
View File

@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 5274,
host: '0.0.0.0',
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})