feat:首次提交

zlx
zhoulexin 2 weeks ago
parent b18af53e52
commit 97a4d51b85

28
.gitignore vendored

@ -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

@ -0,0 +1,13 @@
<!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>

@ -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"
}
}

@ -0,0 +1,6 @@
<template>
<router-view />
</template>
<script setup>
</script>

@ -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'
})
}

@ -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 }
})
}

@ -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;
}

@ -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')

@ -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

@ -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

@ -0,0 +1,644 @@
<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="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.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
}))
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>

@ -0,0 +1,177 @@
<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>

@ -0,0 +1,432 @@
<template>
<div class="page-container">
<div class="page-wrapper">
<!-- 头部导航 -->
<div class="header-bar">
<div class="header-logo">
<el-icon><Collection /></el-icon>
<span>培训报名系统</span>
</div>
</div>
<!-- 表单卡片 -->
<div class="custom-card">
<div class="page-header" style="margin-top: 0;">
<h1 class="page-title">填写报名信息</h1>
<p class="page-subtitle">请填写以下报名信息 <span style="color:#f56c6c">*</span> 为必填项</p>
</div>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
size="large"
>
<!-- 基本信息 -->
<div class="form-section">
<div class="form-section-title">基本信息</div>
<div class="form-row">
<div class="form-item">
<el-form-item label="姓名" prop="name">
<el-input
v-model="formData.name"
placeholder="请输入您的姓名"
clearable
>
<template #prefix><el-icon><User /></el-icon></template>
</el-input>
</el-form-item>
</div>
<div class="form-item">
<el-form-item label="职务/职称" prop="title">
<el-select
v-model="formData.title"
placeholder="请选择职务/职称"
style="width: 100%"
>
<el-option label="教授" value="教授" />
<el-option label="副教授" value="副教授" />
<el-option label="讲师" value="讲师" />
<el-option label="工程师" value="工程师" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</div>
</div>
<div class="form-row">
<div class="form-item">
<el-form-item label="所在单位" prop="organization">
<el-input
v-model="formData.organization"
placeholder="请输入学校/企业全称"
clearable
>
<template #prefix><el-icon><OfficeBuilding /></el-icon></template>
</el-input>
</el-form-item>
</div>
<div class="form-item">
<el-form-item label="所在部门" prop="department">
<el-input
v-model="formData.department"
placeholder="请输入院系/部门名称"
clearable
>
<template #prefix><el-icon><Grid /></el-icon></template>
</el-input>
</el-form-item>
</div>
</div>
<div class="form-row">
<div class="form-item">
<el-form-item label="推荐人" prop="referrer">
<el-input
v-model="formData.referrer"
placeholder="请输入推荐人的姓名"
clearable
>
<template #prefix><el-icon><User /></el-icon></template>
</el-input>
</el-form-item>
</div>
<div class="form-item">
<el-form-item label="推荐人单位" prop="referrerCompany">
<el-input
v-model="formData.referrerCompany"
placeholder="请输入推荐单位"
clearable
>
<template #prefix><el-icon><User /></el-icon></template>
</el-input>
</el-form-item>
</div>
</div>
</div>
<!-- 联系信息 -->
<div class="form-section">
<div class="form-section-title">联系信息</div>
<div class="form-row">
<div class="form-item">
<el-form-item label="手机号码" prop="phone">
<el-input
v-model="formData.phone"
placeholder="请输入手机号码"
clearable
maxlength="11"
>
<template #prefix><el-icon><Phone /></el-icon></template>
</el-input>
</el-form-item>
</div>
<div class="form-item">
<el-form-item label="短信验证码" prop="smsCode">
<el-input
v-model="formData.smsCode"
placeholder="请输入验证码"
clearable
maxlength="6"
style="width: 100%"
>
<template #append>
<el-button
@click="sendSmsCodeFun"
:disabled="smsCountdown > 0"
style="min-width: 100px;"
>
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
</el-button>
</template>
</el-input>
</el-form-item>
</div>
</div>
<div class="form-row">
<div class="form-item">
<el-form-item label="电子邮箱" prop="email">
<el-input
v-model="formData.email"
placeholder="请输入电子邮箱"
clearable
>
<template #prefix><el-icon><Message /></el-icon></template>
</el-input>
</el-form-item>
</div>
<div class="form-item">
<el-form-item label="身份证号" prop="idCard">
<el-input
v-model="formData.idCard"
placeholder="请输入身份证号码"
clearable
maxlength="18"
>
<template #prefix><el-icon><Postcard /></el-icon></template>
</el-input>
</el-form-item>
</div>
</div>
</div>
<!-- 培训信息 -->
<div class="form-section">
<div class="form-section-title">培训信息</div>
<div class="form-row">
<div class="form-item">
<el-form-item label="报名期次" prop="period">
<el-radio-group v-model="formData.period">
<el-radio-button label="第一期 5月22-24日">第一期 5月22-24</el-radio-button>
</el-radio-group>
<el-radio-group v-model="formData.period">
<el-radio-button label="第二期 5月26-28日">第二期 5月26-28</el-radio-button>
</el-radio-group>
</el-form-item>
</div>
</div>
<div class="form-row">
<div class="form-item">
<el-form-item label="培训费用">
<div class="price-tag">
<span class="price-amount">¥2680</span>
<span class="price-unit">/</span>
</div>
</el-form-item>
</div>
</div>
<div class="form-row">
<div class="form-item" style="flex: 100%;">
<el-form-item label="备注说明" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="饮食禁忌、特殊需求等"
maxlength="200"
show-word-limit
/>
</el-form-item>
</div>
</div>
</div>
<!-- 提交按钮 -->
<div class="form-section" style="text-align: center;">
<el-button type="primary" size="large" class="btn-primary" @click="submitForm" :loading="submitLoading" :disabled="submitLoading" style="width: 100%; max-width: 300px;">
<el-icon><Position /></el-icon>
<span style="margin-left: 8px;">提交报名</span>
</el-button>
</div>
</el-form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { submitRegistration, sendSmsCode } from '../../api/registration'
const router = useRouter()
const formRef = ref(null)
const smsCountdown = ref(0)
const submitLoading = ref(false)
let smsTimer = null
//
const formData = reactive({
name: '',
organization: '',
department: '',
title: '',
referrer:'',
referrerCompany:'',
phone: '',
smsCode: '',
email: '',
idCard: '',
period: '第一期 5月22-24日',
remark: ''
})
//
const validatePhone = (rule, value, callback) => {
const phoneReg = /^1[3-9]\d{9}$/
if (!value) {
callback(new Error('请输入手机号码'))
} else if (!phoneReg.test(value)) {
callback(new Error('请输入正确的手机号码'))
} else {
callback()
}
}
const validateSmsCode = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入短信验证码'))
} else if (!/^\d{6}$/.test(value)) {
callback(new Error('验证码为6位数字'))
} else {
callback()
}
}
const validateEmail = (rule, value, callback) => {
const emailReg = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/
if (!value) {
callback(new Error('请输入电子邮箱'))
} else if (!emailReg.test(value)) {
callback(new Error('请输入正确的邮箱地址'))
} else {
callback()
}
}
const validateIdCard = (rule, value, callback) => {
const idCardReg = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/
if (!value) {
callback(new Error('请输入身份证号码'))
} else if (!idCardReg.test(value)) {
callback(new Error('请输入正确的身份证号码'))
} else {
callback()
}
}
const formRules = {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 20, message: '姓名长度为2-20个字符', trigger: 'blur' }
],
referrer: [
{ required: true, message: '请输入推荐人姓名', trigger: 'blur' },
{ min: 2, max: 20, message: '姓名长度为2-20个字符', trigger: 'blur' }
],
referrerCompany:[
{ required: true, message: '请输入推荐人单位', trigger: 'blur' }
],
organization: [
{ required: true, message: '请输入所在单位', trigger: 'blur' }
],
title: [
{ required: true, message: '请选择职务/职称', trigger: 'change' }
],
phone: [
{ required: true, validator: validatePhone, trigger: 'blur' }
],
smsCode: [
{ required: true, validator: validateSmsCode, trigger: 'blur' }
],
email: [
{ required: true, validator: validateEmail, trigger: 'blur' }
],
idCard: [
{ required: true, validator: validateIdCard, trigger: 'blur' }
],
period: [
{ required: true, message: '请选择报名期次', trigger: 'change' }
]
}
//
const sendSmsCodeFun = async () => {
if (!formData.phone) {
ElMessage.warning('请先输入手机号码')
return
}
const phoneReg = /^1[3-9]\d{9}$/
if (!phoneReg.test(formData.phone)) {
ElMessage.error('请输入正确的手机号码')
return
}
try {
await sendSmsCode(formData.phone)
ElMessage.success('验证码已发送,请注意查收')
smsCountdown.value = 60
smsTimer = setInterval(() => {
smsCountdown.value--
if (smsCountdown.value <= 0) {
clearInterval(smsTimer)
}
}, 1000)
} catch (e) {
console.error('发送验证码失败', e)
}
}
//
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
const res = await submitRegistration({
company: formData.organization,
department: formData.department,
email: formData.email,
fee: 2680,
idCard: formData.idCard,
name: formData.name,
referrer: formData.referrer,
referrerCompany: formData.referrerCompany,
period: formData.period,
phone: formData.phone,
remark: formData.remark,
title: formData.title
})
ElMessage.success('报名信息提交成功!即将跳转至支付页面...')
//
setTimeout(() => {
router.push({
path: '/client/payment',
query: {
bmId: res.data.bmId,
name: res.data.name,
referrer: res.data.referrer,
referrerCompany: res.data.referrerCompany,
company: res.data.company,
period: res.data.period
}
})
}, 1500)
} catch (e) {
console.error('提交失败', e)
} finally {
submitLoading.value = false
}
} else {
ElMessage.error('请检查表单填写是否正确')
return false
}
})
}
</script>
<style scoped>
:deep(.el-form-item__content){
gap: 10px;
}
@media screen and (max-width: 768px) {
.form-row {
flex-direction: column;
gap: 0;
}
.form-item {
width: 100%;
}
}
</style>

@ -0,0 +1,445 @@
<template>
<div class="page-container">
<div class="page-wrapper">
<!-- 支付确认卡片 -->
<div class="custom-card pay-card">
<!-- 成功图标和标题 -->
<div class="success-icon-wrapper">
<div class="success-icon">
<el-icon><CircleCheck /></el-icon>
</div>
<h2 class="success-title">报名信息已提交</h2>
<p class="success-subtitle">请完成支付以确认报名</p>
</div>
<!-- 报名信息展示 -->
<div class="registration-info" v-if="registrationData">
<div class="info-row">
<div class="info-item bottomBr">
<span class="info-label">报名编号</span>
<span class="info-value registration-id">{{ registrationData.bmId }}</span>
</div>
<div class="info-item">
<span class="info-label">姓名</span>
<span class="info-value">{{ registrationData.name }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item bottomBr">
<span class="info-label">所在单位</span>
<span class="info-value">{{ registrationData.company }}</span>
</div>
<div class="info-item bottomBr">
<span class="info-label">报名期次</span>
<span class="info-value">{{ registrationData.period }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">推荐人</span>
<span class="info-value">{{ registrationData.referrer }}</span>
</div>
<div class="info-item">
<span class="info-label">推荐人单位</span>
<span class="info-value">{{ registrationData.referrerCompany }}</span>
</div>
</div>
</div>
<!-- 费用展示 -->
<div class="amount-section">
<div class="amount-box">
<span class="amount-label">应付金额</span>
<span class="amount-value">¥2680</span>
</div>
</div>
<!-- 支付方式 -->
<div class="qrcode-section">
<div class="qrcode-title">
<el-icon><CreditCard /></el-icon>
<span>扫码支付</span>
</div>
<div class="qrcode-grid">
<!-- 微信支付 -->
<div class="qrcode-item">
<div class="qrcode-box">
<div class="qrcode-placeholder">
<svg viewBox="0 0 200 200" class="qrcode-svg">
<rect x="20" y="20" width="160" height="160" fill="white" stroke="#07C160" stroke-width="2"/>
<rect x="40" y="40" width="40" height="40" fill="#07C160"/>
<rect x="90" y="40" width="30" height="30" fill="#07C160"/>
<rect x="40" y="90" width="30" height="30" fill="#07C160"/>
<rect x="100" y="90" width="20" height="20" fill="#07C160"/>
<rect x="130" y="90" width="30" height="30" fill="#07C160"/>
<rect x="40" y="130" width="20" height="20" fill="#07C160"/>
<rect x="70" y="130" width="30" height="30" fill="#07C160"/>
<rect x="120" y="130" width="40" height="40" fill="#07C160"/>
</svg>
</div>
<p class="qrcode-label">微信支付</p>
</div>
</div>
<!-- 支付宝支付 -->
<div class="qrcode-item">
<div class="qrcode-box">
<div class="qrcode-placeholder">
<svg viewBox="0 0 200 200" class="qrcode-svg alipay">
<rect x="20" y="20" width="160" height="160" fill="white" stroke="#1677FF" stroke-width="2"/>
<circle cx="100" cy="100" r="50" fill="none" stroke="#1677FF" stroke-width="3"/>
<circle cx="100" cy="100" r="35" fill="none" stroke="#1677FF" stroke-width="2"/>
<rect x="80" y="70" width="40" height="10" fill="#1677FF"/>
<rect x="90" y="85" width="20" height="30" fill="#1677FF"/>
<path d="M 70 110 Q 100 140 130 110" fill="none" stroke="#1677FF" stroke-width="3"/>
</svg>
</div>
<p class="qrcode-label">支付宝支付</p>
</div>
</div>
</div>
<!-- 支付提示 -->
<div class="payment-tips">
<el-icon><InfoFilled /></el-icon>
<span>支付时请备注 <strong>姓名 + 手机号</strong>以便我们快速确认您的报名</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const route = useRoute()
const router = useRouter()
const registrationData = ref(null)
//
onMounted(() => {
const query = route.query
registrationData.value = {
bmId: query.bmId,
name: query.name,
company: query.company,
period: query.period,
referrer: query.referrer,
referrerCompany: query.referrerCompany
}
console.log(registrationData.value,'数据++++')
if (!registrationData.value.bmId) {
ElMessage.error('未找到报名信息')
// setTimeout(() => {
// router.push('/client/form')
// }, 2000)
}
})
</script>
<style scoped>
.success-icon-wrapper {
text-align: center;
margin-bottom: 32px;
}
.success-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
animation: scaleIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.success-icon .el-icon {
font-size: 40px;
color: #fff;
}
@keyframes scaleIn {
0% { transform: scale(0); opacity: 0; }
60% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
.success-title {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.success-subtitle {
font-size: 14px;
color: var(--text-secondary);
}
.registration-info {
background: var(--bg-color);
border-radius: 12px;
padding: 10px 24px;
margin-bottom: 24px;
}
.info-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-item {
flex: 1;
min-width: 180px;
}
.info-label {
display: block;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 6px;
}
.info-value {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
}
.registration-id {
font-family: 'Courier New', monospace;
font-size: 18px;
color: var(--primary-color);
font-weight: 600;
}
.amount-section {
text-align: center;
margin-bottom: 32px;
}
.amount-box {
display: inline-flex;
align-items: center;
gap: 16px;
background: linear-gradient(135deg, #fff5f5 0%, #fef0f0 100%);
padding: 16px 40px;
border-radius: 12px;
border: 2px dashed #F56C6C;
}
.amount-label {
font-size: 16px;
color: var(--text-secondary);
}
.amount-value {
font-size: 32px;
font-weight: 700;
color: #F56C6C;
}
.qrcode-section {
background: var(--bg-color);
border-radius: 12px;
padding: 32px;
margin-bottom: 32px;
}
.qrcode-title {
text-align: center;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.qrcode-grid {
display: flex;
justify-content: center;
gap: 48px;
flex-wrap: wrap;
}
.qrcode-item {
text-align: center;
}
.qrcode-box {
background: #fff;
padding: 16px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.qrcode-box:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.qrcode-placeholder {
width: 160px;
height: 160px;
display: flex;
align-items: center;
justify-content: center;
}
.qrcode-svg {
width: 100%;
height: 100%;
}
.qrcode-label {
margin-top: 12px;
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
}
.payment-tips {
margin-top: 24px;
padding: 16px;
background: #fffbeb;
border-radius: 8px;
text-align: center;
color: #946500;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
@media screen and (max-width: 768px) {
.success-icon-wrapper {
margin-bottom: 16px;
}
.success-icon {
width: 60px;
height: 60px;
}
.success-icon .el-icon {
font-size: 30px;
}
.success-title {
font-size: 18px;
}
.registration-info {
padding: 12px 16px;
margin-bottom: 15px;
}
.info-row {
flex-direction: column;
gap: 8px;
}
.info-item {
min-width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
}
.info-item.bottomBr{
border-bottom: 1px dashed #eee;
}
.info-label {
font-size: 12px;
margin-bottom: 0;
}
.info-value {
font-size: 14px;
}
.registration-id {
font-size: 14px;
}
.amount-section {
margin-bottom: 15px;
}
.amount-box {
padding: 10px 20px;
}
.amount-value {
font-size: 24px;
}
.qrcode-section {
padding: 16px;
margin: 0;
}
.qrcode-title {
font-size: 14px;
margin-bottom: 16px;
}
.qrcode-grid {
gap: 10px;
}
.qrcode-placeholder {
width: 120px;
height: 120px;
}
.qrcode-box {
padding: 12px;
}
.qrcode-label {
font-size: 12px;
margin-top: 8px;
}
.payment-tips {
font-size: 12px;
padding: 12px;
margin-top: 16px;
}
.action-buttons {
flex-direction: column;
}
.action-buttons .el-button {
width: 100%;
}
}
</style>

@ -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
}
})
Loading…
Cancel
Save