瀏覽代碼

保存一下

flyzto 7 月之前
父節點
當前提交
6f6d4d211c

+ 6 - 0
server/libs/logs.js

@@ -30,6 +30,12 @@ class Logs {
     }
   }
 
+  static errDev(...args) {
+    if (process.env.NODE_ENV == 'development') {
+      this.err(...args);
+    }
+  }
+
   static outLine(string) {
     process.stdout.write("\u001b[1A");
     process.stdout.write("\u001b[2K");

+ 5 - 2
server/middleware/authMiddleware.js

@@ -1,16 +1,19 @@
 const jwt = require('jsonwebtoken');
-
+const Logs = require('../libs/logs');
 module.exports = (req, res, next) => {
-  const token = req.headers['authorization'];
+  const token = req.headers['authorization'].replace('Bearer ', '');
+
   if (!token) {
     return res.unauthorized('未提供 token');
   }
+
   try {
     const decoded = jwt.verify(token, process.env.JWT_SECRET);
     req.userId = decoded.userId;
     next();
   }
   catch (err) {
+    Logs.errDev('token验证错误:', err);
     res.unauthorized('无效或已过期的 token');
   }
 };

+ 1 - 0
server/models/User.js

@@ -4,6 +4,7 @@ const { Schema } = mongoose;
 const userSchema = new Schema({
   username: { type: String, required: true, unique: true },
   password: { type: String, required: true },
+  roles: { type: [String], default: ['user'] },
 }, { timestamps: true });
 
 module.exports = mongoose.model('User', userSchema);

+ 20 - 0
server/package-lock.json

@@ -11,6 +11,7 @@
       "dependencies": {
         "axios": "^1.8.4",
         "bcryptjs": "^3.0.2",
+        "cookie-parser": "^1.4.7",
         "dayjs": "^1.11.13",
         "dotenv": "^16.5.0",
         "express": "^5.1.0",
@@ -196,6 +197,25 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/cookie-parser": {
+      "version": "1.4.7",
+      "resolved": "https://registry.npmmirror.com/cookie-parser/-/cookie-parser-1.4.7.tgz",
+      "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
+      "license": "MIT",
+      "dependencies": {
+        "cookie": "0.7.2",
+        "cookie-signature": "1.0.6"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/cookie-parser/node_modules/cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+      "license": "MIT"
+    },
     "node_modules/cookie-signature": {
       "version": "1.2.2",
       "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz",

+ 1 - 0
server/package.json

@@ -13,6 +13,7 @@
   "dependencies": {
     "axios": "^1.8.4",
     "bcryptjs": "^3.0.2",
+    "cookie-parser": "^1.4.7",
     "dayjs": "^1.11.13",
     "dotenv": "^16.5.0",
     "express": "^5.1.0",

+ 2 - 2
server/routes/triangle.js

@@ -49,8 +49,8 @@ router.post('/update_games_relation', (req, res) => {
 
 // 删除关联比赛
 router.post('/remove_games_relation', (req, res) => {
-  const { eventId } = req.body;
-  Games.removeGamesRelation(eventId)
+  const { id } = req.body;
+  Games.removeGamesRelation(id)
   .then(ret => {
     res.sendSuccess(ret);
   })

+ 128 - 43
server/routes/user.js

@@ -6,81 +6,166 @@ const router = express.Router();
 const authMiddleware = require('../middleware/authMiddleware');
 
 const User = require('../models/User');
+const Logs = require('../libs/logs');
 
 // 注册
 router.post('/register', async (req, res) => {
   const { username, password } = req.body;
-  try {
-    const existing = await User.findOne({ username });
+  User.findOne({ username })
+  .then(existing => {
     if (existing) {
-      return res.badRequest('用户已存在');
+      return Promise.reject(new Error('USER_EXISTS'));
     }
-
-    const hashedPassword = await bcrypt.hash(password, 10);
+    return bcrypt.hash(password, 10);
+  })
+  .then(hashedPassword => {
     const user = new User({ username, password: hashedPassword });
-    await user.save();
+    return user.save();
+  })
+  .then(() => {
     res.sendSuccess('注册成功');
-  }
-  catch (err) {
-    res.serverError();
-  }
+  })
+  .catch(err => {
+    Logs.errDev('注册失败:', err);
+    if (err.message === 'USER_EXISTS') {
+      return res.badRequest('用户已存在');
+    }
+    res.badRequest(err.message);
+  });
 });
 
-// 登录
+// 登录 - 支持原路径 /login 和 mock 服务路径 /login
 router.post('/login', async (req, res) => {
   const { username, password } = req.body;
-  try {
-    const user = await User.findOne({ username });
+  User.findOne({ username })
+  .then(user => {
     if (!user) {
-      return res.badRequest('用户不存在');
+      return Promise.reject(new Error('USER_NOT_FOUND'));
     }
-
-    const isMatch = await bcrypt.compare(password, user.password);
+    return Promise.all([
+      bcrypt.compare(password, user.password),
+      Promise.resolve(user),
+    ]);
+  })
+  .then(([isMatch, user]) => {
     if (!isMatch) {
-      return res.badRequest('密码错误');
+      return Promise.reject(new Error('PASSWORD_ERROR'));
     }
-
+    return user;
+  })
+  .then(user => {
     // 签发 Access Token 和 Refresh Token
-    const accessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
+    const accessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '30m' });
     const refreshToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '7d' });
 
-    res.json({ access_token: accessToken, refresh_token: refreshToken });
-  }
-  catch (err) {
-    res.serverError();
-  }
+    // 设置 refresh token 到 cookie
+    res.cookie('jwt', refreshToken, {
+      httpOnly: true,
+      secure: true,
+      sameSite: 'none',
+      maxAge: 7 * 24 * 60 * 60 * 1000 // 7天
+    });
+
+    // 返回格式与前端期望一致
+    res.sendSuccess({
+      accessToken: accessToken,
+      uid: user._id.toString(),
+      username: user.username,
+      roles: user.roles ?? ['user'],
+    });
+
+  })
+  .catch(err => {
+    Logs.errDev('登录失败:', err);
+    if (err.message === 'USER_NOT_FOUND') {
+      return res.badRequest('用户不存在');
+    }
+    if (err.message === 'PASSWORD_ERROR') {
+      return res.badRequest('密码错误');
+    }
+    res.badRequest(err.message);
+  });
+
 });
 
-// 刷新 Token
-router.post('/refresh_token', async (req, res) => {
-  const { refreshToken } = req.body;
+// 刷新 Token - 支持 Vben Admin 的 /refresh 路径
+router.post('/refresh', async (req, res) => {
+  const refreshToken = req.cookies.jwt;
 
   if (!refreshToken) {
-    return res.unauthorized('无效的 "refresh token"');
+    return res.unauthorized('无效的刷新token');
   }
-
-  try {
-    // 验证 Refresh Token
-    const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);
-    const user = await User.findById(decoded.userId);
+  const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);
+  User.findById(decoded.userId)
+  .then(user => {
     if (!user) {
-      return res.unauthorized('用户不存在');
+      return Promise.reject(new Error('USER_NOT_FOUND'));
     }
 
     // 签发新的 Access Token
-    const newAccessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
+    const newAccessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '30m' });
+
+    // 按照Vben Admin期望的格式返回
+    res.send(newAccessToken);
+  })
+  .catch(err => {
+    Logs.errDev('刷新Token失败:', err);
+    res.clearCookie('jwt');
+    if (err.message === 'USER_NOT_FOUND') {
+      return res.unauthorized('用户不存在');
+    }
+    res.unauthorized(err.message);
+  });
+});
+
 
-    res.json({ access_token: newAccessToken });
+// 用户信息 - 支持 mock 服务的 /info 路径
+router.get('/info', authMiddleware, async (req, res) => {
+  User.findById(req.userId).select('-password')
+  .then(user => {
+    if (!user) {
+      return Promise.reject(new Error('USER_NOT_FOUND'));
+    }
+
+    const userInfo = {
+      uid: user._id.toString(),
+      username: user.username,
+      roles: user.roles ?? ['user'],
+    };
+
+    res.sendSuccess(userInfo);
+
+  })
+  .catch(err => {
+    Logs.errDev('获取用户信息错误:', err);
+    if (err.message === 'USER_NOT_FOUND') {
+      return res.notFound('用户不存在');
+    }
+    res.serverError(err.message);
+  });
+});
+
+// 权限码 - 支持 mock 服务的 /codes 路径
+router.get('/codes', authMiddleware, async (req, res) => {
+  try {
+    // 返回所有功能的权限码
+    const codes = [
+      'dashboard',
+      'dashboard:analysis',
+      'dashboard:workbench',
+      'system',
+      'system:account',
+      'system:account:settings',
+      'system:role',
+      'system:menu',
+      'system:dept',
+    ];
+    res.sendSuccess(codes);
   }
   catch (err) {
-    res.unauthorized('无效或已过期的 "refresh token"');
+    Logs.errDev('获取权限码错误:', err);
+    res.serverError(err.message);
   }
 });
 
-// 受保护接口
-// router.get('/profile', authMiddleware, async (req, res) => {
-//   const user = await User.findById(req.userId).select('-password');
-//   res.json(user);
-// });
-
 module.exports = router;

+ 22 - 4
server/server.js

@@ -1,14 +1,29 @@
 const express = require('express');
 const mongoose = require('mongoose');
 const dotenv = require('dotenv');
+const Logs = require('./libs/logs');
 const userRoutes = require('./routes/user');
 const triangleRoutes = require('./routes/triangle');
+const cookieParser = require('cookie-parser');
 const app = express();
 
 dotenv.config();
 
+// 添加 CORS 支持
+app.use((req, res, next) => {
+  res.header('Access-Control-Allow-Origin', '*');
+  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
+  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+
+  if (req.method === 'OPTIONS') {
+    return res.sendStatus(200);
+  }
+  next();
+});
+
 // 中间件
 app.use(express.json({ limit: '10mb' }));
+app.use(cookieParser());
 
 app.use((req, res, next) => {
   res.badRequest = (msg) => {
@@ -17,6 +32,9 @@ app.use((req, res, next) => {
   res.unauthorized = (msg) => {
     res.status(401).json({ statusCode: 401, code: -1,  message: msg ?? 'Unauthorized' });
   }
+  res.notFound = (msg) => {
+    res.status(404).json({ statusCode: 404, code: -1,  message: msg ?? 'Not Found' });
+  }
   res.serverError = (msg) => {
     res.status(500).json({ statusCode: 500, code: -1,  message: msg ?? 'Internal Server Error' });
   }
@@ -30,15 +48,15 @@ app.use((req, res, next) => {
   next();
 });
 
-// 路由
-app.use('/api/user', userRoutes);
+app.use(['/api/user', '/api/auth'], userRoutes);
+
 app.use('/api/triangle', triangleRoutes);
 
 // 启动服务
 const PORT = process.env.PORT || 9055;
 mongoose.connect(process.env.MONGO_URI)
 .then(() => {
-  console.log('MongoDB connected');
+  Logs.out('MongoDB connected');
   app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
 })
-.catch(err => console.error(err));
+.catch(Logs.err);

+ 1 - 1
server/triangle/trangleCalc.js

@@ -2,7 +2,7 @@ const Logs = require('../libs/logs');
 const IOR_KEYS_MAP = require('./iorKeys');
 
 const GOLD_BASE = 1000;
-const WIN_MIN = process.env.NODE_ENV == 'development' ? -50 : -10;
+const WIN_MIN = process.env.NODE_ENV == 'development' ? -30 : -10;
 const JC_REBATE_RATIO = 0.07;
 
 /**

+ 1 - 1
web/apps/web-antd/.env.development

@@ -7,7 +7,7 @@ VITE_BASE=/
 VITE_GLOB_API_URL=/api
 
 # 是否开启 Nitro Mock服务,true 为开启,false 为关闭
-VITE_NITRO_MOCK=true
+VITE_NITRO_MOCK=false
 
 # 是否打开 devtools,true 为打开,false 为关闭
 VITE_DEVTOOLS=false

+ 4 - 0
web/apps/web-antd/package.json

@@ -26,6 +26,7 @@
     "#/*": "./src/*"
   },
   "dependencies": {
+    "@ant-design/icons-vue": "^7.0.1",
     "@vben/access": "workspace:*",
     "@vben/common-ui": "workspace:*",
     "@vben/constants": "workspace:*",
@@ -46,5 +47,8 @@
     "pinia": "catalog:",
     "vue": "catalog:",
     "vue-router": "catalog:"
+  },
+  "devDependencies": {
+    "sass": "catalog:"
   }
 }

+ 18 - 0
web/apps/web-antd/src/api/core/auth.ts

@@ -7,6 +7,14 @@ export namespace AuthApi {
     username?: string;
   }
 
+  /** 注册接口参数 */
+  export interface RegisterParams {
+    username: string;
+    password: string;
+    confirmPassword: string;
+    agreePolicy: boolean;
+  }
+
   /** 登录接口返回值 */
   export interface LoginResult {
     accessToken: string;
@@ -25,6 +33,16 @@ export async function loginApi(data: AuthApi.LoginParams) {
   return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
 }
 
+/**
+ * 用户注册
+ */
+export async function registerApi(data: AuthApi.RegisterParams) {
+  return requestClient.post('/user/register', {
+    username: data.username,
+    password: data.password
+  });
+}
+
 /**
  * 刷新accessToken
  */

+ 4 - 4
web/apps/web-antd/src/api/request.ts

@@ -4,7 +4,7 @@
 import type { RequestClientOptions } from '@vben/request';
 
 import { useAppConfig } from '@vben/hooks';
-import { preferences } from '@vben/preferences';
+import { overridesPreferences as preferences } from '#/preferences';
 import {
   authenticateResponseInterceptor,
   defaultResponseInterceptor,
@@ -36,7 +36,7 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
     const authStore = useAuthStore();
     accessStore.setAccessToken(null);
     if (
-      preferences.app.loginExpiredMode === 'modal' &&
+      preferences.app?.loginExpiredMode === 'modal' &&
       accessStore.isAccessChecked
     ) {
       accessStore.setLoginExpired(true);
@@ -66,7 +66,7 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
       const accessStore = useAccessStore();
 
       config.headers.Authorization = formatToken(accessStore.accessToken);
-      config.headers['Accept-Language'] = preferences.app.locale;
+      config.headers['Accept-Language'] = preferences.app?.locale;
       return config;
     },
   });
@@ -86,7 +86,7 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
       client,
       doReAuthenticate,
       doRefreshToken,
-      enableRefreshToken: preferences.app.enableRefreshToken,
+      enableRefreshToken: preferences.app?.enableRefreshToken ?? false,
       formatToken,
     }),
   );

+ 5 - 0
web/apps/web-antd/src/locales/langs/en-US/page.json

@@ -10,5 +10,10 @@
     "title": "Dashboard",
     "analytics": "Analytics",
     "workspace": "Workspace"
+  },
+  "match": {
+    "title": "Match Management",
+    "related": "Related Matches",
+    "relatedDesc": "Manage match relation information"
   }
 }

+ 5 - 0
web/apps/web-antd/src/locales/langs/zh-CN/page.json

@@ -10,5 +10,10 @@
     "title": "概览",
     "analytics": "分析页",
     "workspace": "工作台"
+  },
+  "match": {
+    "title": "比赛管理",
+    "related": "关联比赛",
+    "relatedDesc": "管理比赛关联信息"
   }
 }

+ 1 - 0
web/apps/web-antd/src/preferences.ts

@@ -9,5 +9,6 @@ export const overridesPreferences = defineOverridesPreferences({
   // overrides
   app: {
     name: import.meta.env.VITE_APP_TITLE,
+    enableRefreshToken: true,
   },
 });

+ 29 - 0
web/apps/web-antd/src/router/routes/modules/match.ts

@@ -0,0 +1,29 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+  {
+    meta: {
+      icon: 'ion:football-outline',
+      order: 3,
+      title: $t('page.match.related'),
+    },
+    name: 'Match',
+    path: '/match',
+    children: [
+      {
+        name: 'RelatedMatch',
+        path: '/related',
+        component: () => import('#/views/match/related/index.vue'),
+        meta: {
+          icon: 'ion:git-compare-outline',
+          title: $t('page.match.related'),
+          roles: ['admin'], // Only users with admin role can access this page
+        },
+      },
+    ],
+  },
+];
+
+export default routes;

+ 2 - 77
web/apps/web-antd/src/router/routes/modules/vben.ts

@@ -1,81 +1,6 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import {
-  VBEN_DOC_URL,
-  VBEN_ELE_PREVIEW_URL,
-  VBEN_GITHUB_URL,
-  VBEN_LOGO_URL,
-  VBEN_NAIVE_PREVIEW_URL,
-} from '@vben/constants';
-
-import { IFrameView } from '#/layouts';
-import { $t } from '#/locales';
-
-const routes: RouteRecordRaw[] = [
-  {
-    meta: {
-      badgeType: 'dot',
-      icon: VBEN_LOGO_URL,
-      order: 9998,
-      title: $t('demos.vben.title'),
-    },
-    name: 'VbenProject',
-    path: '/vben-admin',
-    children: [
-      {
-        name: 'VbenDocument',
-        path: '/vben-admin/document',
-        component: IFrameView,
-        meta: {
-          icon: 'lucide:book-open-text',
-          link: VBEN_DOC_URL,
-          title: $t('demos.vben.document'),
-        },
-      },
-      {
-        name: 'VbenGithub',
-        path: '/vben-admin/github',
-        component: IFrameView,
-        meta: {
-          icon: 'mdi:github',
-          link: VBEN_GITHUB_URL,
-          title: 'Github',
-        },
-      },
-      {
-        name: 'VbenNaive',
-        path: '/vben-admin/naive',
-        component: IFrameView,
-        meta: {
-          badgeType: 'dot',
-          icon: 'logos:naiveui',
-          link: VBEN_NAIVE_PREVIEW_URL,
-          title: $t('demos.vben.naive-ui'),
-        },
-      },
-      {
-        name: 'VbenElementPlus',
-        path: '/vben-admin/ele',
-        component: IFrameView,
-        meta: {
-          badgeType: 'dot',
-          icon: 'logos:element',
-          link: VBEN_ELE_PREVIEW_URL,
-          title: $t('demos.vben.element-plus'),
-        },
-      },
-    ],
-  },
-  {
-    name: 'VbenAbout',
-    path: '/vben-admin/about',
-    component: () => import('#/views/_core/about/index.vue'),
-    meta: {
-      icon: 'lucide:copyright',
-      title: $t('demos.vben.about'),
-      order: 9999,
-    },
-  },
-];
+// No routes are defined, which will effectively remove the "项目" (Project) and "关于" (About) menu items
+const routes: RouteRecordRaw[] = [];
 
 export default routes;

+ 5 - 46
web/apps/web-antd/src/views/_core/authentication/login.vue

@@ -1,6 +1,5 @@
 <script lang="ts" setup>
 import type { VbenFormSchema } from '@vben/common-ui';
-import type { BasicOption } from '@vben/types';
 
 import { computed, markRaw } from 'vue';
 
@@ -13,58 +12,13 @@ defineOptions({ name: 'Login' });
 
 const authStore = useAuthStore();
 
-const MOCK_USER_OPTIONS: BasicOption[] = [
-  {
-    label: 'Super',
-    value: 'vben',
-  },
-  {
-    label: 'Admin',
-    value: 'admin',
-  },
-  {
-    label: 'User',
-    value: 'jack',
-  },
-];
-
 const formSchema = computed((): VbenFormSchema[] => {
   return [
-    {
-      component: 'VbenSelect',
-      componentProps: {
-        options: MOCK_USER_OPTIONS,
-        placeholder: $t('authentication.selectAccount'),
-      },
-      fieldName: 'selectAccount',
-      label: $t('authentication.selectAccount'),
-      rules: z
-        .string()
-        .min(1, { message: $t('authentication.selectAccount') })
-        .optional()
-        .default('vben'),
-    },
     {
       component: 'VbenInput',
       componentProps: {
         placeholder: $t('authentication.usernameTip'),
       },
-      dependencies: {
-        trigger(values, form) {
-          if (values.selectAccount) {
-            const findUser = MOCK_USER_OPTIONS.find(
-              (item) => item.value === values.selectAccount,
-            );
-            if (findUser) {
-              form.setValues({
-                password: '123456',
-                username: findUser.value,
-              });
-            }
-          }
-        },
-        triggerFields: ['selectAccount'],
-      },
       fieldName: 'username',
       label: $t('authentication.username'),
       rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
@@ -93,6 +47,11 @@ const formSchema = computed((): VbenFormSchema[] => {
   <AuthenticationLogin
     :form-schema="formSchema"
     :loading="authStore.loginLoading"
+    :show-code-login="false"
+    :show-qrcode-login="false"
+    :show-forget-password="false"
+    :show-register="false"
+    :show-third-party-login="false"
     @submit="authStore.authLogin"
   />
 </template>

+ 40 - 3
web/apps/web-antd/src/views/_core/authentication/register.vue

@@ -1,15 +1,21 @@
 <script lang="ts" setup>
 import type { VbenFormSchema } from '@vben/common-ui';
 import type { Recordable } from '@vben/types';
+import type { AuthApi } from '#/api/core/auth';
 
 import { computed, h, ref } from 'vue';
+import { useRouter } from 'vue-router';
 
 import { AuthenticationRegister, z } from '@vben/common-ui';
 import { $t } from '@vben/locales';
+import { notification } from 'ant-design-vue';
+
+import { registerApi } from '#/api';
 
 defineOptions({ name: 'Register' });
 
 const loading = ref(false);
+const router = useRouter();
 
 const formSchema = computed((): VbenFormSchema[] => {
   return [
@@ -81,9 +87,40 @@ const formSchema = computed((): VbenFormSchema[] => {
   ];
 });
 
-function handleSubmit(value: Recordable<any>) {
-  // eslint-disable-next-line no-console
-  console.log('register submit:', value);
+async function handleSubmit(value: Recordable<any>) {
+  try {
+    loading.value = true;
+
+    // 将通用类型的表单数据转换为RegisterParams类型
+    const registerData: AuthApi.RegisterParams = {
+      username: value.username,
+      password: value.password,
+      confirmPassword: value.confirmPassword,
+      agreePolicy: value.agreePolicy
+    };
+
+    // 调用注册API
+    await registerApi(registerData);
+
+    // 注册成功提示
+    notification.success({
+      message: '注册成功',
+      description: '您已成功注册账号,请登录',
+      duration: 3,
+    });
+
+    // 跳转到登录页
+    router.push('/auth/login');
+  } catch (error: any) {
+    // 显示错误信息
+    notification.error({
+      message: '注册失败',
+      description: error?.message || '请稍后再试',
+      duration: 3,
+    });
+  } finally {
+    loading.value = false;
+  }
 }
 </script>
 

+ 67 - 0
web/apps/web-antd/src/views/match/components/game_item.vue

@@ -0,0 +1,67 @@
+<script>
+export default {
+  data() {
+    return {
+    }
+  },
+  props: {
+    eventId: {
+      type: Number
+    },
+    leagueName: {
+      type: String
+    },
+    teamHomeName: {
+      type: String
+    },
+    teamAwayName: {
+      type: String
+    },
+    dateTime: {
+      type: String
+    },
+    selected: {
+      type: Boolean
+    },
+    disabled: {
+      type: Boolean
+    },
+    autoScroll: {
+      type: Boolean
+    },
+    hideControl: {
+      type: Boolean
+    }
+  },
+  watch: {
+    selected(newVlaue) {
+      if (this.autoScroll && newVlaue) {
+        this.$refs['gameItem'].scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
+      }
+    }
+  },
+  computed: {
+  },
+  methods: {
+    hideItem() {
+      this.$emit('hide');
+    }
+  },
+  mounted() {
+  }
+}
+</script>
+
+<template>
+  <div class="game-item" :class="{ selected, disabled }" ref="gameItem">
+    <div class="league-name">{{ leagueName }}</div>
+    <div class="team-name team-home">{{ teamHomeName }}</div>
+    <div class="team-name team-away">{{ teamAwayName }}</div>
+    <div class="date-time">{{ dateTime }}</div>
+    <div class="hide-control" v-if="hideControl">
+      <button type="button" @click.stop="hideItem">隐藏</button>
+    </div>
+  </div>
+</template>
+
+<style scoped></style>

+ 100 - 0
web/apps/web-antd/src/views/match/related/index.vue

@@ -0,0 +1,100 @@
+<script setup>
+import { Page } from '@vben/common-ui';
+import { requestClient } from '#/api/request';
+import { Button, message } from 'ant-design-vue';
+import { ref, onMounted } from 'vue';
+import dayjs from 'dayjs';
+import gameItem from '../components/game_item.vue';
+
+const gamesRelations = ref([]);
+const gamesInfo = ref({});
+const gamesTime = ref({});
+const currentRelation = ref({});
+const selectedTime = ref(-1);
+const selectedKeyword = ref('');
+const selectedInfo = ref(null);
+
+async function getGamesInfo() {
+  try {
+    const data = await requestClient.get('/triangle/get_games_list');
+    return data;
+  } catch (error) {
+    console.error('Failed to fetch games info:', error);
+    message.error('获取比赛信息失败');
+    return [];
+  }
+}
+
+async function getGamesRelations() {
+  try {
+    const data = await requestClient.get('/triangle/get_games_relation');
+    return data;
+  } catch (error) {
+    console.error('Failed to fetch game relations:', error);
+    message.error('获取比赛关系失败');
+    return [];
+  }
+}
+
+async function setGamesRelation() {
+  const rel = currentRelation.value;
+  Object.values(rel).forEach(item => {
+    console.log(item);
+    delete item.orderWeight;
+  });
+  const id = rel['jc']?.eventId;
+  if (!id) {
+    return Promise.reject('没有选择竞彩的比赛');
+  }
+  try {
+    await requestClient.post('/triangle/update_games_relation', { id, rel });
+    return id;
+  } catch (error) {
+    console.error('Failed to set game relation:', error);
+    message.error('设置比赛关系失败');
+  }
+}
+
+async function removeGamesRelation(id) {
+  try {
+    await requestClient.post('/triangle/remove_games_relation', { id });
+  } catch (error) {
+    console.error('Failed to remove game relation:', error);
+    message.error('删除比赛关系失败');
+  }
+}
+
+function updateGamesInfo() {
+  getGamesInfo().then((data) => {
+    gamesInfo.value = data;
+  });
+}
+
+function updateGamesRelations() {
+  getGamesRelations().then((data) => {
+    gamesRelations.value = data;
+  });
+}
+
+onMounted(() => {
+  updateGamesInfo();
+  updateGamesRelations();
+});
+
+
+</script>
+
+<template>
+  <Page>
+    <div class="match-related-container">
+      <div class="match-related-header">
+        <div class="match-related-header-title">
+          <span>比赛关系</span>
+        </div>
+      </div>
+    </div>
+  </Page>
+</template>
+
+<style lang="scss" scoped>
+</style>

+ 7 - 2
web/apps/web-antd/vite.config.mts

@@ -1,16 +1,21 @@
 import { defineConfig } from '@vben/vite-config';
+import { resolve } from 'path';
 
 export default defineConfig(async () => {
   return {
     application: {},
     vite: {
+      resolve: {
+        alias: {
+          '@ant-design/icons-vue': resolve(__dirname, 'node_modules/@ant-design/icons-vue')
+        }
+      },
       server: {
         proxy: {
           '/api': {
             changeOrigin: true,
             rewrite: (path) => path.replace(/^\/api/, ''),
-            // mock代理目标地址
-            target: 'http://localhost:5320/api',
+            target: 'http://localhost:9055/api',
             ws: true,
           },
         },

+ 7 - 39
web/pnpm-lock.yaml

@@ -27,9 +27,6 @@ catalogs:
     '@eslint/js':
       specifier: ^9.26.0
       version: 9.26.0
-    '@faker-js/faker':
-      specifier: ^9.7.0
-      version: 9.7.0
     '@iconify/json':
       specifier: ^2.2.334
       version: 2.2.334
@@ -51,9 +48,6 @@ catalogs:
     '@manypkg/get-packages':
       specifier: ^3.0.0
       version: 3.0.0
-    '@nolebase/vitepress-plugin-git-changelog':
-      specifier: ^2.17.0
-      version: 2.17.0
     '@playwright/test':
       specifier: ^1.52.0
       version: 1.52.0
@@ -69,9 +63,6 @@ catalogs:
     '@tailwindcss/typography':
       specifier: ^0.5.16
       version: 0.5.16
-    '@tanstack/vue-query':
-      specifier: ^5.75.1
-      version: 5.75.1
     '@tanstack/vue-store':
       specifier: ^0.7.0
       version: 0.7.0
@@ -84,9 +75,6 @@ catalogs:
     '@types/html-minifier-terser':
       specifier: ^7.0.2
       version: 7.0.2
-    '@types/jsonwebtoken':
-      specifier: ^9.0.9
-      version: 9.0.9
     '@types/lodash.clonedeep':
       specifier: ^4.5.9
       version: 4.5.9
@@ -126,9 +114,6 @@ catalogs:
     '@vee-validate/zod':
       specifier: ^4.15.0
       version: 4.15.0
-    '@vite-pwa/vitepress':
-      specifier: ^1.0.0
-      version: 1.0.0
     '@vitejs/plugin-vue':
       specifier: ^5.2.3
       version: 5.2.3
@@ -216,9 +201,6 @@ catalogs:
     echarts:
       specifier: ^5.6.0
       version: 5.6.0
-    element-plus:
-      specifier: ^2.9.9
-      version: 2.9.9
     eslint:
       specifier: ^9.26.0
       version: 9.26.0
@@ -279,9 +261,6 @@ catalogs:
     globals:
       specifier: ^16.0.0
       version: 16.0.0
-    h3:
-      specifier: ^1.15.3
-      version: 1.15.3
     happy-dom:
       specifier: ^17.4.6
       version: 17.4.6
@@ -294,9 +273,6 @@ catalogs:
     jsonc-eslint-parser:
       specifier: ^2.4.0
       version: 2.4.0
-    jsonwebtoken:
-      specifier: ^9.0.2
-      version: 9.0.2
     lefthook:
       specifier: ^1.11.12
       version: 1.11.12
@@ -315,12 +291,6 @@ catalogs:
     lucide-vue-next:
       specifier: ^0.507.0
       version: 0.507.0
-    medium-zoom:
-      specifier: ^1.1.0
-      version: 1.1.0
-    naive-ui:
-      specifier: ^2.41.0
-      version: 2.41.0
     nitropack:
       specifier: ^2.11.11
       version: 2.11.11
@@ -447,9 +417,6 @@ catalogs:
     unbuild:
       specifier: ^3.5.0
       version: 3.5.0
-    unplugin-element-plus:
-      specifier: ^0.10.0
-      version: 0.10.0
     vee-validate:
       specifier: ^4.15.0
       version: 4.15.0
@@ -474,12 +441,6 @@ catalogs:
     vite-plugin-vue-devtools:
       specifier: ^7.7.6
       version: 7.7.6
-    vitepress:
-      specifier: ^1.6.3
-      version: 1.6.3
-    vitepress-plugin-group-icons:
-      specifier: ^1.5.2
-      version: 1.5.2
     vitest:
       specifier: ^3.1.2
       version: 3.1.2
@@ -647,6 +608,9 @@ importers:
 
   apps/web-antd:
     dependencies:
+      '@ant-design/icons-vue':
+        specifier: ^7.0.1
+        version: 7.0.1(vue@3.5.13(typescript@5.8.3))
       '@vben/access':
         specifier: workspace:*
         version: link:../../packages/effects/access
@@ -707,6 +671,10 @@ importers:
       vue-router:
         specifier: 'catalog:'
         version: 4.5.1(vue@3.5.13(typescript@5.8.3))
+    devDependencies:
+      sass:
+        specifier: 'catalog:'
+        version: 1.87.0
 
   apps/web-ele:
     dependencies: