补充新生报名页面stu
This commit is contained in:
parent
2fe6a590e8
commit
d85ebeb7f0
|
|
@ -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?
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="vite/client" />
|
||||
import { Request } from './src/utils/http/request'
|
||||
declare global {
|
||||
const $request: Request
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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'
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// 本地缓冲key
|
||||
|
||||
//token
|
||||
export const TOKEN_KEY = 'token'
|
||||
//账号
|
||||
export const ACCOUNT_KEY = 'account'
|
||||
//设置
|
||||
export const SETTING_KEY = 'setting'
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export enum PageEnum {
|
||||
//登录页面
|
||||
LOGIN = '/login',
|
||||
//无权限页面
|
||||
ERROR_403 = '/403',
|
||||
INDEX = '/'
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 || {})
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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'),
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue