From 97a4d51b85adb4539d8635fb839b4e1bdb026acd Mon Sep 17 00:00:00 2001 From: zhoulexin Date: Fri, 15 May 2026 15:24:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E9=A6=96=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 28 ++ index.html | 13 + package.json | 22 + src/App.vue | 6 + src/api/login.js | 23 ++ src/api/registration.js | 47 +++ src/assets/styles/main.css | 522 ++++++++++++++++++++++++ src/main.js | 18 + src/router/index.js | 58 +++ src/utils/request.js | 67 ++++ src/views/admin/DashboardPage.vue | 644 ++++++++++++++++++++++++++++++ src/views/admin/LoginPage.vue | 177 ++++++++ src/views/client/FormPage.vue | 432 ++++++++++++++++++++ src/views/client/PaymentPage.vue | 445 +++++++++++++++++++++ vite.config.js | 10 + 15 files changed, 2512 insertions(+) create mode 100644 .gitignore create mode 100644 index.html create mode 100644 package.json create mode 100644 src/App.vue create mode 100644 src/api/login.js create mode 100644 src/api/registration.js create mode 100644 src/assets/styles/main.css create mode 100644 src/main.js create mode 100644 src/router/index.js create mode 100644 src/utils/request.js create mode 100644 src/views/admin/DashboardPage.vue create mode 100644 src/views/admin/LoginPage.vue create mode 100644 src/views/client/FormPage.vue create mode 100644 src/views/client/PaymentPage.vue create mode 100644 vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7fd28d --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.logRecord +npm-debug.logRecord* +yarn-debug.logRecord* +yarn-error.logRecord* +pnpm-debug.logRecord* +lerna-debug.logRecord* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +src/plugins/**/node_modules/ +!src/plugins/**/dist +package-lock.json \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..fc22a01 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + 培训报名系统 + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..b9a26dc --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "registration-system", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.15.2", + "element-plus": "^2.6.1", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.2.0" + } +} diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..d372e96 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,6 @@ + + + diff --git a/src/api/login.js b/src/api/login.js new file mode 100644 index 0000000..4c30016 --- /dev/null +++ b/src/api/login.js @@ -0,0 +1,23 @@ +import request from '../utils/request' + +export function login(data) { + return request({ + url: '/api/user/login', + method: 'post', + data + }) +} + +export function logout() { + return request({ + url: '/api/user/logout', + method: 'post' + }) +} + +export function getUserInfo() { + return request({ + url: '/api/user/info', + method: 'get' + }) +} diff --git a/src/api/registration.js b/src/api/registration.js new file mode 100644 index 0000000..99197aa --- /dev/null +++ b/src/api/registration.js @@ -0,0 +1,47 @@ +import request from '../utils/request' + +export function getRegistrationList(params) { + return request({ + url: '/api/registration/list', + method: 'get', + params + }) +} + +export function getRegistrationStatistics() { + return request({ + url: '/api/registration/statistics', + method: 'get' + }) +} + +export function submitRegistration(data) { + return request({ + url: '/api/registration/submit', + method: 'post', + data + }) +} + +export function updateRegistrationStatus(randomCode, status) { + return request({ + url: `/api/registration/update/${randomCode}`, + method: 'put', + data: { status } + }) +} + +export function getRegistrationDetail(id) { + return request({ + url: `/api/registration/detail/${id}`, + method: 'get' + }) +} + +export function sendSmsCode(phone) { + return request({ + url: '/api/sms/send', + method: 'post', + data: { phone } + }) +} diff --git a/src/assets/styles/main.css b/src/assets/styles/main.css new file mode 100644 index 0000000..7f7095c --- /dev/null +++ b/src/assets/styles/main.css @@ -0,0 +1,522 @@ +/* 全局样式 - 扁平化设计 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #409EFF; + --success-color: #67C23A; + --warning-color: #E6A23C; + --danger-color: #F56C6C; + --info-color: #909399; + --text-primary: #303133; + --text-regular: #606266; + --text-secondary: #909399; + --border-color: #DCDFE6; + --bg-color: #F5F7FA; + --card-bg: #FFFFFF; +} + +html, body { + font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, 'Microsoft YaHei', Arial, sans-serif; + font-size: 14px; + color: var(--text-primary); + background: var(--bg-color); + min-height: 100vh; +} + +#app { + min-height: 100vh; +} + +/* 容器样式 */ +.page-container { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; +} + +.page-wrapper { + max-width: 1200px; + margin: 0 auto; +} + +/* 卡片样式 */ +.custom-card { + background: var(--card-bg); + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + padding: 40px; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.custom-card:hover { + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); +} + +/* 表单样式 */ +.form-section { + margin-bottom: 24px; +} + +.form-section-title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 16px; + padding-left: 12px; + border-left: 4px solid var(--primary-color); +} + +.form-row { + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +.form-item { + flex: 1; + min-width: 250px; +} + +/* 按钮样式 */ +.btn-primary { + background: linear-gradient(135deg, var(--primary-color) 0%, #66b1ff 100%); + border: none; + border-radius: 8px; + padding: 12px 32px; + font-size: 16px; + font-weight: 500; + transition: all 0.3s ease; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(64, 158, 255, 0.4); +} + +.btn-block { + width: 100%; + display: block; +} + +/* 页面标题 */ +.page-header { + text-align: center; + margin-bottom: 40px; +} + +.page-title { + font-size: 28px; + font-weight: 600; + margin-bottom: 12px; +} + +.page-subtitle { + font-size: 14px; +} + +/* 头部导航 */ +.header-bar { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + padding: 16px 24px; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 12px; + margin-bottom: 24px; +} + +.header-logo { + display: flex; + align-items: center; + gap: 12px; + font-size: 18px; + font-weight: 600; + color: var(--primary-color); +} + +.header-actions { + display: flex; + gap: 12px; +} + +/* 成功页面 */ +.success-icon { + width: 80px; + height: 80px; + background: linear-gradient(135deg, var(--success-color) 0%, #85ce61 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 24px; + animation: scaleIn 0.5s ease; +} + +.success-icon .el-icon { + font-size: 40px; + color: #fff; +} + +@keyframes scaleIn { + 0% { transform: scale(0); opacity: 0; } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); opacity: 1; } +} + +/* 信息展示 */ +.info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin: 24px 0; +} + +.info-item { + background: var(--bg-color); + padding: 16px; + border-radius: 8px; +} + +.info-label { + font-size: 12px; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.info-value { + font-size: 16px; + font-weight: 500; + color: var(--text-primary); +} + +/* 二维码区域 */ +.qrcode-section { + text-align: center; + padding: 24px; + background: var(--bg-color); + border-radius: 12px; + margin-top: 24px; +} + +.qrcode-title { + font-size: 16px; + font-weight: 500; + margin-bottom: 16px; +} + +.qrcode-grid { + display: flex; + justify-content: center; + gap: 40px; + flex-wrap: wrap; +} + +.qrcode-item { + text-align: center; +} + +.qrcode-box { + background: #fff; + padding: 16px; + border-radius: 8px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); +} + +.qrcode-label { + margin-top: 12px; + font-size: 14px; + color: var(--text-secondary); +} + +/* 费用展示 */ +.price-tag { + display: inline-flex; + align-items: center; + gap: 8px; + background: linear-gradient(135deg, #fff5f5 0%, #fef0f0 100%); + padding: 12px 24px; + border-radius: 8px; + border: 1px dashed var(--danger-color); +} + +.price-amount { + font-size: 28px; + font-weight: 700; + color: var(--danger-color); +} + +.price-unit { + font-size: 14px; + color: var(--text-secondary); +} + +/* 登录页样式 */ +.login-container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; +} + +.login-card { + width: 100%; + max-width: 420px; + padding: 48px 40px; +} + +.login-title { + text-align: center; + font-size: 24px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 32px; +} + +/* 管理端样式 */ +.admin-container { + min-height: 100vh; + background: var(--bg-color); +} + +.admin-header { + background: #fff; + padding: 16px 32px; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + position: sticky; + top: 0; + z-index: 100; +} + +.admin-logo { + font-size: 20px; + font-weight: 600; + color: var(--primary-color); + display: flex; + align-items: center; + gap: 8px; +} + +.admin-main { + padding: 24px 32px; + max-width: 1400px; + margin: 0 auto; +} + +.admin-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 24px; +} + +.stat-card { + background: #fff; + padding: 24px; + border-radius: 12px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.stat-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); +} + +.stat-icon { + width: 48px; + height: 48px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; +} + +.stat-icon.blue { background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%); } +.stat-icon.green { background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%); } +.stat-icon.orange { background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%); } +.stat-icon.red { background: linear-gradient(135deg, #F56C6C 0%, #f78989 100%); } + +.stat-icon .el-icon { + font-size: 24px; + color: #fff; +} + +.stat-value { + font-size: 28px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 4px; +} + +.stat-label { + font-size: 14px; + color: var(--text-secondary); +} + +.admin-table-card { + background: #fff; + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); +} + +/* 响应式设计 */ +@media screen and (max-width: 768px) { + .page-container { + padding: 12px; + } + .pay-card { + height: calc(100vh - 20px); + } + .custom-card { + padding: 24px 16px; + border-radius: 8px; + } + + .page-title { + font-size: 22px; + } + + .form-row { + flex-direction: column; + gap: 0; + } + + .form-item { + min-width: 100%; + margin-bottom: 16px; + } + + .header-bar { + flex-direction: column; + gap: 16px; + padding: 16px; + } + + .header-actions { + width: 100%; + justify-content: center; + } + + .login-card { + padding: 32px 20px; + } + + .admin-header { + padding: 12px 16px; + } + + .admin-main { + padding: 16px; + } + + .qrcode-grid { + gap: 24px; + } + + .price-tag { + padding: 10px 16px; + } + + .price-amount { + font-size: 24px; + } + + .admin-stats { + grid-template-columns: repeat(2, 1fr); + } + + .stat-card { + padding: 16px; + } + + .stat-value { + font-size: 22px; + } + + .info-grid { + grid-template-columns: 1fr; + } +} + +@media screen and (max-width: 480px) { + .admin-stats { + grid-template-columns: 1fr; + } + + .btn-primary { + padding: 10px 24px; + font-size: 14px; + } +} + +/* 表格样式优化 */ +.el-table { + border-radius: 8px; + overflow: hidden; +} + +.el-table th { + background-color: var(--bg-color) !important; + font-weight: 600; +} + +.el-table td { + border-bottom: 1px solid var(--bg-color) !important; +} + +/* 输入框样式优化 */ +.el-input__wrapper { + border-radius: 8px; + padding: 4px 12px; +} + +.el-textarea__inner { + border-radius: 8px; +} + +.el-select .el-input__wrapper { + border-radius: 8px; +} + +/* 单选框样式 */ +.el-radio-button__inner { + border-radius: 8px !important; +} + +/* 动画 */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.3s ease; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} + +.slide-enter-active, +.slide-leave-active { + transition: all 0.3s ease; +} + +.slide-enter-from { + transform: translateX(-20px); + opacity: 0; +} + +.slide-leave-to { + transform: translateX(20px); + opacity: 0; +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..8897637 --- /dev/null +++ b/src/main.js @@ -0,0 +1,18 @@ +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import './assets/styles/main.css' + +const app = createApp(App) + +// 注册所有图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(router) +app.use(ElementPlus) +app.mount('#app') diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..98e15cd --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,58 @@ +import { createRouter, createWebHistory } from 'vue-router' + +const routes = [ + { + path: '/', + redirect: '/client/form' + }, + { + path: '/client', + children: [ + { + path: 'form', + name: 'ClientForm', + component: () => import('../views/client/FormPage.vue') + }, + { + path: 'payment', + name: 'Payment', + component: () => import('../views/client/PaymentPage.vue') + } + ] + }, + { + path: '/admin', + children: [ + { + path: 'login', + name: 'AdminLogin', + component: () => import('../views/admin/LoginPage.vue') + }, + { + path: 'dashboard', + name: 'Dashboard', + component: () => import('../views/admin/DashboardPage.vue'), + meta: { requiresAuth: true } + } + ] + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// 路由守卫 +router.beforeEach((to, from, next) => { + if (to.meta.requiresAuth) { + const token = localStorage.getItem('adminToken') + if (!token) { + next('/admin/login') + return + } + } + next() +}) + +export default router diff --git a/src/utils/request.js b/src/utils/request.js new file mode 100644 index 0000000..5eefe64 --- /dev/null +++ b/src/utils/request.js @@ -0,0 +1,67 @@ +import axios from 'axios' +import { ElMessage } from 'element-plus' + +const request = axios.create({ + baseURL: 'http://10.23.22.43:8099', + timeout: 15000 +}) + +// 请求拦截器 +request.interceptors.request.use( + (config) => { + const token = localStorage.getItem('adminToken') + if (token) { + config.headers['token'] = `${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +request.interceptors.response.use( + (response) => { + const res = response.data + + if (res.code && res.code !== 200 && res.code !== 0) { + ElMessage.error(res.message || '请求失败') + return Promise.reject(new Error(res.message || '请求失败')) + } + + return res + }, + (error) => { + if (error.response) { + switch (error.response.status) { + case 401: + ElMessage.error('登录已过期,请重新登录') + localStorage.removeItem('adminToken') + localStorage.removeItem('adminUser') + setTimeout(() => { + window.location.href = '/admin/login' + }, 1500) + break + case 403: + ElMessage.error('无权限访问') + break + case 404: + ElMessage.error('请求地址不存在') + break + case 500: + ElMessage.error('服务器错误') + break + default: + ElMessage.error(error.response.data?.message || '网络错误') + } + } else if (error.code === 'ECONNABORTED') { + ElMessage.error('请求超时,请重试') + } else { + ElMessage.error('网络连接失败') + } + return Promise.reject(error) + } +) + +export default request diff --git a/src/views/admin/DashboardPage.vue b/src/views/admin/DashboardPage.vue new file mode 100644 index 0000000..10ea4c6 --- /dev/null +++ b/src/views/admin/DashboardPage.vue @@ -0,0 +1,644 @@ + + + + + diff --git a/src/views/admin/LoginPage.vue b/src/views/admin/LoginPage.vue new file mode 100644 index 0000000..96c1195 --- /dev/null +++ b/src/views/admin/LoginPage.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/src/views/client/FormPage.vue b/src/views/client/FormPage.vue new file mode 100644 index 0000000..32ee0f2 --- /dev/null +++ b/src/views/client/FormPage.vue @@ -0,0 +1,432 @@ + + + + + diff --git a/src/views/client/PaymentPage.vue b/src/views/client/PaymentPage.vue new file mode 100644 index 0000000..18fe558 --- /dev/null +++ b/src/views/client/PaymentPage.vue @@ -0,0 +1,445 @@ + + + + + \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..bbe9e00 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + host: '0.0.0.0', + port: 3000 + } +})