Compare commits
No commits in common. 'zlx' and 'main' have entirely different histories.
@ -1,3 +0,0 @@
|
||||
# 开发环境
|
||||
# VITE_API_BASE_URL=http://10.23.22.43:8099
|
||||
VITE_API_BASE_URL=https://robotclass.ngsk.tech:7001/backend/
|
||||
@ -1,2 +0,0 @@
|
||||
# 生产环境
|
||||
VITE_API_BASE_URL=
|
||||
@ -1,28 +0,0 @@
|
||||
# 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
|
||||
@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<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>培训报名系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
@ -1,23 +0,0 @@
|
||||
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'
|
||||
})
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
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 }
|
||||
})
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 87 KiB |
@ -1,522 +0,0 @@
|
||||
/* 全局样式 - 扁平化设计 */
|
||||
* {
|
||||
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 {
|
||||
min-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;
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
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')
|
||||
@ -1,58 +0,0 @@
|
||||
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
|
||||
@ -1,67 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
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
|
||||
@ -1,650 +0,0 @@
|
||||
<template>
|
||||
<div class="admin-container">
|
||||
<!-- 头部 -->
|
||||
<div class="admin-header">
|
||||
<div class="admin-logo">
|
||||
<el-icon><Lock /></el-icon>
|
||||
<span>培训报名管理系统</span>
|
||||
</div>
|
||||
<div class="admin-user">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="user-info">
|
||||
<el-icon><Avatar /></el-icon>
|
||||
<span>{{ adminUser.username }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<div class="admin-main">
|
||||
<!-- 统计卡片 -->
|
||||
<div class="admin-stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon blue">
|
||||
<el-icon><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">{{ statistics.total }}</div>
|
||||
<div class="stat-label">总报名数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon green">
|
||||
<el-icon><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">{{ statistics.paid }}</div>
|
||||
<div class="stat-label">已支付</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon orange">
|
||||
<el-icon><Clock /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">{{ statistics.submitted }}</div>
|
||||
<div class="stat-label">待支付</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon red">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">¥{{ statistics.amount }}</div>
|
||||
<div class="stat-label">收款金额</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索报名编号、姓名、手机号、所在单位"
|
||||
prefix-icon="Search"
|
||||
clearable
|
||||
style="max-width: 300px;"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<el-select v-model="filterStatus" placeholder="筛选状态" clearable style="width: 150px;">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="已支付" value="paid" />
|
||||
<el-option label="待支付" value="submitted" />
|
||||
</el-select>
|
||||
<el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="refreshData">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="admin-table-card">
|
||||
<div class="table-header">
|
||||
<h3>报名信息列表</h3>
|
||||
<el-button type="success" icon="Download" @click="exportData">
|
||||
导出数据
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="filteredData"
|
||||
stripe
|
||||
border
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: '600' }"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="id" label="报名编号" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="registration-id">{{ row.id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="姓名" width="100" align="center" />
|
||||
<el-table-column prop="organization" label="所在单位" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="referrer" label="推荐人" width="100" align="center" />
|
||||
<el-table-column prop="referrerCompany" label="推荐人单位" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="department" label="部门" width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="title" label="职务" width="100" align="center" />
|
||||
<el-table-column prop="phone" label="手机号" width="120" align="center" />
|
||||
<el-table-column prop="period" label="期次" width="180" align="center" />
|
||||
<el-table-column prop="amount" label="金额" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-text">¥{{ row.amount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'paid' ? 'success' : 'warning'">
|
||||
{{ row.status === 'paid' ? '已支付' : '待支付' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="报名时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="viewDetail(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button type="warning" link @click="editStatus(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
{{ row.status === 'paid' ? '改为待支付' : '改为已支付' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="报名详情"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="detail-content" v-if="currentDetail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="报名编号">{{ currentDetail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ currentDetail.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在单位">{{ currentDetail.organization }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在部门">{{ currentDetail.department || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="推荐人">{{ currentDetail.referrer }}</el-descriptions-item>
|
||||
<el-descriptions-item label="推荐人单位">{{ currentDetail.referrerCompany }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职务/职称">{{ currentDetail.title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号码">{{ currentDetail.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电子邮箱">{{ currentDetail.email }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号">{{ currentDetail.idCard }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名期次">{{ currentDetail.period }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名金额">¥{{ currentDetail.amount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名状态">
|
||||
<el-tag :type="currentDetail.status === 'paid' ? 'success' : 'warning'">
|
||||
{{ currentDetail.status === 'paid' ? '已支付' : '待支付' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="报名时间">
|
||||
{{ formatTime(currentDetail.createTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注说明" :span="2">
|
||||
{{ currentDetail.remark || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||
<el-button
|
||||
v-if="currentDetail && currentDetail.status === 'submitted'"
|
||||
type="success"
|
||||
@click="markAsPaid"
|
||||
>
|
||||
标记为已支付
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getRegistrationList, getRegistrationStatistics, updateRegistrationStatus, getRegistrationDetail } from '../../api/registration'
|
||||
|
||||
const router = useRouter()
|
||||
// 数据
|
||||
const loading = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const filterStatus = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentDetail = ref(null)
|
||||
|
||||
// 管理员信息
|
||||
const adminUser = reactive({
|
||||
username: 'admin'
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const statistics = reactive({
|
||||
total: 0,
|
||||
paid: 0,
|
||||
submitted: 0,
|
||||
amount: 0
|
||||
})
|
||||
|
||||
// 原始数据
|
||||
const allData = ref([])
|
||||
|
||||
// 过滤后的数据
|
||||
const filteredData = computed(() => {
|
||||
return allData.value
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getRegistrationList({
|
||||
keyword: searchKeyword.value,
|
||||
pageNum: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
status: filterStatus.value
|
||||
})
|
||||
|
||||
// 映射接口字段
|
||||
allData.value = res.data.records.map(item => ({
|
||||
id: item.bmId,
|
||||
name: item.name,
|
||||
organization: item.company,
|
||||
department: item.department,
|
||||
title: item.title,
|
||||
phone: item.phone,
|
||||
period: item.period,
|
||||
amount: item.fee,
|
||||
status: item.status,
|
||||
createTime: item.submitTime,
|
||||
email: item.email,
|
||||
idCard: item.idCard,
|
||||
remark: item.remark,
|
||||
randomCode: item.randomCode,
|
||||
referrer: item.referrer,
|
||||
referrerCompany: item.referrerCompany
|
||||
}))
|
||||
|
||||
total.value = res.data.total
|
||||
} catch (e) {
|
||||
console.error('加载数据失败', e)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadStatistics = async () => {
|
||||
try {
|
||||
const res = await getRegistrationStatistics()
|
||||
statistics.total = res.data.totalRegistrations
|
||||
statistics.paid = res.data.paidRegistrations
|
||||
statistics.submitted = res.data.pendingPaymentRegistrations
|
||||
statistics.amount = res.data.paidAmount
|
||||
} catch (e) {
|
||||
console.error('加载统计数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
currentPage.value = 1
|
||||
loadData()
|
||||
ElMessage.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
currentPage.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
currentPage.value = val
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (row) => {
|
||||
currentDetail.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 标记为已支付
|
||||
const markAsPaid = async () => {
|
||||
ElMessageBox.confirm('确定将该报名标记为已支付吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await updateRegistrationStatus(currentDetail.value.randomCode, 'paid')
|
||||
detailDialogVisible.value = false
|
||||
loadData()
|
||||
loadStatistics()
|
||||
ElMessage.success('操作成功!')
|
||||
} catch (e) {
|
||||
console.error('操作失败', e)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 编辑支付状态
|
||||
const editStatus = (row) => {
|
||||
const newStatus = row.status === 'paid' ? 'submitted' : 'paid'
|
||||
const actionText = newStatus === 'paid' ? '标记为已支付' : '标记为待支付'
|
||||
|
||||
ElMessageBox.confirm(`确定要将该报名${actionText}吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await updateRegistrationStatus(row.randomCode, newStatus)
|
||||
loadData()
|
||||
loadStatistics()
|
||||
ElMessage.success('状态修改成功!')
|
||||
} catch (e) {
|
||||
console.error('操作失败', e)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
const exportData = () => {
|
||||
if (allData.value.length === 0) {
|
||||
ElMessage.warning('暂无数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
const exportData = allData.value.map(item => ({
|
||||
报名编号: item.id,
|
||||
姓名: item.name,
|
||||
所在单位: item.organization,
|
||||
部门: item.department || '',
|
||||
职务: item.title,
|
||||
手机号: item.phone,
|
||||
邮箱: item.email,
|
||||
身份证号: item.idCard,
|
||||
期次: item.period,
|
||||
金额: item.amount,
|
||||
状态: item.status === 'paid' ? '已支付' : '待支付',
|
||||
报名时间: formatTime(item.createTime),
|
||||
备注: item.remark || ''
|
||||
}))
|
||||
|
||||
// 简单导出为CSV
|
||||
const headers = Object.keys(exportData[0]).join(',')
|
||||
const rows = exportData.map(item => Object.values(item).join(','))
|
||||
const csv = [headers, ...rows].join('\n')
|
||||
|
||||
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `报名数据_${new Date().toLocaleDateString()}.csv`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('数据导出成功!')
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleCommand = (command) => {
|
||||
if (command === 'logout') {
|
||||
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
localStorage.removeItem('adminToken')
|
||||
localStorage.removeItem('adminUser')
|
||||
router.push('/admin/login')
|
||||
ElMessage.success('已退出登录')
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
const storedUser = localStorage.getItem('adminUser')
|
||||
if (storedUser) {
|
||||
const user = JSON.parse(storedUser)
|
||||
adminUser.username = user.username
|
||||
}
|
||||
loadStatistics()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.admin-main {
|
||||
padding: 24px 32px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.admin-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 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);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.admin-table-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-header h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.registration-id {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.amount-text {
|
||||
font-weight: 600;
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.admin-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.admin-main {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.admin-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-bar .el-input,
|
||||
.search-bar .el-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.admin-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,177 +0,0 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-card custom-card">
|
||||
<div class="login-header">
|
||||
<div class="login-icon">
|
||||
<el-icon><Lock /></el-icon>
|
||||
</div>
|
||||
<h2 class="login-title">管理端登录</h2>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="请输入用户名"
|
||||
prefix-icon="User"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="请输入密码"
|
||||
prefix-icon="Lock"
|
||||
clearable
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="password-toggle" @click="showPassword = !showPassword">
|
||||
<View v-if="!showPassword" />
|
||||
<Hide v-else />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
class="btn-primary btn-block"
|
||||
size="large"
|
||||
@click="handleLogin"
|
||||
:loading="loading"
|
||||
>
|
||||
<el-icon><Position /></el-icon>
|
||||
<span style="margin-left: 8px;">登 录</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { login } from '../../api/login'
|
||||
|
||||
const router = useRouter()
|
||||
const loginFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
const showPassword = ref(false)
|
||||
|
||||
// 登录表单
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
// 校验规则
|
||||
const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 登录处理
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
await loginFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await login({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password
|
||||
})
|
||||
|
||||
// 存储token
|
||||
localStorage.setItem('adminToken', res.data.token)
|
||||
localStorage.setItem('adminUser', JSON.stringify({
|
||||
username: loginForm.username,
|
||||
loginTime: new Date().toISOString()
|
||||
}))
|
||||
|
||||
ElMessage.success('登录成功!')
|
||||
router.push('/admin/dashboard')
|
||||
} catch (error) {
|
||||
console.error('登录失败', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.login-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.login-icon .el-icon {
|
||||
font-size: 32px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.login-card {
|
||||
padding: 32px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,18 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 4000,
|
||||
// 允许特定的主机名访问
|
||||
allowedHosts: ['robotclass.ngsk.tech'],
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue