From 552369551836d888e871b2288b6d0105646dfb4e Mon Sep 17 00:00:00 2001 From: Disciple_C Date: Thu, 23 Jul 2026 15:00:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=90=E8=90=A5=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E5=9F=BA=E5=BB=BA=20=E6=90=AD=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 4 +- src/App.tsx | 1 + src/hooks/useAuth.ts | 33 +- src/layouts/BasicLayout.tsx | 4 +- src/pages/login/index.module.less | 810 +++++++++++++++++++++++++++++- src/pages/login/index.tsx | 233 +++++++-- src/pages/not-found/index.tsx | 2 +- src/router/guard.ts | 51 +- src/router/index.ts | 17 +- src/router/routes.ts | 14 +- src/stores/menuStore.ts | 57 ++- 11 files changed, 1096 insertions(+), 130 deletions(-) diff --git a/index.html b/index.html index 5ff9342..d3cfca5 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,8 @@ - - 赛事运营平台 + + 六个羽友赛事运营平台
diff --git a/src/App.tsx b/src/App.tsx index ba5b391..f6f5c6d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ export default defineComponent({ const theme = { token: { colorPrimary: '#1677ff', + borderRadius: 8, }, }; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 8a18967..5eca581 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -3,7 +3,7 @@ import { useState } from './useState'; import { watch } from 'vue'; import { useMenuStore } from '@/stores/menuStore'; import { usePermissionStore } from '@/stores/permissionStore'; -import router, { WHITE_LIST } from '@/router'; +import router from '@/router'; const TOKEN_KEY = 'MY_APP_AUTH_TOKEN'; @@ -60,34 +60,3 @@ function createAuth() { } export const auth = createAuth(); - -/** - * 路由守卫逻辑(由 router/guard.ts 使用) - * 判断逻辑: - * 1. 白名单路径 → 直接放行 - * 2. 已登录但菜单未加载 → 加载菜单后放行 - * 3. 已登录且菜单已加载 → 直接放行 - * 4. 未登录 → 跳转 /login - */ -export async function handleRouteGuard( - toPath: string, -): Promise<{ allow: boolean; redirect?: string }> { - // 白名单直接放行 - if (WHITE_LIST.includes(toPath) || toPath.match(/^\/:pathMatch/)) { - return { allow: true }; - } - - // 未登录 - if (!auth.isLoggedIn()) { - return { allow: false, redirect: '/login' }; - } - - // 已登录但菜单未加载 → 先加载 - const { loadMenu, loaded } = useMenuStore(); - if (!loaded.value) { - const { loadPermissions } = usePermissionStore(); - await Promise.all([loadMenu(), loadPermissions()]); - } - - return { allow: true }; -} diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index 784e918..00e4baa 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -77,7 +77,7 @@ export default defineComponent({
- {collapsed.value ? 'CPMS' : 'CPMS 运营平台'} + {collapsed.value ? '6羽' : '六个羽友赛事'}
-
CPMS 运营平台 ©{new Date().getFullYear()}
+
六个羽友赛事运营平台 ©{new Date().getFullYear()}
); diff --git a/src/pages/login/index.module.less b/src/pages/login/index.module.less index 7932457..cae8ec8 100644 --- a/src/pages/login/index.module.less +++ b/src/pages/login/index.module.less @@ -1,20 +1,806 @@ +// ── 登录页 — 左右分栏 — 动画背景版 ── +// 左侧:深色品牌展示区,含浮动光球、网格呼吸、脉冲涟漪、粒子点阵等多层动画 +// 右侧:白色登录表单区,入场渐显动画 + .container { min-height: 100vh; display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + overflow: hidden; } -.card { - width: 400px; - max-width: 90vw; - border-radius: 12px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); +// ━━━━ 左侧品牌展示区 ━━━━ +.brand { + flex: 1; + display: flex; + flex-direction: column; + position: relative; + background: linear-gradient(160deg, #0c1e3a 0%, #132744 50%, #0a1628 100%); + color: #fff; + overflow: hidden; + padding: 40px 56px 40px 64px; +} - :global(.ant-card-head-title) { - text-align: center; - font-size: 18px; - font-weight: 600; +// ── 动画层1: 浮动光球 ── +.orb1 { + position: absolute; + width: 460px; + height: 460px; + border-radius: 50%; + background: radial-gradient( + circle, + rgba(22, 119, 255, 0.14) 0%, + rgba(22, 119, 255, 0.04) 40%, + transparent 70% + ); + top: -100px; + right: 20%; + z-index: 0; + animation: orbFloat1 12s ease-in-out infinite; + filter: blur(2px); +} + +.orb2 { + position: absolute; + width: 360px; + height: 360px; + border-radius: 50%; + background: radial-gradient( + circle, + rgba(82, 196, 26, 0.1) 0%, + rgba(82, 196, 26, 0.03) 40%, + transparent 70% + ); + bottom: 40px; + left: 10%; + z-index: 0; + animation: orbFloat2 15s ease-in-out infinite; + filter: blur(2px); +} + +.orb3 { + position: absolute; + width: 280px; + height: 280px; + border-radius: 50%; + background: radial-gradient( + circle, + rgba(250, 173, 20, 0.08) 0%, + rgba(250, 173, 20, 0.02) 40%, + transparent 70% + ); + top: 50%; + left: 55%; + z-index: 0; + animation: orbFloat3 18s ease-in-out infinite; + filter: blur(3px); +} + +// ── 动画层2: 脉冲涟漪 ── +.pulse { + position: absolute; + width: 200px; + height: 200px; + border-radius: 50%; + border: 1px solid rgba(22, 119, 255, 0.15); + top: 30%; + right: 15%; + z-index: 0; + animation: pulseExpand 4s ease-out infinite; + + &::before { + content: ''; + position: absolute; + inset: 30px; + border-radius: 50%; + border: 1px solid rgba(22, 119, 255, 0.08); + animation: pulseExpand 4s ease-out infinite 1s; + } + + &::after { + content: ''; + position: absolute; + inset: 60px; + border-radius: 50%; + border: 1px solid rgba(22, 119, 255, 0.04); + animation: pulseExpand 4s ease-out infinite 2s; + } +} + +// ── 动画层3: 网格呼吸 ── +.brandGrid { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + background-image: + linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px); + background-size: 50px 50px; + animation: gridBreathe 6s ease-in-out infinite; +} + +// ── 动画层4: 粒子点阵 ── +.particles { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; +} + +.particle { + position: absolute; + width: 3px; + height: 3px; + border-radius: 50%; + background: rgba(22, 119, 255, 0.5); + animation: particleFloat 8s ease-in-out infinite; + + &::before { + content: ''; + position: absolute; + width: 2px; + height: 2px; + border-radius: 50%; + background: rgba(105, 177, 255, 0.3); + top: -8px; + left: 3px; + animation: particleFloat 6s ease-in-out infinite reverse; + } +} + +// ── 动画层5: 扫描线 ── +.scanLine { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, rgba(22, 119, 255, 0.3), transparent); + z-index: 1; + animation: scanDown 8s linear infinite; +} + +// ━━━━ 动画定义 ━━━━ +@keyframes orbFloat1 { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 25% { + transform: translate(30px, 20px) scale(1.05); + } + 50% { + transform: translate(-20px, 40px) scale(0.95); + } + 75% { + transform: translate(10px, -30px) scale(1.02); + } +} + +@keyframes orbFloat2 { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 33% { + transform: translate(-40px, -20px) scale(1.08); + } + 66% { + transform: translate(30px, 30px) scale(0.92); + } +} + +@keyframes orbFloat3 { + 0%, + 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(-50px, -40px); + } +} + +@keyframes pulseExpand { + 0% { + transform: scale(0.8); + opacity: 0.6; + } + 100% { + transform: scale(1.6); + opacity: 0; + } +} + +@keyframes gridBreathe { + 0%, + 100% { + opacity: 0.6; + } + 50% { + opacity: 1; + } +} + +@keyframes particleFloat { + 0%, + 100% { + transform: translateY(0) translateX(0); + opacity: 0.5; + } + 25% { + transform: translateY(-30px) translateX(10px); + opacity: 1; + } + 50% { + transform: translateY(-60px) translateX(-5px); + opacity: 0.3; + } + 75% { + transform: translateY(-30px) translateX(15px); + opacity: 0.8; + } +} + +@keyframes scanDown { + 0% { + top: -2px; + } + 100% { + top: 100%; + } +} + +// ━━━━ 内容层 ━━━━ +.brandInner { + position: relative; + z-index: 2; + display: flex; + flex-direction: column; + flex: 1; + animation: fadeInUp 0.8s ease-out both; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +// ── Logo ── +.logoRow { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 48px; +} + +.logoMark { + width: 48px; + height: 48px; + border-radius: 14px; + background: linear-gradient(135deg, #1677ff, #69b1ff); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + font-weight: 800; + color: #fff; + box-shadow: 0 4px 20px rgba(22, 119, 255, 0.45); + animation: logoPulse 3s ease-in-out infinite; + position: relative; + + &::after { + content: ''; + position: absolute; + top: 4px; + left: 4px; + width: 18px; + height: 7px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.3); + } +} + +@keyframes logoPulse { + 0%, + 100% { + box-shadow: 0 4px 20px rgba(22, 119, 255, 0.45); + } + 50% { + box-shadow: + 0 4px 30px rgba(22, 119, 255, 0.7), + 0 0 60px rgba(22, 119, 255, 0.2); + } +} + +.logoText { + font-size: 18px; + font-weight: 600; + letter-spacing: 2px; + color: rgba(255, 255, 255, 0.9); +} + +// ── 大标题 ── +.brandTitle { + font-size: 32px; + font-weight: 700; + line-height: 1.35; + letter-spacing: 1px; + margin-bottom: 12px; + animation: fadeInUp 0.8s ease-out 0.1s both; +} + +.brandSubtitle { + font-size: 15px; + line-height: 1.7; + color: rgba(255, 255, 255, 0.55); + margin-bottom: 40px; + max-width: 400px; + animation: fadeInUp 0.8s ease-out 0.2s both; +} + +// ── 数据统计条 ── +.statsRow { + display: flex; + gap: 36px; + margin-bottom: 44px; + animation: fadeInUp 0.8s ease-out 0.3s both; +} + +.statItem { + display: flex; + flex-direction: column; + gap: 4px; +} + +.statValue { + font-size: 28px; + font-weight: 700; + color: #fff; + letter-spacing: 1px; +} + +.statLabel { + font-size: 12px; + color: rgba(255, 255, 255, 0.45); + letter-spacing: 1px; +} + +.statHighlight { + color: #69b1ff; +} + +// ── 赛事模拟卡片 ── +.eventCards { + display: flex; + gap: 16px; + margin-bottom: 44px; + animation: fadeInUp 0.8s ease-out 0.4s both; +} + +.eventCard { + flex: 1; + padding: 20px; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 14px; + backdrop-filter: blur(8px); + transition: all 0.3s ease; + position: relative; + overflow: hidden; + + // 内部光效装饰 + &::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle at 30% 30%, rgba(22, 119, 255, 0.06) 0%, transparent 50%); + animation: cardShimmer 5s linear infinite; + pointer-events: none; + } + + &:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(22, 119, 255, 0.3); + transform: translateY(-3px); + box-shadow: 0 8px 24px rgba(22, 119, 255, 0.15); + } +} + +@keyframes cardShimmer { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.eventIcon { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: 700; + color: #fff; + margin-bottom: 12px; + position: relative; + z-index: 1; + + &.blue { + background: linear-gradient(135deg, #1677ff, #4096ff); + box-shadow: 0 2px 12px rgba(22, 119, 255, 0.3); + } + + &.green { + background: linear-gradient(135deg, #52c41a, #73d13d); + box-shadow: 0 2px 12px rgba(82, 196, 26, 0.3); + } + + &.orange { + background: linear-gradient(135deg, #fa8c16, #ffa940); + box-shadow: 0 2px 12px rgba(250, 140, 22, 0.3); + } +} + +.eventTitle { + font-size: 15px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); + margin-bottom: 6px; + position: relative; + z-index: 1; +} + +.eventDesc { + font-size: 12px; + color: rgba(255, 255, 255, 0.45); + line-height: 1.5; + position: relative; + z-index: 1; +} + +// ── 步骤流程 ── +.flowSteps { + display: flex; + align-items: center; + gap: 0; + animation: fadeInUp 0.8s ease-out 0.5s both; +} + +.step { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + position: relative; +} + +// ── 星点效果 ── +// 中心发光点 + 四条十字光芒 + 缓慢旋转 + 闪烁呼吸 +.stepDot { + position: relative; + width: 20px; + height: 20px; + margin-bottom: 12px; + + // 中心核心圆点 + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 6px; + height: 6px; + border-radius: 50%; + background: #fff; + box-shadow: + 0 0 4px 2px rgba(22, 119, 255, 0.8), + 0 0 12px 4px rgba(22, 119, 255, 0.5), + 0 0 24px 8px rgba(22, 119, 255, 0.2); + transform: translate(-50%, -50%); + animation: starCorePulse 2s ease-in-out infinite; + } + + // 十字光芒线(横+纵) + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + transform: translate(-50%, -50%) rotate(45deg); + animation: + starSpin 6s linear infinite, + starGlowPulse 2s ease-in-out infinite; + // 横向光芒 + background: + linear-gradient( + 0deg, + transparent 45%, + rgba(22, 119, 255, 0.9) 48%, + #fff 50%, + rgba(22, 119, 255, 0.9) 52%, + transparent 55% + ), + linear-gradient( + 90deg, + transparent 45%, + rgba(22, 119, 255, 0.9) 48%, + #fff 50%, + rgba(22, 119, 255, 0.9) 52%, + transparent 55% + ); + } +} + +// 步骤之间的连线 +.stepLine { + flex: 1; + height: 1px; + min-width: 20px; + background: linear-gradient(90deg, rgba(22, 119, 255, 0.5), rgba(22, 119, 255, 0.15)); + align-self: center; + margin-top: -6px; // 微调与星点中心对齐(因为label占空间) +} + +@keyframes starCorePulse { + 0%, + 100% { + box-shadow: + 0 0 4px 2px rgba(22, 119, 255, 0.8), + 0 0 12px 4px rgba(22, 119, 255, 0.5), + 0 0 24px 8px rgba(22, 119, 255, 0.2); + } + 50% { + box-shadow: + 0 0 6px 3px rgba(22, 119, 255, 1), + 0 0 20px 6px rgba(22, 119, 255, 0.7), + 0 0 40px 12px rgba(22, 119, 255, 0.3); + } +} + +@keyframes starSpin { + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } +} + +@keyframes starGlowPulse { + 0%, + 100% { + opacity: 0.7; + } + 50% { + opacity: 1; + } +} + +.stepLabel { + font-size: 13px; + color: rgba(255, 255, 255, 0.7); + font-weight: 500; + text-align: center; +} + +// ── 左侧底部版权 ── +.brandFooter { + margin-top: auto; + font-size: 12px; + color: rgba(255, 255, 255, 0.2); + letter-spacing: 1px; +} + +// ━━━━ 右侧登录区 ━━━━ +.loginSide { + width: 460px; + min-width: 380px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 48px; + background: #fff; + position: relative; +} + +.loginWrapper { + width: 100%; + max-width: 340px; + animation: fadeInRight 0.6s ease-out 0.3s both; +} + +@keyframes fadeInRight { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.loginTitle { + font-size: 24px; + font-weight: 700; + color: rgba(0, 0, 0, 0.88); + margin-bottom: 6px; +} + +.loginDesc { + font-size: 14px; + color: rgba(0, 0, 0, 0.45); + margin-bottom: 36px; +} + +.loginForm { + :global(.ant-input-affix-wrapper) { + padding: 10px 14px; + border-radius: 10px; + border-color: #d9d9d9; + transition: all 0.3s; + + &:hover { + border-color: #1677ff; + } + + &:focus-within { + border-color: #1677ff; + box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.08); + } + } + + :global(.ant-input-prefix) { + color: rgba(0, 0, 0, 0.3); + margin-right: 8px; + } + + :global(.ant-form-item) { + margin-bottom: 22px; + } +} + +.loginBtn { + height: 46px; + border-radius: 10px; + font-size: 15px; + font-weight: 600; + letter-spacing: 3px; + transition: all 0.3s; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(22, 119, 255, 0.35); + } +} + +// ── 记住密码行 ── +.rememberRow { + margin-bottom: 8px; + + :global(.ant-checkbox-wrapper) { + color: rgba(0, 0, 0, 0.55); + font-size: 13px; + } + + :global(.ant-checkbox-checked .ant-checkbox-inner) { + background-color: #1677ff; + border-color: #1677ff; + } +} + +.loginFooter { + margin-top: 28px; + text-align: center; + font-size: 12px; + color: rgba(0, 0, 0, 0.25); +} + +// ━━━━ 响应式 — PC 端自适应 ━━━━ + +// ── 大屏 (>1200px): 默认样式,无需额外处理 ── + +// ── 中屏 (≤1200px): 缩减间距、光球尺寸、字号 ── +@media screen and (max-width: 1200px) { + .brand { + padding: 32px 36px 32px 40px; + } + + .brandTitle { + font-size: 28px; + } + + .brandSubtitle { + font-size: 14px; + margin-bottom: 32px; + } + + .statsRow { + gap: 24px; + margin-bottom: 32px; + } + + .statValue { + font-size: 24px; + } + + .eventCards { + gap: 12px; + margin-bottom: 32px; + } + + .eventCard { + padding: 16px; + } + + .eventIcon { + width: 32px; + height: 32px; + margin-bottom: 10px; + } + + .eventTitle { + font-size: 14px; + } + + .eventDesc { + font-size: 11px; + } + + .orb1 { + width: 360px; + height: 360px; + } + + .orb2 { + width: 280px; + height: 280px; + } + + .orb3 { + width: 200px; + height: 200px; + } + + .loginSide { + width: 400px; + min-width: 340px; + padding: 36px 40px; + } + + .loginWrapper { + max-width: 320px; + } + + .loginTitle { + font-size: 22px; + } + + .loginDesc { + font-size: 13px; + } + + .stepLabel { + font-size: 12px; } } diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 6eb3069..1db2b00 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -1,7 +1,8 @@ import { defineComponent, reactive, ref } from 'vue'; -import { Button, Card, Form, FormItem, Input, message } from 'ant-design-vue'; +import { Button, Checkbox, Form, FormItem, Input, message } from 'ant-design-vue'; import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'; import { auth } from '@/hooks/useAuth'; +import { useEffect } from '@/hooks/useEffect'; import styles from './index.module.less'; interface LoginForm { @@ -9,20 +10,31 @@ interface LoginForm { password: string; } -/** - * 登录页 - * auth.login() 内部会自动:保存 token → 拉取菜单+权限 → 动态注册路由 → 跳转首页 - */ export default defineComponent({ name: 'LoginPage', setup() { const loading = ref(false); + const rememberMe = ref(false); const form = reactive({ - username: 'admin', - password: '123456', + username: '', + password: '', }); + useEffect(() => { + const saved = localStorage.getItem('cpms_login_remember'); + if (saved) { + try { + const data = JSON.parse(saved); + form.username = data.username || ''; + form.password = data.password || ''; + rememberMe.value = true; + } catch { + localStorage.removeItem('cpms_login_remember'); + } + } + }, []); + const rules = { username: [{ required: true, message: '请输入用户名' }], password: [{ required: true, message: '请输入密码' }], @@ -37,14 +49,21 @@ export default defineComponent({ loading.value = true; try { - // TODO: 替换为真实登录接口 - // const res = await post('/auth/login', { username: form.username, password: form.password }); - // await auth.login(res.token); - - // Mock:模拟登录 await new Promise((r) => setTimeout(r, 600)); await auth.login('mock_token_' + Date.now()); message.success('登录成功'); + + if (rememberMe.value) { + localStorage.setItem( + 'cpms_login_remember', + JSON.stringify({ + username: form.username, + password: form.password, + }), + ); + } else { + localStorage.removeItem('cpms_login_remember'); + } } catch (err) { message.error('登录失败,请重试'); } finally { @@ -52,34 +71,174 @@ export default defineComponent({ } }; + const particles = [ + { top: '8%', left: '15%', delay: '0s', duration: '8s' }, + { top: '22%', left: '65%', delay: '1.5s', duration: '7s' }, + { top: '45%', left: '25%', delay: '0.8s', duration: '9s' }, + { top: '60%', left: '75%', delay: '2s', duration: '6.5s' }, + { top: '75%', left: '40%', delay: '3s', duration: '8.5s' }, + { top: '15%', left: '80%', delay: '1s', duration: '7.5s' }, + { top: '35%', left: '50%', delay: '2.5s', duration: '6s' }, + { top: '85%', left: '20%', delay: '0.5s', duration: '9.5s' }, + { top: '50%', left: '90%', delay: '3.5s', duration: '8s' }, + { top: '92%', left: '55%', delay: '1.2s', duration: '7s' }, + ]; + return () => (
- -
- - }} +
+
+
+
+
+
+
+
+ {particles.map((p, i) => ( +
- - - }} - /> - - - - - - + ))} +
+ +
+
+
6
+ 羽友赛事 +
+

六个羽友赛事运营平台

+

+ 专业的赛事管理、报名运营与数据分析一站式解决方案,助力每一场精彩赛事 +

+
+
+ + 1,200+ + + 累计赛事场次 +
+
+ + 50,000+ + + 参赛选手 +
+
+ + 100% + + 运营满意度 +
+
+ +
+
+
📋
+
赛事管理
+
创建发布、赛事审核、赛程编排全流程在线化
+
+
+
🏆
+
对阵分组
+
智能分组算法、自动生成对阵表与淘汰树
+
+
+
📊
+
数据看板
+
参赛统计、实时进度、成绩排名可视化呈现
+
+
+ +
+
+ + 创建赛事 +
+
+
+ + 赛事审核 +
+
+
+ + 分组对阵 +
+
+
+ + 成绩录入 +
+
+
+ + 数据复盘 +
+
+ +
六个羽友赛事运营平台 ©{new Date().getFullYear()}
+
+
+ +
+
+

欢迎回来

+

登录六个羽友赛事运营平台

+ +
+ + }} + /> + + + }} + /> + + + {/* 记住密码 */} +
+ 记住密码 +
+ + + + +
+ +
六个羽友赛事运营平台 ©{new Date().getFullYear()}
+
+
); }, diff --git a/src/pages/not-found/index.tsx b/src/pages/not-found/index.tsx index 7070442..921db6e 100644 --- a/src/pages/not-found/index.tsx +++ b/src/pages/not-found/index.tsx @@ -10,7 +10,7 @@ export default defineComponent({ setup() { const router = useRouter(); - const goHome = () => router.push('/'); + const goHome = () => router.push('/login'); return () => ( { // 设置页面标题 const appTitle = import.meta.env.VITE_APP_TITLE || 'CPMS 运营平台'; document.title = to.meta.title ? `${to.meta.title} - ${appTitle}` : appTitle; - const { allow, redirect } = await handleRouteGuard(to.path); - - if (allow) { - // 已登录访问 /login → 跳首页 - if (to.path === '/login' && auth.isLoggedIn()) { + // ── /login 特殊处理 ── + if (to.path === '/login') { + if (auth.isLoggedIn()) { const { homePath } = await import('@/stores/menuStore').then((m) => m.useMenuStore()); next(homePath.value); - return; + } else { + next(); } - next(); - } else { - next(redirect || '/login'); + return; } + + // ── 未登录 → 跳转登录页 ── + if (!auth.isLoggedIn()) { + next('/login'); + return; + } + + // ── 已登录:确保菜单和动态路由已加载 ── + const { loadMenu, loaded } = await import('@/stores/menuStore').then((m) => m.useMenuStore()); + if (!loaded.value) { + const { loadPermissions } = await import('@/stores/permissionStore').then((m) => + m.usePermissionStore(), + ); + await Promise.all([loadMenu(), loadPermissions()]); + + // 动态路由刚注册,需要用新路由表重新匹配当前路径 + next({ path: to.fullPath, replace: true }); + return; + } + + next(); }); diff --git a/src/router/index.ts b/src/router/index.ts index 0cccd85..1a56f2d 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -13,17 +13,22 @@ const router = createRouter({ const WHITE_LIST = ['/login', '/404']; /** - * 重置路由:清除动态添加的子路由,恢复到只包含静态路由的状态 - * 用于退出登录时清理 + * 静态路由名称集合(这些路由不会被 resetRouter 移除) + */ +const STATIC_ROUTE_NAMES = new Set(['Login', 'NotFound']); + +/** + * 重置路由:清除动态添加的路由(包括 BasicLayout 和其子路由), + * 恢复到只包含静态路由的状态。用于退出登录时清理。 */ export function resetRouter() { const currentRoutes = router.getRoutes(); - const protectedNames = new Set(['Login', 'BasicLayout', 'NotFound']); currentRoutes.forEach((route) => { - // 保留静态路由(Login, BasicLayout, NotFound),移除动态添加的子路由 - if (!protectedNames.has(route.name as string)) { - router.removeRoute(route.name!); + const routeName = route.name as string; + // 保留静态路由(Login, NotFound),移除所有动态路由(含 BasicLayout 及其子路由) + if (routeName && !STATIC_ROUTE_NAMES.has(routeName)) { + router.removeRoute(routeName); } }); } diff --git a/src/router/routes.ts b/src/router/routes.ts index 2b711fc..a33f51a 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -6,6 +6,9 @@ import type { RouteRecordRaw } from 'vue-router'; * 动态路由由 menuStore 在登录后通过 router.addRoute 注入, * 后端下发 MenuNode 包含 component 字段, * 前端通过 import.meta.glob 动态匹配 src/pages 下的组件。 + * + * 注意: BasicLayout 路由不在此注册,而是由 menuStore 动态添加, + * 这样可以避免空 children 导致的路由匹配问题。 */ export const constantRoutes: RouteRecordRaw[] = [ // ── 登录页(不经过布局)── @@ -16,17 +19,8 @@ export const constantRoutes: RouteRecordRaw[] = [ meta: { title: '登录' }, }, - // ── 主布局(动态路由的父容器) ── - // name 为 'BasicLayout',动态路由通过 router.addRoute('BasicLayout', child) 挂载到此布局下 - { - path: '/', - name: 'BasicLayout', - component: () => import('@/layouts/BasicLayout'), - children: [], - meta: { title: '首页' }, - }, - // ── 404 ── + // 注意: 通配路由必须放在最后,且不能被动态路由覆盖 { path: '/:pathMatch(.*)*', name: 'NotFound', diff --git a/src/stores/menuStore.ts b/src/stores/menuStore.ts index 2c8628c..97a4380 100644 --- a/src/stores/menuStore.ts +++ b/src/stores/menuStore.ts @@ -141,6 +141,44 @@ function getFirstMenuPath(nodes: MenuNode[]): string { return '/dashboard'; } +/** + * 注册 BasicLayout 布局路由(如果不存在) + * 动态路由作为其子路由挂载 + */ +function ensureLayoutRoute() { + const existing = router.hasRoute('BasicLayout'); + if (!existing) { + router.addRoute({ + path: '/', + name: 'BasicLayout', + component: () => import('@/layouts/BasicLayout'), + children: [], + meta: { title: '首页' }, + }); + } +} + +/** + * 注册兜底菜单和路由(后端接口不可用时使用) + */ +function registerFallbackRoutes() { + ensureLayoutRoute(); + + router.addRoute('BasicLayout', { + path: 'dashboard', + name: 'Dashboard', + component: () => import('@/pages/dashboard'), + meta: { title: '工作台', icon: 'DashboardOutlined' }, + }); + router.addRoute('BasicLayout', { + path: 'about', + name: 'About', + component: () => import('@/pages/about'), + meta: { title: '关于', icon: 'InfoCircleOutlined' }, + }); + router.addRoute('BasicLayout', { path: '', redirect: '/dashboard' }); +} + export function useMenuStore() { const loadMenu = async () => { if (state.loaded) return; @@ -151,6 +189,9 @@ export function useMenuStore() { state.menuItems = transformMenuNode(menuTree); state.homePath = getFirstMenuPath(menuTree); + // 确保 BasicLayout 布局路由已注册 + ensureLayoutRoute(); + // 动态注册路由:将后端路由配置注入到 BasicLayout 布局下 const dynamicRoutes = transformMenuToRoutes(menuTree); dynamicRoutes.forEach((route) => { @@ -170,20 +211,8 @@ export function useMenuStore() { ]; state.homePath = '/dashboard'; - // 兜底路由(挂载到 BasicLayout 下) - router.addRoute('BasicLayout', { - path: '/dashboard', - name: 'Dashboard', - component: () => import('@/pages/dashboard'), - meta: { title: '工作台', icon: 'DashboardOutlined' }, - }); - router.addRoute('BasicLayout', { - path: '/about', - name: 'About', - component: () => import('@/pages/about'), - meta: { title: '关于', icon: 'InfoCircleOutlined' }, - }); - router.addRoute('BasicLayout', { path: '', redirect: '/dashboard' }); + // 兜底路由 + registerFallbackRoutes(); state.loaded = true; }