Просмотр исходного кода

Merge branch 'dev'

# Conflicts:
#	src/api/system/common/nav/gameNavigator.ts
#	src/views/system/common/nav/components/GameNavigator.vue
wenkai 9 месяцев назад
Родитель
Сommit
1fc8034afe
39 измененных файлов с 4188 добавлено и 1189 удалено
  1. 1 0
      .gitignore
  2. 10 0
      prompt.txt
  3. 63 0
      src/api/system/advice/index.ts
  4. 70 0
      src/api/system/advice/types.ts
  5. 30 0
      src/api/system/backup/index.ts
  6. 55 3
      src/api/system/common/nav/gameNavigator.ts
  7. 48 0
      src/api/system/eventMd/index.ts
  8. 99 0
      src/api/system/eventMd/types.ts
  9. 2 0
      src/api/system/gameAthlete/types.ts
  10. 88 1
      src/api/system/gameEvent/index.ts
  11. 119 0
      src/api/system/gameEventGroup/types.ts
  12. 6 6
      src/api/system/gameEventProject/types.ts
  13. 2 0
      src/api/system/gameReferee/types.ts
  14. 61 0
      src/api/system/gameScore/index.ts
  15. 14 4
      src/api/system/gameScore/types.ts
  16. 16 0
      src/api/system/gameTeam/index.ts
  17. 2 0
      src/api/system/gameTeam/types.ts
  18. 63 0
      src/api/system/relateMenu/index.ts
  19. 46 0
      src/api/system/relateMenu/types.ts
  20. 62 2
      src/layout/components/Navbar.vue
  21. 14 1
      src/router/index.ts
  22. 34 0
      src/store/modules/gameEvent.ts
  23. 1 0
      src/utils/request.ts
  24. 317 9
      src/views/index.vue
  25. 1 9
      src/views/system/activityIntroduction/index.vue
  26. 193 0
      src/views/system/advice/index.vue
  27. 195 48
      src/views/system/common/nav/components/GameNavigator.vue
  28. 141 54
      src/views/system/gameAthlete/index.vue
  29. 190 458
      src/views/system/gameEvent/edit.vue
  30. 903 81
      src/views/system/gameEvent/index.vue
  31. 6 6
      src/views/system/gameEventConfig/index.vue
  32. 450 0
      src/views/system/gameEventGroup/detail.vue
  33. 337 77
      src/views/system/gameEventGroup/index.vue
  34. 24 14
      src/views/system/gameEventProject/index.vue
  35. 152 202
      src/views/system/gameEventSchedule/index.vue
  36. 42 40
      src/views/system/gameReferee/index.vue
  37. 285 146
      src/views/system/gameScore/gameScoreEdit.vue
  38. 29 12
      src/views/system/gameScore/index.vue
  39. 17 16
      src/views/system/gameTeam/index.vue

+ 1 - 0
.gitignore

@@ -12,6 +12,7 @@ tests/e2e/reports
 selenium-debug.log
 
 # Editor directories and files
+.md
 .idea
 .vscode
 *.suo

+ 10 - 0
prompt.txt

@@ -0,0 +1,10 @@
+根据src/api/system/gameEvent/index.ts/generateBib方法
+
+在views/system/gameEvent/index.vue进行以下操作:
+
+1.增加一个按钮,点击弹出一个窗口
+2.其中可以上传一个背景图和一个logo 并且可以移动logo的位置
+3.其中还有一个示例条形码,可以拖到指定的位置
+4.图片正中间有一个指定数字1234,可以为他设置字体 大小 颜色
+
+点击生成按钮 调用generateBib方法

+ 63 - 0
src/api/system/advice/index.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { AdviceVO, AdviceForm, AdviceQuery } from '@/api/system/advice/types';
+
+/**
+ * 查询用户反馈建议列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listAdvice = (query?: AdviceQuery): AxiosPromise<AdviceVO[]> => {
+  return request({
+    url: '/system/advice/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询用户反馈建议详细
+ * @param id
+ */
+export const getAdvice = (id: string | number): AxiosPromise<AdviceVO> => {
+  return request({
+    url: '/system/advice/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增用户反馈建议
+ * @param data
+ */
+export const addAdvice = (data: AdviceForm) => {
+  return request({
+    url: '/system/advice',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改用户反馈建议
+ * @param data
+ */
+export const updateAdvice = (data: AdviceForm) => {
+  return request({
+    url: '/system/advice',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除用户反馈建议
+ * @param id
+ */
+export const delAdvice = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/system/advice/' + id,
+    method: 'delete'
+  });
+};

+ 70 - 0
src/api/system/advice/types.ts

@@ -0,0 +1,70 @@
+export interface AdviceVO {
+  /**
+   *
+   */
+  id: string | number;
+
+  /**
+   * 姓名
+   */
+  name: string;
+
+  /**
+   * 队伍id
+   */
+  teamId: string | number;
+
+  /**
+   * 反馈内容
+   */
+  content: string;
+}
+
+export interface AdviceForm extends BaseEntity {
+  /**
+   *
+   */
+  id?: string | number;
+
+  /**
+   * 姓名
+   */
+  name?: string;
+
+  /**
+   * 队伍id
+   */
+  teamId?: string | number;
+
+  /**
+   * 队伍名称
+   */
+  teamName?: string;
+
+  /**
+   * 反馈内容
+   */
+  content?: string;
+}
+
+export interface AdviceQuery extends PageQuery {
+  /**
+   * 姓名
+   */
+  name?: string;
+
+  /**
+   * 队伍名称
+   */
+  teamName?: string;
+
+  /**
+   * 反馈内容
+   */
+  content?: string;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}

+ 30 - 0
src/api/system/backup/index.ts

@@ -0,0 +1,30 @@
+import request from '@/utils/request';
+
+/**
+ * 备份数据库 - 下载SQL文件
+ */
+export function backupDatabase() {
+  return request({
+    url: '/system/backup/backup',
+    method: 'get',
+    responseType: 'blob'
+  });
+}
+
+/**
+ * 恢复数据库 - 上传SQL文件
+ * @param file SQL文件
+ */
+export function restoreDatabase(file: File) {
+  const formData = new FormData();
+  formData.append('file', file);
+
+  return request({
+    url: '/system/backup/restore',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+}

+ 55 - 3
src/api/system/common/nav/gameNavigator.ts

@@ -33,6 +33,34 @@ export interface GameNavigatorBo {
   sortNum?: number;
   status?: number;
   remark?: string;
+  navId?: number;
+  name?: string;
+  pic?: string;
+  color?: string;
+  jumpType?: number;
+  jumpPath?: string;
+  activityType?: number;
+  appId?: string;
+  sortNum?: number;
+  status?: number;
+  remark?: string;
+}
+
+// 赛事关联菜单查询参数
+export interface RelateMenuQuery {
+  eventId?: string | number;
+}
+
+// 编辑赛事关联菜单参数
+export interface EditRelateMenuParams {
+  /**
+   * 赛事id
+   */
+  eventId: string | number;
+  /**
+   * 菜单列表 - 菜单的navId数组
+   */
+  menus: number[];
 }
 
 // 查询底部主导航列表
@@ -80,7 +108,7 @@ export function updateNavigator(data: GameNavigatorBo) {
     url: '/system/scenic/navigator',
     method: 'put',
     data: data
-  });
+  })
 }
 
 // 删除底部主导航
@@ -88,7 +116,7 @@ export function delNavigator(navId: number | Array<number>) {
   return request<ApiResult<void>>({
     url: `/system/scenic/navigator/${navId}`,
     method: 'delete'
-  });
+  })
 }
 
 // 导出底部主导航
@@ -97,7 +125,7 @@ export function exportNavigator(query: GameNavigatorBo) {
     url: '/system/scenic/navigator/export',
     method: 'post',
     data: query
-  });
+  })
 }
 
 // 金刚区(type=4)固定类型数据
@@ -107,3 +135,27 @@ export function listKingKongNav() {
     method: 'get'
   });
 }
+
+// 查询赛事关联菜单
+export function listRelateMenu(query: RelateMenuQuery & PageQuery) {
+  return request<
+    ApiResult<{
+      rows: GameNavigatorVo[];
+      total: number;
+    }>
+  >({
+    url: '/system/scenic/navigator/list/relate',
+    method: 'get',
+    params: query
+  });
+}
+
+// 编辑赛事关联菜单
+export function editRelate(data: EditRelateMenuParams) {
+  return request({
+    url: '/system/scenic/navigator/edit/relate',
+    method: 'post',
+    data: data
+  });
+  });
+}

+ 48 - 0
src/api/system/eventMd/index.ts

@@ -0,0 +1,48 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { EventMdVO, EventMdForm } from '@/api/system/eventMd/types';
+
+/**
+ * 查询移动端富文本详细
+ * @param id
+ */
+export const getEventMd = (id: string | number): AxiosPromise<EventMdVO> => {
+  return request({
+    url: '/system/markdown/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 根据赛事id和类型获取移动端富文本详细信息
+ * @param id
+ */
+export const getEventMdByEventAndType = (eventId: string | number, type: number): AxiosPromise<EventMdVO> => {
+  return request({
+    url: `/system/markdown/${eventId}/${type}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 编辑移动端富文本
+ * @param data
+ */
+export const editEventMd = (data: EventMdForm) => {
+  return request({
+    url: '/system/markdown',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 删除移动端富文本
+ * @param id
+ */
+export const delEventMd = (eventId: string | number, type: number) => {
+  return request({
+    url: `/system/markdown/${eventId}/${type}`,
+    method: 'delete'
+  });
+};

+ 99 - 0
src/api/system/eventMd/types.ts

@@ -0,0 +1,99 @@
+export interface EventMdVO {
+  /**
+   * 
+   */
+  id: string | number;
+
+  /**
+   * 赛事id
+   */
+  eventId: string | number;
+
+  /**
+   * 标题
+   */
+  title: string;
+
+  /**
+   * 内容
+   */
+  content: string;
+
+  /**
+   * 类型 1:竞赛流程 2:竞赛项目 3:活动议程 4:项目介绍 5:竞赛流程
+  6:赛事分组 7:运动员号码簿 8:项目场地
+   */
+  type: number;
+
+  /**
+   * 备注
+   */
+  remark: string;
+
+}
+
+export interface EventMdForm extends BaseEntity {
+  /**
+   * 
+   */
+  id?: string | number;
+
+  /**
+   * 赛事id
+   */
+  eventId?: string | number;
+
+  /**
+   * 标题
+   */
+  title?: string;
+
+  /**
+   * 内容
+   */
+  content?: string;
+
+  /**
+   * 类型 1:竞赛流程 2:竞赛项目 3:活动议程 4:项目介绍 5:竞赛流程
+  6:赛事分组 7:运动员号码簿 8:项目场地
+   */
+  type?: number;
+
+  /**
+   * 备注
+   */
+  remark?: string;
+
+}
+
+export interface EventMdQuery extends PageQuery {
+
+  /**
+   * 赛事id
+   */
+  eventId?: string | number;
+
+  /**
+   * 标题
+   */
+  title?: string;
+
+  /**
+   * 内容
+   */
+  content?: string;
+
+  /**
+   * 类型 1:竞赛流程 2:竞赛项目 3:活动议程 4:项目介绍 5:竞赛流程
+  6:赛事分组 7:运动员号码簿 8:项目场地
+   */
+  type?: number;
+
+    /**
+     * 日期范围参数
+     */
+    params?: any;
+}
+
+
+

+ 2 - 0
src/api/system/gameAthlete/types.ts

@@ -73,6 +73,7 @@ export interface GameAthleteVO {
    * 参与项目列表
    */
   projectValue: string;
+  projectList: String[];
 
   /**
    * 状态(0正常 1停用)
@@ -165,6 +166,7 @@ export interface GameAthleteForm extends BaseEntity {
    * 参与项目列表
    */
   projectValue?: string;
+  projectList: String[];
 
   /**
    * 选中的项目列表(用于穿梭框)

+ 88 - 1
src/api/system/gameEvent/index.ts

@@ -92,7 +92,7 @@ export function getDefaultEvent() {
 export function changeEventDefault(eventId: string | number, isDefault: string) {
   const data = {
     eventId,
-    isDefault,
+    isDefault
     // eventName
   };
   return request({
@@ -113,3 +113,90 @@ export const getEventCount = (type: number) => {
     method: 'get'
   });
 };
+
+/**
+ * 生成号码对照表
+ * @param query
+ * @returns {*}
+ */
+
+export const generateNumberTable = () => {
+  return request({
+    url: '/system/number/generateTable',
+    method: 'get'
+  });
+};
+
+/**
+ * 生成号码布(直接下载 ZIP)
+ *
+ * @param bgImage - 背景图文件 (File)
+ * @param logo - Logo 文件 (File, 可选)
+ * @param bibParam - 布局参数
+ * @returns {AxiosPromise} 返回 Promise,但实际不解析数据,用于触发下载
+ */
+export const generateBib = (bgImage: File, logo: File | null, bibParam: GenerateBibBo): AxiosPromise<void> => {
+  const formData = new FormData();
+
+  // 添加文件
+  formData.append('bgImage', bgImage);
+  if (logo) {
+    formData.append('logo', logo);
+  }
+
+  // 添加 bibParam 对象(作为 Blob 以确保正确的 MIME 类型)
+  formData.append(
+    'bibParam',
+    new Blob([JSON.stringify(bibParam)], {
+      type: 'application/json'
+    })
+  );
+
+  // 同时也添加单独的参数作为备选方案(如果后端需要)
+  if (bibParam.logoX !== undefined) {
+    formData.append('logoX', bibParam.logoX.toString());
+  }
+  if (bibParam.logoY !== undefined) {
+    formData.append('logoY', bibParam.logoY.toString());
+  }
+  if (bibParam.qRCodeX !== undefined) {
+    formData.append('qRCodeX', bibParam.qRCodeX.toString());
+  }
+  if (bibParam.qRCodeY !== undefined) {
+    formData.append('qRCodeY', bibParam.qRCodeY.toString());
+  }
+  if (bibParam.fontName) {
+    formData.append('fontName', bibParam.fontName);
+  }
+  if (bibParam.fontSize !== undefined) {
+    formData.append('fontSize', bibParam.fontSize.toString());
+  }
+  if (bibParam.fontColor !== undefined) {
+    formData.append('fontColor', bibParam.fontColor.toString());
+  }
+  console.log('formData', [...formData.entries()]);
+  return request({
+    url: '/system/number/generateBib',
+    method: 'post',
+    data: formData,
+    headers: {
+      'repeatSubmit': false
+    },
+    // 必须设置 responseType 为 'blob' 才能触发文件下载
+    responseType: 'blob'
+  });
+};
+
+// -------------------------------
+// 类型定义(放在 types.ts 中)
+// -------------------------------
+
+export interface GenerateBibBo {
+  logoX?: number; // Logo X 坐标
+  logoY?: number; // Logo Y 坐标
+  qRCodeX?: number; // 二维码 X 坐标
+  qRCodeY?: number; // 二维码 Y 坐标
+  fontName?: string; // 字体名称,如 "simhei"
+  fontSize?: number; // 字体大小
+  fontColor?: number; // 字体颜色,如 0xFF0000 (红色)
+}

+ 119 - 0
src/api/system/gameEventGroup/types.ts

@@ -45,6 +45,46 @@ export interface GameEventGroupVO {
    */
   remark: string;
 
+  /**
+   * 包含组数
+   */
+  includeGroupNum: number;
+
+  /**
+   * 项目id
+   */
+  projectId: string | number;
+
+  /**
+   * 人数/组别
+   */
+  personNum: number;
+
+  /**
+   * 组别比赛开始时间
+   */
+  beginTime: string;
+
+  /**
+   * 组别比赛结束时间
+   */
+  endTime: string;
+
+  /**
+   * 道数
+   */
+  trackNum: number;
+
+  /**
+   * 场地数量
+   */
+  fieldNum: number;
+
+  /**
+   * 每组用时
+   */
+  duration: number;
+
 }
 
 export interface GameEventGroupForm extends BaseEntity {
@@ -98,6 +138,45 @@ export interface GameEventGroupForm extends BaseEntity {
    */
   remark?: string;
 
+  /**
+   * 包含组数
+   */
+  includeGroupNum?: number;
+
+  /**
+   * 项目id
+   */
+  projectId?: string | number;
+
+  /**
+   * 人数/组别
+   */
+  personNum?: number;
+
+  /**
+   * 组别比赛开始时间
+   */
+  beginTime?: string;
+
+  /**
+   * 组别比赛结束时间
+   */
+  endTime?: string;
+
+  /**
+   * 道数
+   */
+  trackNum?: number;
+
+  /**
+   * 场地数量
+   */
+  fieldNum?: number;
+
+  /**
+   * 每组用时
+   */
+  duration?: number;
 }
 
 export interface GameEventGroupQuery extends PageQuery {
@@ -137,6 +216,46 @@ export interface GameEventGroupQuery extends PageQuery {
    */
   status?: string;
 
+  /**
+   * 包含组数
+   */
+  includeGroupNum?: number;
+
+  /**
+   * 项目id
+   */
+  projectId?: string | number;
+
+  /**
+   * 人数/组别
+   */
+  personNum?: number;
+
+  /**
+   * 组别比赛开始时间
+   */
+  beginTime?: string;
+
+  /**
+   * 组别比赛结束时间
+   */
+  endTime?: string;
+
+  /**
+   * 道数
+   */
+  trackNum?: number;
+
+  /**
+   * 场地数量
+   */
+  fieldNum?: number;
+
+  /**
+   * 每组用时
+   */
+  duration?: number;
+
   /**
    * 日期范围参数
    */

+ 6 - 6
src/api/system/gameEventProject/types.ts

@@ -19,9 +19,9 @@ export interface GameEventProjectVO {
   projectType: string;
 
   /**
-   * 项目组别
+   * 归类(0个人项目/1团体项目)
    */
-  groupType: string;
+  classification: string;
 
   /**
    * 裁判组员
@@ -125,9 +125,9 @@ export interface GameEventProjectForm extends BaseEntity {
   projectType?: string;
 
   /**
-   * 项目组别
+   * 归类(0个人项目/1团体项目)
    */
-  groupType?: string;
+  classification?: string;
 
   /**
    * 裁判组员
@@ -237,9 +237,9 @@ export interface GameEventProjectQuery extends PageQuery {
   projectType?: string;
 
   /**
-   * 项目组别
+   * 归类(0个人项目/1团体项目)
    */
-  groupType?: string;
+  classification?: string;
 
   /**
    * 开始时间

+ 2 - 0
src/api/system/gameReferee/types.ts

@@ -33,6 +33,7 @@ export interface GameRefereeVO {
    * 负责的项目
    */
   projectList: string;
+  projectList2: string[];
 
   /**
    * 裁判码
@@ -86,6 +87,7 @@ export interface GameRefereeForm extends BaseEntity {
    * 负责的项目
    */
   projectList?: string;
+  projectList2?: string[];
 
   /**
    * 裁判码

+ 61 - 0
src/api/system/gameScore/index.ts

@@ -27,6 +27,19 @@ export const getGameScore = (scoreId: string | number): AxiosPromise<GameScoreVO
   });
 };
 
+/**
+ * 根据运动员ID和项目ID查询成绩
+ * @param athleteId
+ * @param projectId
+ */
+export const getScoreByAthleteIdAndProjectId = (athleteId: string | number, projectId: string | number): AxiosPromise<GameScoreVO> => {
+  return request({
+    url: '/system/gameScore/getScoreByAthleteIdAndProjectId',
+    method: 'get',
+    params: { athleteId, projectId }
+  });
+};
+
 /**
  * 查询项目成绩详细
  * @param projectId
@@ -72,3 +85,51 @@ export const delGameScore = (scoreId: string | number | Array<string | number>)
     method: 'delete'
   });
 };
+
+/**
+ * 获取项目成绩综合数据(包含运动员/队伍信息)
+ * @param eventId 赛事ID
+ * @param projectId 项目ID
+ * @param classification 项目分类
+ * @param searchValue 搜索值
+ * @param pageQuery 分页参数
+ */
+export const getProjectScoreData = (params: {
+  eventId: string | number;
+  projectId: string | number;
+  classification: string;
+  searchValue?: string;
+  pageNum?: number;
+  pageSize?: number;
+}) => {
+  return request({
+    url: '/system/gameScore/getProjectScoreData',
+    method: 'get',
+    params
+  });
+};
+
+/**
+ * 更新成绩并重新计算排名积分
+ * @param data 成绩数据
+ */
+export const updateScoreAndRecalculate = (data: GameScoreForm) => {
+  return request({
+    url: '/system/gameScore/updateScoreAndRecalculate',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 重新计算项目排名和积分
+ * @param eventId 赛事ID
+ * @param projectId 项目ID
+ */
+export const recalculateRankingsAndPoints = (eventId: string | number, projectId: string | number) => {
+  return request({
+    url: '/system/gameScore/recalculateRankingsAndPoints',
+    method: 'post',
+    params: { eventId, projectId }
+  });
+};

+ 14 - 4
src/api/system/gameScore/types.ts

@@ -24,9 +24,14 @@ export interface GameScoreVO {
   teamId: string | number;
 
   /**
-   * 成绩
+   * 个人成绩
    */
-  scoreValue: number;
+  individualPerformance: number;
+
+  /**
+   * 团队成绩
+   */
+  teamPerformance: number;
 
   /**
    * 成绩类型
@@ -107,9 +112,14 @@ export interface GameScoreForm extends BaseEntity {
   teamId?: string | number;
 
   /**
-   * 成绩值
+   * 个人成绩
+   */
+  individualPerformance?: number;
+
+  /**
+   * 团队成绩
    */
-  scoreValue?: number;
+  teamPerformance?: number;
 
   /**
    * 成绩类型

+ 16 - 0
src/api/system/gameTeam/index.ts

@@ -72,3 +72,19 @@ export const getTeamCount = () => {
     method: 'get'
   });
 };
+
+/**
+ * 更新队伍中的运动员列表
+ * @param teamId 队伍ID
+ * @param athleteIds 运动员ID列表
+ */
+export const updateTeamAthletes = (teamId: string | number, athleteIds: Array<string | number>) => {
+  return request({
+    url: '/system/gameTeam/updateAthletes',
+    method: 'put',
+    data: {
+      teamId,
+      athleteIds
+    }
+  });
+};

+ 2 - 0
src/api/system/gameTeam/types.ts

@@ -32,6 +32,7 @@ export interface GameTeamVO {
    * 队员列表
    */
   athleteValue: string;
+  athleteList: String[];
 
   /**
    * 参与项目列表
@@ -93,6 +94,7 @@ export interface GameTeamForm extends BaseEntity {
    * 队员列表
    */
   athleteValue?: string;
+  athleteList: String[];
 
   /**
    * 参与项目列表

+ 63 - 0
src/api/system/relateMenu/index.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { MenuVO, MenuForm, MenuQuery } from '@/api/system/menu/types';
+
+/**
+ * 查询赛事菜单关联列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listMenu = (query?: MenuQuery): AxiosPromise<MenuVO[]> => {
+  return request({
+    url: '/system/menu/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询赛事菜单关联详细
+ * @param id
+ */
+export const getMenu = (id: string | number): AxiosPromise<MenuVO> => {
+  return request({
+    url: '/system/menu/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增赛事菜单关联
+ * @param data
+ */
+export const addMenu = (data: MenuForm) => {
+  return request({
+    url: '/system/menu',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改赛事菜单关联
+ * @param data
+ */
+export const updateMenu = (data: MenuForm) => {
+  return request({
+    url: '/system/menu',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除赛事菜单关联
+ * @param id
+ */
+export const delMenu = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/system/menu/' + id,
+    method: 'delete'
+  });
+};

+ 46 - 0
src/api/system/relateMenu/types.ts

@@ -0,0 +1,46 @@
+export interface MenuVO {
+  /**
+   * 
+   */
+  id: string | number;
+
+  /**
+   * 赛事id
+   */
+  eventId: string | number;
+
+  /**
+   * 菜单列表
+   */
+  menuList: string;
+
+}
+
+export interface MenuForm extends BaseEntity {
+  /**
+   * 
+   */
+  id?: string | number;
+
+  /**
+   * 赛事id
+   */
+  eventId?: string | number;
+
+  /**
+   * 菜单列表
+   */
+  menuList?: string;
+
+}
+
+export interface MenuQuery extends PageQuery {
+
+    /**
+     * 日期范围参数
+     */
+    params?: any;
+}
+
+
+

+ 62 - 2
src/layout/components/Navbar.vue

@@ -4,6 +4,16 @@
     <breadcrumb v-if="!settingsStore.topNav" id="breadcrumb-container" class="breadcrumb-container" />
     <top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />
 
+    <!-- 当前赛事信息显示 -->
+    <div class="current-event-info">
+      <el-icon class="event-icon"><Trophy /></el-icon>
+      <span class="event-label">当前默认赛事:</span>
+      <span class="event-name">{{ defaultEventInfo?.eventName || '暂无指定默认赛事' }}</span>
+      <el-tag v-if="defaultEventInfo?.eventCode" type="success" size="small" class="event-code">
+        {{ defaultEventInfo.eventCode }}
+      </el-tag>
+    </div>
+
     <div class="right-menu flex align-center">
       <template v-if="appStore.device !== 'mobile'">
         <el-select
@@ -99,6 +109,9 @@ import { TenantVO } from '@/api/types';
 import notice from './notice/index.vue';
 import router from '@/router';
 import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
+import { Trophy } from '@element-plus/icons-vue';
+import { useGameEventStore } from '@/store/modules/gameEvent';
+import { storeToRefs } from 'pinia';
 
 const appStore = useAppStore();
 const userStore = useUserStore();
@@ -118,6 +131,10 @@ const tenantEnabled = ref(true);
 // 搜索菜单
 const searchMenuRef = ref<InstanceType<typeof SearchMenu>>();
 
+// 使用gameEvent store
+const gameEventStore = useGameEventStore();
+const { defaultEventInfo } = storeToRefs(gameEventStore);
+
 const openSearchMenu = () => {
   searchMenuRef.value?.openSearch();
 };
@@ -127,7 +144,7 @@ const dynamicTenantEvent = async (tenantId: string) => {
   if (companyName.value != null && companyName.value !== '') {
     await dynamicTenant(tenantId);
     dynamic.value = true;
-    await proxy?.$router.push('/');
+    await router.push('/');
     await proxy?.$tab.closeAllPage();
     await proxy?.$tab.refreshPage();
   }
@@ -136,7 +153,7 @@ const dynamicTenantEvent = async (tenantId: string) => {
 const dynamicClearEvent = async () => {
   await dynamicClear();
   dynamic.value = false;
-  await proxy?.$router.push('/');
+  await router.push('/');
   await proxy?.$tab.closeAllPage();
   await proxy?.$tab.refreshPage();
 };
@@ -148,6 +165,8 @@ const initTenantList = async () => {
   if (tenantEnabled.value) {
     tenantList.value = data.voList;
   }
+  // 获取默认赛事信息
+  await gameEventStore.fetchDefaultEvent();
 };
 
 defineExpose({
@@ -246,6 +265,47 @@ watch(
     left: 50px;
   }
 
+  .current-event-info {
+    position: absolute;
+    left: 50%;
+    transform: translateX(-50%);
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 0 16px;
+    height: 100%;
+    background: rgba(64, 158, 255, 0.1);
+    border-radius: 4px;
+    border: 1px solid rgba(64, 158, 255, 0.2);
+    
+    .event-icon {
+      font-size: 16px;
+      color: #409eff;
+    }
+    
+    .event-label {
+      font-size: 14px;
+      color: #606266;
+      font-weight: 500;
+    }
+    
+    .event-name {
+      font-size: 14px;
+      color: #409eff;
+      font-weight: 600;
+      max-width: 200px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    
+    .event-code {
+      font-size: 12px;
+      height: 20px;
+      line-height: 18px;
+    }
+  }
+
   .errLog-container {
     display: inline-block;
     vertical-align: top;

+ 14 - 1
src/router/index.ts

@@ -138,7 +138,7 @@ export const constantRoutes: RouteRecordRaw[] = [
     hidden: true,
     children: [
       {
-        path: 'edit/:projectId/:projectName/:groupType/:eventId',
+        path: 'edit/:projectId/:projectName/:projectType/:eventId/:classification',
         component: () => import('@/views/system/gameScore/gameScoreEdit.vue'),
         name: 'GameScoreEdit',
         meta: { title: '修改成绩', icon: 'form' }
@@ -157,6 +157,19 @@ export const constantRoutes: RouteRecordRaw[] = [
         meta: { title: '日程管理', icon: 'form' }
       },
     ]
+  },
+  {
+    path: '/system/gameEventGroup',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: 'detail',
+        component: () => import('@/views/system/gameEventGroup/detail.vue'),
+        name: 'GameEventGroupDetail',
+        meta: { title: '分组详情', icon: 'view' }
+      }
+    ]
   }
 ];
 

+ 34 - 0
src/store/modules/gameEvent.ts

@@ -0,0 +1,34 @@
+import { defineStore } from 'pinia';
+import { getDefaultEvent } from '@/api/system/gameEvent';
+import { GameEventVO } from '@/api/system/gameEvent/types';
+
+export const useGameEventStore = defineStore('gameEvent', {
+  state: () => ({
+    defaultEventInfo: null as GameEventVO | null,
+  }),
+  actions: {
+    /**
+     * 从API获取默认赛事信息并更新store
+     */
+    async fetchDefaultEvent() {
+      try {
+        const res = await getDefaultEvent();
+        if (res.code === 200 && res.data) {
+          this.defaultEventInfo = res.data;
+        } else {
+          this.defaultEventInfo = null;
+        }
+      } catch (error) {
+        console.error('获取默认赛事信息失败:', error);
+        this.defaultEventInfo = null;
+      }
+    },
+    /**
+     * 直接更新store中的默认赛事信息
+     * @param eventInfo 新的默认赛事信息
+     */
+    updateDefaultEvent(eventInfo: GameEventVO | null) {
+      this.defaultEventInfo = eventInfo;
+    },
+  },
+}); 

+ 1 - 0
src/utils/request.ts

@@ -29,6 +29,7 @@ axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID;
 const service = axios.create({
   baseURL: import.meta.env.VITE_APP_BASE_API,
   // baseURL: 'http://meet2.sportsrobo.club:8080',
+  // baseURL: 'http://localhost:8080',
   timeout: 50000
 });
 

+ 317 - 9
src/views/index.vue

@@ -204,13 +204,87 @@
         </el-card>
       </el-col>
     </el-row>
+
+    <!-- 还原赛事弹窗 -->
+    <el-dialog v-model="restoreDialogVisible" title="还原赛事" width="500px" :before-close="handleCloseRestoreDialog">
+      <div class="restore-dialog-content">
+        <div class="restore-description">
+          <el-icon class="description-icon">
+            <InfoFilled />
+          </el-icon>
+          <p>请选择您需要的操作:</p>
+        </div>
+
+        <div class="restore-actions">
+          <div class="action-card backup-card" @click="handleBackup">
+            <div class="action-icon">
+              <el-icon :size="32">
+                <FolderAdd />
+              </el-icon>
+            </div>
+            <div class="action-content">
+              <h3>备份数据库</h3>
+              <p>将当前数据库备份为SQL文件下载到本地</p>
+            </div>
+            <el-button v-if="backupLoading" type="primary" loading size="small"> 备份中...</el-button>
+          </div>
+
+          <div class="action-card restore-card">
+            <div class="action-icon">
+              <el-icon :size="32">
+                <RefreshLeft />
+              </el-icon>
+            </div>
+            <div class="action-content">
+              <h3>恢复数据库</h3>
+              <p>从SQL备份文件中恢复数据库数据</p>
+            </div>
+            <div class="restore-upload">
+              <el-upload ref="uploadRef" :show-file-list="false" :before-upload="beforeUpload" :http-request="handleFileUpload" accept=".sql" drag>
+                <el-icon class="el-icon--upload">
+                  <UploadFilled />
+                </el-icon>
+                <div class="el-upload__text">将SQL文件拖到此处,或<em>点击上传</em></div>
+                <template #tip>
+                  <div class="el-upload__tip">只能上传.sql文件,且不超过100MB</div>
+                </template>
+              </el-upload>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="handleCloseRestoreDialog">取消</el-button>
+        </span>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="Index" lang="ts">
 import { ref, nextTick, onMounted } from 'vue';
 import { ElMessage } from 'element-plus';
-import { TrendCharts, PieChart, Monitor, Operation, Calendar, Bell, Trophy, Clock, VideoPlay, CircleCheck, DataBoard } from '@element-plus/icons-vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+import {
+  TrendCharts,
+  PieChart,
+  Monitor,
+  Operation,
+  Calendar,
+  Bell,
+  Trophy,
+  Clock,
+  VideoPlay,
+  CircleCheck,
+  DataBoard,
+  InfoFilled,
+  FolderAdd,
+  RefreshLeft,
+  UploadFilled
+} from '@element-plus/icons-vue';
 import * as echarts from 'echarts';
 import { getArticleCount } from '@/api/system/article';
 import { getEventCount, listGameEvent } from '@/api/system/gameEvent';
@@ -219,6 +293,8 @@ import { getRefereeCount } from '@/api/system/gameReferee';
 import { getTeamCount } from '@/api/system/gameTeam';
 import { listNotice } from '@/api/system/notice';
 import { countOnlineUser } from '@/api/monitor/online';
+import { backupDatabase, restoreDatabase } from '@/api/system/backup';
+import { useRouter } from 'vue-router';
 
 // 响应式数据
 const statsData = ref([
@@ -258,15 +334,19 @@ const statsData = ref([
     loading: true
   }
 ]);
+
 const router = useRouter();
 const onlineUsers = ref(1);
+const restoreDialogVisible = ref(false);
+const backupLoading = ref(false);
+const uploadRef = ref();
 const lastUpdateTime = ref('');
 
 const quickActions = ref([
   { name: '新增赛事', type: 'primary' as const, icon: 'Plus', action: 'addEvent' },
   { name: '参赛队伍管理', type: 'success' as const, icon: 'User', action: 'manageTeam' },
   { name: '裁判管理', type: 'warning' as const, icon: 'Flag', action: 'manageReferees' },
-  { name: '数据导出', type: 'info' as const, icon: 'Download', action: 'exportData' },
+  { name: '还原赛事', type: 'info' as const, icon: 'RefreshLeft', action: 'restoreEvent' },
   { name: '刷新数据', type: 'default' as const, icon: 'Refresh', action: 'refresh' }
 ]);
 
@@ -488,7 +568,7 @@ const handleQuickAction = (action: string) => {
       // 跳转到新增赛事页面
       router.push(`/system/gameEvent/add`);
       break;
-    case 'manageAthletes':
+    case 'manageTeam':
       // 跳转到参赛队伍管理页面
       router.push(`/game/gameTeam`);
       break;
@@ -496,12 +576,9 @@ const handleQuickAction = (action: string) => {
       // 跳转到裁判管理页面
       router.push(`/game/gameReferee`);
       break;
-    case 'exportData':
-      // 导出数据
-      ElMessage({
-        message: '请到具体的管理页面进行导出',
-        type: 'info'
-      });
+    case 'restoreEvent':
+      // 显示还原赛事弹窗
+      restoreDialogVisible.value = true;
       break;
     case 'refresh':
       // 刷新统计数据和最新赛事
@@ -532,6 +609,99 @@ const getAllData = async () => {
   getOnlineUser();
 };
 
+// 弹窗相关方法
+const handleCloseRestoreDialog = () => {
+  restoreDialogVisible.value = false;
+  backupLoading.value = false;
+};
+
+// 备份数据库
+const handleBackup = async () => {
+  backupLoading.value = true;
+  try {
+    await proxy?.download('system/backup/backup', {}, `db_backup_${new Date().getTime()}.sql`);
+    ElMessage({
+      message: '数据库备份下载成功',
+      type: 'success'
+    });
+
+    restoreDialogVisible.value = false;
+  } catch (error) {
+    console.error('备份失败:', error);
+    ElMessage({
+      message: '数据库备份失败,请稍后重试',
+      type: 'error'
+    });
+  } finally {
+    backupLoading.value = false;
+  }
+};
+
+// 文件上传前的验证
+const beforeUpload = (file: File) => {
+  const isSQL = file.name.toLowerCase().endsWith('.sql');
+  const isLt100M = file.size / 1024 / 1024 < 100;
+
+  if (!isSQL) {
+    ElMessage({
+      message: '只能上传.sql格式的文件',
+      type: 'error'
+    });
+    return false;
+  }
+  if (!isLt100M) {
+    ElMessage({
+      message: '上传文件大小不能超过100MB',
+      type: 'error'
+    });
+    return false;
+  }
+  return true;
+};
+
+// 自定义文件上传处理
+const handleFileUpload = async (options: any) => {
+  const { file } = options;
+
+  try {
+    ElMessage({
+      message: '正在恢复数据库,请稍等...',
+      type: 'info',
+      duration: 0
+    });
+
+    const response = await restoreDatabase(file);
+
+    // 关闭加载消息
+    ElMessage.closeAll();
+
+    if (response.code === 200) {
+      ElMessage({
+        message: '数据库恢复成功',
+        type: 'success'
+      });
+      restoreDialogVisible.value = false;
+
+      // 刷新页面数据
+      setTimeout(() => {
+        getAllData();
+      }, 1000);
+    } else {
+      ElMessage({
+        message: response.msg || '数据库恢复失败',
+        type: 'error'
+      });
+    }
+  } catch (error) {
+    ElMessage.closeAll();
+    console.error('恢复失败:', error);
+    ElMessage({
+      message: '数据库恢复失败,请检查文件格式或稍后重试',
+      type: 'error'
+    });
+  }
+};
+
 onMounted(() => {
   getAllData();
   updateTime();
@@ -822,4 +992,142 @@ onMounted(() => {
     color: #909399;
   }
 }
+
+// 还原赛事弹窗样式
+.restore-dialog-content {
+  .restore-description {
+    display: flex;
+    align-items: center;
+    margin-bottom: 24px;
+    padding: 16px;
+    background: #f8f9fa;
+    border-radius: 8px;
+
+    .description-icon {
+      color: #409eff;
+      margin-right: 12px;
+      font-size: 20px;
+    }
+
+    p {
+      margin: 0;
+      color: #606266;
+      font-size: 14px;
+    }
+  }
+
+  .restore-actions {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+  }
+
+  .action-card {
+    border: 2px solid #e4e7ed;
+    border-radius: 12px;
+    padding: 20px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    position: relative;
+
+    &:hover {
+      border-color: #409eff;
+      box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
+    }
+
+    &.backup-card {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      .action-icon {
+        color: #67c23a;
+        margin-right: 16px;
+      }
+
+      .action-content {
+        flex: 1;
+
+        h3 {
+          margin: 0 0 8px 0;
+          color: #303133;
+          font-size: 16px;
+          font-weight: 600;
+        }
+
+        p {
+          margin: 0;
+          color: #909399;
+          font-size: 14px;
+        }
+      }
+    }
+
+    &.restore-card {
+      .action-icon {
+        color: #e6a23c;
+        margin-bottom: 16px;
+      }
+
+      .action-content {
+        margin-bottom: 20px;
+
+        h3 {
+          margin: 0 0 8px 0;
+          color: #303133;
+          font-size: 16px;
+          font-weight: 600;
+        }
+
+        p {
+          margin: 0;
+          color: #909399;
+          font-size: 14px;
+        }
+      }
+
+      .restore-upload {
+        :deep(.el-upload) {
+          width: 100%;
+        }
+
+        :deep(.el-upload-dragger) {
+          width: 100%;
+          height: 120px;
+          border: 2px dashed #d9d9d9;
+          border-radius: 8px;
+          background: #fafafa;
+          transition: all 0.3s ease;
+
+          &:hover {
+            border-color: #409eff;
+            background: #f0f8ff;
+          }
+        }
+
+        :deep(.el-icon--upload) {
+          font-size: 32px;
+          color: #c0c4cc;
+          margin-bottom: 8px;
+        }
+
+        :deep(.el-upload__text) {
+          color: #606266;
+          font-size: 14px;
+
+          em {
+            color: #409eff;
+            font-style: normal;
+          }
+        }
+
+        :deep(.el-upload__tip) {
+          margin-top: 8px;
+          color: #909399;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+}
 </style>

+ 1 - 9
src/views/system/activityIntroduction/index.vue

@@ -4,9 +4,6 @@
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <!--            <el-form-item label="赛事id" prop="eventId">-->
-            <!--              <el-input v-model="queryParams.eventId" placeholder="请输入赛事id" clearable @keyup.enter="handleQuery" />-->
-            <!--            </el-form-item>-->
             <el-form-item label="标题" prop="title">
               <el-input v-model="queryParams.title" placeholder="请输入标题" clearable @keyup.enter="handleQuery" />
             </el-form-item>
@@ -54,8 +51,6 @@
 
       <el-table v-loading="loading" border :data="articleList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="" align="center" prop="id" v-if="true" />
-        <el-table-column label="赛事id" align="center" prop="eventId" />
         <el-table-column label="标题" align="center" prop="title" />
         <!--        <el-table-column label="内容" align="center" prop="content" />-->
         <el-table-column label="文章类型" align="center" prop="type">
@@ -90,13 +85,10 @@
     <!-- 添加或修改文章对话框 -->
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="1200px" append-to-body>
       <el-form ref="articleFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="赛事id" prop="eventId">
-          <el-input v-model="form.eventId" placeholder="请输入赛事id" />
-        </el-form-item>
         <el-form-item label="标题" prop="title">
           <el-input v-model="form.title" placeholder="请输入标题" />
         </el-form-item>
-        <el-form-item label="内容">
+        <el-form-item label="内容" prop="content">
           <editor v-model="form.content" :min-height="192" />
         </el-form-item>
         <el-form-item label="文章类型" prop="type">

+ 193 - 0
src/views/system/advice/index.vue

@@ -0,0 +1,193 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="姓名" prop="name">
+              <el-input v-model="queryParams.name" placeholder="请输入姓名" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="队伍名称" prop="teamName">
+              <el-input v-model="queryParams.teamName" placeholder="请输入队伍名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:advice:remove']"
+              >删除
+            </el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:advice:export']"> 导出 </el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="adviceList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="id" align="center" prop="id" v-if="true" />
+        <el-table-column label="姓名" align="center" prop="name" />
+        <!--        <el-table-column label="队伍id" align="center" prop="teamId" />-->
+        <el-table-column label="队伍名称" align="center" prop="teamName" />
+        <!--        <el-table-column label="反馈内容" align="center" prop="content" />-->
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="预览" placement="top">
+              <el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['system:article:view']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:advice:remove']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <el-dialog title="建议预览" v-model="dialog.visible" width="1200px" append-to-body>
+      <el-form ref="articleFormRef" :model="form" label-width="80px" disabled>
+        <el-form-item label="姓名" prop="name">
+          <el-input v-model="form.name" />
+        </el-form-item>
+        <el-form-item label="队伍名称" prop="teamName">
+          <el-input v-model="form.teamName" />
+        </el-form-item>
+        <el-form-item label="内容">
+          <editor v-model="form.content" :min-height="192" readOnly />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="Advice" lang="ts">
+import { listAdvice, delAdvice } from '@/api/system/advice';
+import { AdviceVO, AdviceQuery, AdviceForm } from '@/api/system/advice/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const adviceList = ref<AdviceVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const adviceFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: AdviceForm = {
+  id: undefined,
+  name: undefined,
+  teamId: undefined,
+  content: undefined
+};
+const data = reactive<PageData<AdviceForm, AdviceQuery>>({
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    name: undefined,
+    content: undefined,
+    params: {}
+  },
+  rules: {
+    name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
+    teamId: [{ required: true, message: '队伍id不能为空', trigger: 'change' }],
+    content: [{ required: true, message: '反馈内容不能为空', trigger: 'blur' }]
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询用户反馈建议列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listAdvice(queryParams.value);
+  adviceList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+};
+
+/** 表单重置 */
+const reset = () => {
+  form.value = { ...initFormData };
+  adviceFormRef.value?.resetFields();
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: AdviceVO[]) => {
+  ids.value = selection.map((item) => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: AdviceVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除用户反馈建议编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
+  await delAdvice(_ids);
+  proxy?.$modal.msgSuccess('删除成功');
+  await getList();
+};
+
+const handleView = async (row?: AdviceVO) => {
+  Object.assign(form.value, row);
+  dialog.visible = true;
+};
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download(
+    'system/advice/export',
+    {
+      ...queryParams.value
+    },
+    `advice_${new Date().getTime()}.xlsx`
+  );
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 195 - 48
src/views/system/common/nav/components/GameNavigator.vue

@@ -53,6 +53,13 @@
 
     <!-- 菜单列表 -->
     <el-card class="list-card">
+      <el-table v-loading="loading" :data="navItems" @selection-change="handleSelectionChange" style="width: 100%">
+      <template #header>
+        <div class="list-header">
+          <span>菜单列表</span>
+          <span class="result-count">共找到 {{ total }} 条记录</span>
+        </div>
+      </template>
       <el-table v-loading="loading" :data="navItems" @selection-change="handleSelectionChange" style="width: 100%">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="序号" align="center" width="80" v-if="columns[0].visible">
@@ -76,6 +83,14 @@
             <el-color-picker v-model="scope.row.color" size="large" :disabled="true" />
           </template>
         </el-table-column>
+        <el-table-column label="颜色" align="center" prop="color" width="150" v-if="columns[4].visible">
+          <template #default="scope">
+            <div
+              v-if="scope.row.color"
+              :style="{ backgroundColor: scope.row.color, width: '30px', height: '20px', borderRadius: '4px', margin: '0 auto' }"
+            ></div>
+          </template>
+        </el-table-column>
         <el-table-column label="链接类别" align="center" prop="jumpType" width="150" v-if="columns[5].visible">
           <template #default="scope">
             {{ getJumpTypeText(scope.row.jumpType) }}
@@ -174,6 +189,8 @@
           <el-col :span="12">
             <el-form-item label="颜色" required>
               <div class="color-input-group">
+                <el-color-picker v-model="form.color" size="large" class="color-picker" @change="handleColorChange" />
+                <el-input v-model="colorInput" placeholder="#RRGGBB" class="color-input" @input="handleColorInput" @blur="validateColorInput" />
                 <el-color-picker v-model="form.color" show-alpha size="large" class="color-picker" @change="handleColorChange" />
                 <el-input v-model="colorInput" placeholder="#RRGGBB" class="color-input" @input="handleColorInput" @blur="validateColorInput" />
               </div>
@@ -375,6 +392,14 @@ interface NavItem extends GameNavigatorVo {
 // 表单数据
 const navItems = ref<NavItem[]>([]);
 const formRef = ref();
+const form = reactive<
+  GameNavigatorBo & {
+    createTime?: string;
+    updateTime?: string;
+  }
+>({
+const formRef = ref();
+const queryRef = ref();
 const form = reactive<
   GameNavigatorBo & {
     createTime?: string;
@@ -393,7 +418,7 @@ const form = reactive<
   status: 0,
   remark: '',
   createTime: '',
-  updateTime: ''
+  updateTime: '',
 });
 
 // 响应式数据
@@ -461,9 +486,17 @@ function formatDateTime(date: string | Date | null | undefined): string {
   });
 }
 
+// 格式化日期为查询格式 (YYYY-MM-DD)
+function formatDateForQuery(date: Date): string {
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, '0');
+  const day = String(date.getDate()).padStart(2, '0');
+  return `${year}-${month}-${day}`;
+}
+
 // 获取跳转类型文本
 function getJumpTypeText(jumpType: number | null | undefined): string {
-  if (!jumpType) return '-';
+  if (!jumpType) return '-'
   const typeMap: Record<number, string> = {
     1: '跳转链接',
     2: '不跳转',
@@ -473,43 +506,66 @@ function getJumpTypeText(jumpType: number | null | undefined): string {
     6: '公众号文章',
     7: '公众号',
     8: '电话拨号'
-  };
-  return typeMap[jumpType] || '-';
+  }
+  return typeMap[jumpType] || '-'
 }
 
 // 获取最大排序值
 function getMaxSortNum(): number {
   if (navItems.value.length === 0) {
-    return 0;
+    return 0
   }
-  const maxSort = Math.max(...navItems.value.map((item) => item.sortNum || 0));
-  return maxSort;
+  const maxSort = Math.max(...navItems.value.map(item => item.sortNum || 0))
+  return maxSort
 }
 
 // 加载菜单数据
 async function loadNavData() {
-  loading.value = true;
+  loading.value = true
   try {
     const response = await getEnabledNavigator();
     if (response.code === 200 && response.data) {
       navItems.value = response.data as unknown as NavItem[];
+    // 构建查询参数
+    const query: any = {
+      pageNum: 1,
+      pageSize: 100, // 设置一个较大的值以获取所有数据
+      ...queryParams
+    };
+
+    // 处理日期范围
+    if (dateRange.value && dateRange.value.length === 2) {
+      query.beginTime = formatDateForQuery(dateRange.value[0]);
+      query.endTime = formatDateForQuery(dateRange.value[1]);
+    }
+
+    const response = await listNavigator(query);
+    if (response.code === 200 && (response as any).rows) {
+      navItems.value = (response as any).rows || [];
+      total.value = (response as any).total || 0;
     } else {
       navItems.value = [];
+      navItems.value = [];
+      total.value = 0;
     }
   } catch (error) {
     console.error('加载数据失败:', error);
     navItems.value = [];
+    console.error('加载数据失败:', error);
+    navItems.value = [];
+    total.value = 0;
   } finally {
-    loading.value = false;
+    loading.value = false
     // 数据加载完成后,更新排序值
     if (!form.navId) {
-      form.sortNum = nextSortNum.value;
+      form.sortNum = nextSortNum.value
     }
   }
 }
 
 /** 搜索按钮操作 */
 function handleQuery() {
+  // 重置到第一页
   loadNavData();
 }
 
@@ -522,6 +578,10 @@ function resetQuery() {
     status: undefined,
     createTime: ''
   });
+  // 重置查询引用
+  if (queryRef.value) {
+    queryRef.value.resetFields();
+  }
   loadNavData();
 }
 
@@ -584,12 +644,14 @@ function handleView(row: NavItem) {
 
 // 处理编辑
 function handleEdit(item: NavItem) {
+  reset();
+  const normalizedColor = normalizeColor(item.color || '#409EFF');
   reset();
   const formData = {
     navId: item.navId,
     name: item.name,
     pic: item.pic,
-    color: item.color || '#409EFF',
+    color: normalizedColor,
     jumpType: item.jumpType || 1,
     jumpPath: item.jumpType === 2 ? item.jumpPath || '#' : item.jumpPath,
     activityType: item.activityType || 1,
@@ -604,6 +666,11 @@ function handleEdit(item: NavItem) {
   colorInput.value = item.color || '#409EFF';
   dialogVisible.value = true;
   dialogTitle.value = '修改菜单';
+  };
+  Object.assign(form, formData);
+  colorInput.value = normalizedColor;
+  dialogVisible.value = true;
+  dialogTitle.value = '修改菜单';
 }
 
 // 处理添加
@@ -611,6 +678,12 @@ function handleAdd() {
   reset();
   // 设置排序值为当前最大值+1
   form.sortNum = nextSortNum.value;
+  // 确保颜色值为16进制格式
+  form.color = normalizeColor(form.color);
+  colorInput.value = form.color;
+  dialogVisible.value = true;
+  dialogTitle.value = '添加菜单';
+  form.sortNum = nextSortNum.value;
   dialogVisible.value = true;
   dialogTitle.value = '添加菜单';
 }
@@ -629,11 +702,11 @@ async function handleDelete(item: NavItem) {
       ElMessage.success('删除成功');
       loadNavData();
     } else {
-      ElMessage.error(response.msg || '删除失败');
+      ElMessage.error(response.msg || '删除失败')
     }
   } catch (error) {
     if (error !== 'cancel') {
-      ElMessage.error('删除失败');
+      ElMessage.error('删除失败')
     }
   }
 }
@@ -641,8 +714,8 @@ async function handleDelete(item: NavItem) {
 // 处理提交
 async function handleSubmit() {
   try {
-    await formRef.value.validate();
-    submitting.value = true;
+    await formRef.value.validate()
+    submitting.value = true
 
     const payload: any = {
       navId: form.navId,
@@ -657,29 +730,29 @@ async function handleSubmit() {
       status: form.status,
       remark: form.remark,
       createTime: form.createTime,
-      updateTime: form.updateTime
+      updateTime: form.updateTime,
       // type: '4' // 菜单固定类型
-    };
+    }
 
-    let response;
+    let response
     if (form.navId) {
-      response = await updateNavigator(payload);
+      response = await updateNavigator(payload)
     } else {
-      response = await addNavigator(payload);
+      response = await addNavigator(payload)
     }
 
     if (response.code === 200) {
-      ElMessage.success(form.navId ? '修改成功' : '添加成功');
-      dialogVisible.value = false;
-      loadNavData();
+      ElMessage.success(form.navId ? '修改成功' : '添加成功')
+      dialogVisible.value = false
+      loadNavData()
     } else {
-      ElMessage.error(response.msg || '操作失败');
+      ElMessage.error(response.msg || '操作失败')
     }
   } catch (error) {
-    console.error('提交失败:', error);
-    ElMessage.error('操作失败');
+    console.error('提交失败:', error)
+    ElMessage.error('操作失败')
   } finally {
-    submitting.value = false;
+    submitting.value = false
   }
 }
 
@@ -700,6 +773,9 @@ function reset() {
     createTime: '',
     updateTime: ''
   });
+  colorInput.value = defaultColor;
+  formRef.value?.clearValidate();
+  });
   colorInput.value = '#409EFF';
   formRef.value?.clearValidate();
 }
@@ -707,50 +783,72 @@ function reset() {
 // 跳转类型变更处理
 function handleJumpTypeChange() {
   if (form.jumpType === 2) {
-    form.jumpPath = '#';
+    form.jumpPath = '#'
   } else if (form.jumpType === 4) {
-    form.jumpPath = '';
+    form.jumpPath = ''
   }
 }
 
 // 图片上传前验证
 function beforeIconUpload(file: File) {
-  const isImage = file.type.startsWith('image/');
-  const isLt2M = file.size / 1024 / 1024 < 2;
+  const isImage = file.type.startsWith('image/')
+  const isLt2M = file.size / 1024 / 1024 < 2
 
   if (!isImage) {
-    ElMessage.error('只能上传图片文件!');
-    return false;
+    ElMessage.error('只能上传图片文件!')
+    return false
   }
   if (!isLt2M) {
-    ElMessage.error('图片大小不能超过 2MB!');
-    return false;
+    ElMessage.error('图片大小不能超过 2MB!')
+    return false
   }
-  return true;
+  return true
 }
 
 // 图片上传成功
 function handleIconSuccess(response: any) {
   if (response.code === 200) {
-    form.pic = response.data.url;
-    ElMessage.success('图片上传成功');
+    form.pic = response.data.url
+    ElMessage.success('图片上传成功')
   } else {
-    ElMessage.error(response.msg || '图片上传失败');
+    ElMessage.error(response.msg || '图片上传失败')
   }
 }
 
 // 颜色输入框的值
-const colorInput = ref('#409EFF');
+const colorInput = ref('#409EFF')
 
 // 将RGB颜色转换为16进制
 const rgbToHex = (rgb: string): string => {
   // 如果是16进制格式,直接返回
   if (rgb.startsWith('#')) {
-    return rgb;
+    return rgb
   }
 
   // 如果是RGB格式,转换为16进制
-  const rgbMatch = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
+  const rgbMatch = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/)
+  if (rgbMatch) {
+    const r = parseInt(rgbMatch[1])
+    const g = parseInt(rgbMatch[2])
+    const b = parseInt(rgbMatch[3])
+    return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
+  }
+
+  // 如果无法解析,返回默认值
+  return '#409EFF';
+};
+
+// 将任意颜色格式统一转换为16进制
+const normalizeColor = (color: string): string => {
+  if (!color) return '#409EFF';
+
+  // 如果已经是16进制格式,直接返回
+  if (color.startsWith('#')) {
+    return color;
+  }
+
+  // 如果是RGB格式,转换为16进制
+  const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
   if (rgbMatch) {
     const r = parseInt(rgbMatch[1]);
     const g = parseInt(rgbMatch[2]);
@@ -758,7 +856,9 @@ const rgbToHex = (rgb: string): string => {
     return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
   }
 
-  // 如果无法解析,返回默认值
+  // 尝试解析其他格式或返回默认值
+  return '#409EFF';
+};
   return '#409EFF';
 };
 
@@ -766,33 +866,48 @@ const rgbToHex = (rgb: string): string => {
 const handleColorInput = (value: string) => {
   // 实时同步到颜色选择器
   if (value.startsWith('#')) {
-    form.color = value;
+    form.color = value
   }
-};
+}
 
 // 处理颜色选择器变化
 const handleColorChange = (value: string) => {
   // 当颜色选择器改变时,将16进制值填充到输入框
   if (value) {
+    const hexValue = normalizeColor(value);
+    form.color = hexValue;
+    colorInput.value = hexValue;
     const hexValue = rgbToHex(value);
     colorInput.value = hexValue;
   }
-};
+}
 
 // 验证颜色输入框
 const validateColorInput = () => {
-  const hexRegex = /^#[0-9A-Fa-f]{6}$/;
+  const hexRegex = /^#[0-9A-Fa-f]{6}$/
   if (!hexRegex.test(colorInput.value)) {
     ElMessage.warning('请输入正确的16进制颜色格式,如 #FF0000');
     colorInput.value = form.color;
+    ElMessage.warning('请输入正确的16进制颜色格式,如 #FF0000');
+    colorInput.value = form.color;
+  } else {
+    // 验证通过,确保表单中的颜色值也是16进制格式
+    form.color = colorInput.value;
   }
-};
+}
 
 // 初始化数据
 onMounted(() => {
   loadNavData();
   // 初始化排序值
   form.sortNum = nextSortNum.value;
+  // 确保初始颜色值为16进制格式
+  form.color = normalizeColor(form.color);
+  colorInput.value = form.color;
+  // 加载初始数据
+  loadNavData();
+});
+  form.sortNum = nextSortNum.value;
 });
 </script>
 
@@ -805,6 +920,21 @@ onMounted(() => {
   margin-bottom: 20px;
 }
 
+.search-card .el-form {
+  padding: 20px 0;
+}
+
+.list-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.result-count {
+  color: #909399;
+  font-size: 14px;
+}
+
 .action-card {
   margin-bottom: 20px;
 }
@@ -976,4 +1106,21 @@ onMounted(() => {
   color: #909399;
   margin-top: 8px;
 }
+
+.color-input-group {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.color-picker {
+  flex-shrink: 0;
+}
+
+.color-input {
+  flex: 1;
+  min-width: 120px;
+}
+</style>
+
 </style>

+ 141 - 54
src/views/system/gameAthlete/index.vue

@@ -4,9 +4,6 @@
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <!-- <el-form-item label="赛事名称" prop="eventName">
-              <el-input v-model="queryParams.eventName" placeholder="请输入赛事名称" clearable @keyup.enter="handleQuery" />
-            </el-form-item> -->
             <el-form-item label="队伍名称" prop="teamName">
               <el-input v-model="queryParams.teamName" placeholder="请输入队伍名称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
@@ -69,9 +66,9 @@
           </template>
         </el-table-column>
         <el-table-column label="年龄" align="center" prop="age" v-if="columns[5].visible" />
-        <el-table-column label="参与项目" align="center" prop="projectValue" width="200px" v-if="columns[6].visible">
+        <el-table-column label="参与项目" align="center" prop="projectList" width="200px" v-if="columns[6].visible">
           <template #default="scope">
-            {{ formatProjectList(scope.row.projectValue) }}
+            {{ formatProjectList(scope.row.projectList) }}
           </template>
         </el-table-column>
         <el-table-column label="证件号" align="center" prop="idCard" v-if="columns[7].visible" />
@@ -190,7 +187,7 @@
 <script setup name="GameAthlete" lang="ts">
 import { nextTick, ref, onMounted } from 'vue';
 import { listGameAthlete, getGameAthlete, delGameAthlete, addGameAthlete, updateGameAthlete } from '@/api/system/gameAthlete';
-import { listGameTeam } from '@/api/system/gameTeam';
+import { listGameTeam, updateTeamAthletes } from '@/api/system/gameTeam';
 import { listGameEventProject } from '@/api/system/gameEventProject';
 // import { getDefaultEvent } from '@/api/system/gameEvent';
 import { GameAthleteVO, GameAthleteQuery, GameAthleteForm } from '@/api/system/gameAthlete/types';
@@ -216,7 +213,7 @@ const total = ref(0);
 // 列显隐数据
 const columns = ref<FieldOption[]>([
   { key: 0, label: '主键', visible: false },
-  { key: 1, label: '赛事名称', visible: true },
+  { key: 1, label: '赛事名称', visible: false },
   { key: 2, label: '运动员编号', visible: true },
   { key: 3, label: '姓名', visible: true },
   { key: 4, label: '性别', visible: true },
@@ -275,16 +272,17 @@ const initFormData: GameAthleteForm = {
   tshirtSize: undefined,
   groupType: undefined,
   projectValue: undefined,
+  projectList: [],
   selectedProjects: [], // 添加已选项目列表
   status: undefined,
   remark: undefined
 };
 const data = reactive<PageData<GameAthleteForm, GameAthleteQuery>>({
-  form: { ...initFormData, eventId: undefined },
+  form: { ...initFormData},
   queryParams: {
     pageNum: 1,
     pageSize: 10,
-    eventId: undefined, // 会自动设置为默认赛事ID
+    eventId: undefined,
     eventName: undefined,
     teamId: undefined,
     teamName: undefined,
@@ -338,7 +336,6 @@ const getTeamNameById = (teamId: string | number) => {
 // 获取赛事项目列表
 const getProjectList = async (eventId?: string) => {
   const res = await listGameEventProject({
-    // eventId: eventId || String(form.value.eventId),
     pageNum: 1,
     pageSize: 1000,
     orderByColumn: '',
@@ -352,25 +349,20 @@ const getProjectList = async (eventId?: string) => {
 };
 
 // 格式化项目列表显示
-const formatProjectList = (projectValue: string) => {
-  if (!projectValue) return '';
+const formatProjectList = (projectList: string[]) => {
+  if (!projectList) return '';
   // 将逗号分隔的ID列表转换为项目名称列表
-  const projectIds = projectValue.split(',');
-  const projectNames = projectIds.map((id) => {
-    const project = gameEventProjectList.value.find((p) => p.key === id);
-    return project ? project.label : id;
-  });
-  return projectNames.join(', ');
+  // const projectIds = projectValue.split(',');
+  // const projectNames = projectIds.map((id) => {
+  //   const project = gameEventProjectList.value.find((p) => p.key === id);
+  //   return project ? project.label : id;
+  // });
+  const projectNames = gameEventProjectList.value.filter((p) => projectList.includes(p.key)).map((p) => p.label);
+  return projectNames.join(',');
 };
 
 /** 查询参赛队员列表 */
 const getList = async () => {
-  // if (!queryParams.value.eventId) {
-  //   proxy?.$modal.msgWarning('未获取到默认赛事信息');
-  //   loading.value = false;
-  //   return;
-  // }
-
   loading.value = true;
   const res = await listGameAthlete(queryParams.value);
   gameAthleteList.value = res.rows;
@@ -428,11 +420,11 @@ const handleAdd = () => {
   dialog.visible = true;
   dialog.title = '添加参赛队员';
   // 获取项目列表
-  nextTick(() => {
-    if (form.value.eventId) {
-      getProjectList(String(form.value.eventId));
-    }
-  });
+  // nextTick(() => {
+  //   if (form.value.eventId) {
+  //     getProjectList(String(form.value.eventId));
+  //   }
+  // });
 };
 
 /** 修改按钮操作 */
@@ -452,11 +444,11 @@ const handleUpdate = async (row?: GameAthleteVO) => {
   dialog.visible = true;
   dialog.title = '修改参赛队员';
   // 获取项目列表
-  nextTick(() => {
-    if (form.value.eventId) {
-      getProjectList(String(form.value.eventId));
-    }
-  });
+  // nextTick(() => {
+  //   if (form.value.eventId) {
+  //     getProjectList(String(form.value.eventId));
+  //   }
+  // });
 };
 
 /** 提交按钮 */
@@ -464,24 +456,85 @@ const submitForm = () => {
   gameAthleteFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
       buttonLoading.value = true;
-      // 处理项目列表数据,将数组转换为逗号分隔的字符串
-      const submitForm = { ...form.value };
-      if (submitForm.selectedProjects && submitForm.selectedProjects.length > 0) {
-        submitForm.projectValue = submitForm.selectedProjects.join(',');
-      } else {
-        submitForm.projectValue = '';
+      try {
+        // 处理项目列表数据,将数组转换为逗号分隔的字符串
+        const submitForm = { ...form.value };
+        if (submitForm.selectedProjects && submitForm.selectedProjects.length > 0) {
+          submitForm.projectList = submitForm.selectedProjects;
+          // submitForm.projectValue = submitForm.selectedProjects.join(',');
+        } else {
+          // submitForm.projectValue = '';
+          submitForm.projectList = [];
+        }
+        // 删除selectedProjects属性,因为它不需要提交到后端
+        delete submitForm.selectedProjects;
+
+        let result;
+        if (form.value.athleteId) {
+          result = await updateGameAthlete(submitForm);
+        } else {
+          result = await addGameAthlete(submitForm);
+        }
+
+        // 更新队伍表中的运动员列表
+        if (submitForm.teamId) {
+          try {
+            // 获取当前队伍的所有运动员
+            const currentTeamAthletes = gameAthleteList.value
+              .filter(athlete => athlete.teamId === submitForm.teamId)
+              .map(athlete => athlete.athleteId);
+            
+            // 添加新运动员到列表中
+            if (!form.value.athleteId) {
+              // 新增运动员,需要从返回结果中获取新ID
+              const newAthleteId = result.data?.athleteId || submitForm.athleteId;
+              if (newAthleteId) {
+                currentTeamAthletes.push(newAthleteId);
+              }
+            } else {
+              // 更新运动员,检查是否改变了队伍
+              const originalAthlete = gameAthleteList.value.find(a => a.athleteId === form.value.athleteId);
+              if (originalAthlete && originalAthlete.teamId !== submitForm.teamId) {
+                // 运动员改变了队伍,需要从原队伍中移除,添加到新队伍中
+                
+                // 从原队伍中移除运动员
+                if (originalAthlete.teamId) {
+                  const originalTeamAthletes = gameAthleteList.value
+                    .filter(athlete => athlete.teamId === originalAthlete.teamId && athlete.athleteId !== form.value.athleteId)
+                    .map(athlete => athlete.athleteId);
+                  
+                  await updateTeamAthletes(originalAthlete.teamId, originalTeamAthletes);
+                }
+                
+                // 添加到新队伍中
+                if (!currentTeamAthletes.includes(form.value.athleteId)) {
+                  currentTeamAthletes.push(form.value.athleteId);
+                }
+              } else if (originalAthlete && originalAthlete.teamId === submitForm.teamId) {
+                // 运动员在同一队伍中,确保在列表中
+                if (!currentTeamAthletes.includes(form.value.athleteId)) {
+                  currentTeamAthletes.push(form.value.athleteId);
+                }
+              }
+            }
+            
+            // 更新队伍表中的运动员列表
+            await updateTeamAthletes(submitForm.teamId, currentTeamAthletes);
+          } catch (error) {
+            console.error('更新队伍运动员列表失败:', error);
+            proxy?.$modal.msgWarning('运动员信息保存成功,但更新队伍运动员列表失败');
+          }
+        }
+
+        proxy?.$modal.msgSuccess('操作成功');
+        dialog.visible = false;
+        await getList();
+      } catch (error) {
+        console.error('操作失败:', error);
+        proxy?.$modal.msgError('操作失败');
+      } finally {
+        buttonLoading.value = false;
       }
-      // 删除selectedProjects属性,因为它不需要提交到后端
-      delete submitForm.selectedProjects;
-
-      if (form.value.athleteId) {
-        await updateGameAthlete(submitForm).finally(() => (buttonLoading.value = false));
-      } else {
-        await addGameAthlete(submitForm).finally(() => (buttonLoading.value = false));
-      }
-      proxy?.$modal.msgSuccess('操作成功');
-      dialog.visible = false;
-      await getList();
     }
   });
 };
@@ -490,9 +543,42 @@ const submitForm = () => {
 const handleDelete = async (row?: GameAthleteVO) => {
   const _athleteIds = row?.athleteId || ids.value;
   await proxy?.$modal.confirm('是否确认删除参赛队员编号为"' + _athleteIds + '"的数据项?').finally(() => (loading.value = false));
-  await delGameAthlete(_athleteIds);
-  proxy?.$modal.msgSuccess('删除成功');
-  await getList();
+  
+  try {
+    await delGameAthlete(_athleteIds);
+    
+    // 从队伍表中移除被删除的运动员
+    const athleteIdsToRemove = Array.isArray(_athleteIds) ? _athleteIds : [_athleteIds];
+    const teamsToUpdate = new Set<number | string>();
+    
+    // 收集需要更新的队伍ID
+    athleteIdsToRemove.forEach(athleteId => {
+      const athlete = gameAthleteList.value.find(a => a.athleteId === athleteId);
+      if (athlete && athlete.teamId) {
+        teamsToUpdate.add(athlete.teamId);
+      }
+    });
+    
+    // 更新每个相关队伍中的运动员列表
+    for (const teamId of teamsToUpdate) {
+      try {
+        const currentTeamAthletes = gameAthleteList.value
+          .filter(athlete => athlete.teamId === teamId && !athleteIdsToRemove.includes(athlete.athleteId))
+          .map(athlete => athlete.athleteId);
+        
+        await updateTeamAthletes(teamId, currentTeamAthletes);
+      } catch (error) {
+        console.error(`更新队伍 ${teamId} 运动员列表失败:`, error);
+        proxy?.$modal.msgWarning(`运动员删除成功,但更新队伍 ${teamId} 运动员列表失败`);
+      }
+    }
+    
+    proxy?.$modal.msgSuccess('删除成功');
+    await getList();
+  } catch (error) {
+    console.error('删除失败:', error);
+    proxy?.$modal.msgError('删除失败');
+  }
 };
 
 /** 导出按钮操作 */
@@ -541,5 +627,6 @@ const importTemplate = () => {
 onMounted(() => {
   getList();
   getTeamList();
+  getProjectList();
 });
 </script>

+ 190 - 458
src/views/system/gameEvent/edit.vue

@@ -81,17 +81,6 @@
             </el-row>
 
             <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="是否默认赛事" prop="isDefault">
-                  <el-radio-group v-model="basicForm.isDefault" >
-                    <el-radio
-                      v-for="dict in sys_yes_no"
-                      :key="dict.value"
-                      :value="dict.value"
-                    >{{ dict.label }}</el-radio>
-                  </el-radio-group>
-                </el-form-item>
-              </el-col>
               <el-col :span="12">
                 <el-form-item label="状态" prop="status">
                   <el-radio-group v-model="basicForm.status">
@@ -133,119 +122,94 @@
           <div class="menu-config">
             <div class="menu-header">
               <h3>赛事菜单</h3>
-              <el-button type="primary" @click="addMenuItem" icon="Plus">添加菜单项</el-button>
+              <el-button type="primary" @click="showMenuSelectDialog" icon="Plus">添加菜单项</el-button>
             </div>
 
             <el-table :data="menuItems" border style="width: 100%">
-              <el-table-column label="菜单名称" prop="menuName">
+              <el-table-column label="菜单名称" prop="name" width="200">
                 <template #default="scope">
-                  <el-input v-model="scope.row.menuName" placeholder="请输入菜单名称" />
+                  <span>{{ scope.row.name }}</span>
                 </template>
               </el-table-column>
-              <el-table-column label="活动类型" prop="activityStatus">
+              <el-table-column label="图标" prop="pic" width="80">
                 <template #default="scope">
-                  <el-radio-group v-model="scope.row.activityStatus">
-                    <el-radio :value="0">赛前</el-radio>
-                    <el-radio :value="1">赛中</el-radio>
-                    <el-radio :value="2">赛后</el-radio>
-                  </el-radio-group>
+                  <el-image v-if="scope.row.pic" :src="scope.row.pic" style="width: 40px; height: 40px" fit="cover" />
+                  <span v-else>-</span>
                 </template>
               </el-table-column>
-              <el-table-column label="图标" prop="icon">
+              <el-table-column label="跳转类型" prop="jumpType" width="100">
                 <template #default="scope">
-                  <el-input v-model="scope.row.icon" placeholder="请输入图标" />
+                  <span>{{ getJumpTypeLabel(scope.row.jumpType) }}</span>
                 </template>
               </el-table-column>
-              <el-table-column label="颜色" prop="color">
+              <el-table-column label="颜色" prop="color" width="100">
                 <template #default="scope">
-                  <el-input v-model="scope.row.color" placeholder="请输入颜色" />
+                  <div v-if="scope.row.color" :style="{ backgroundColor: scope.row.color, width: '30px', height: '20px', borderRadius: '4px' }"></div>
                 </template>
               </el-table-column>
-              <el-table-column label="是否站外" prop="isFrame">
+              <el-table-column label="跳转路径" prop="jumpPath">
                 <template #default="scope">
-                  <el-radio-group v-model="scope.row.isFrame">
-                    <el-radio :value="1">站内</el-radio>
-                    <el-radio :value="0">站外</el-radio>
-                  </el-radio-group>
+                  <span>{{ scope.row.jumpPath }}</span>
                 </template>
               </el-table-column>
-              <el-table-column label="链接" prop="siteLink">
+              <el-table-column label="排序" prop="sortNum" width="80">
                 <template #default="scope">
-                  <el-input v-model="scope.row.siteLink" placeholder="请输入链接" />
+                  <span>{{ scope.row.sortNum }}</span>
                 </template>
               </el-table-column>
-              <el-table-column label="排序" prop="orderNum">
+              <el-table-column label="活动类型" prop="activityType" width="100">
                 <template #default="scope">
-                  <el-input-number v-model="scope.row.orderNum" :min="1" :max="999" />
+                  <span>{{ scope.row.activityType === 0 ? '赛前' : scope.row.activityType === 1 ? '赛中' : '赛后' }}</span>
                 </template>
               </el-table-column>
-              <el-table-column label="操作">
+              <el-table-column label="操作" width="100">
                 <template #default="scope">
                   <el-button type="danger" size="small" @click="removeMenuItem(scope.$index)" icon="Delete">删除 </el-button>
                 </template>
               </el-table-column>
             </el-table>
           </div>
-        </el-tab-pane>
 
-        <!-- 赛事项目标签页 -->
-        <el-tab-pane label="赛事项目" name="projects">
-          <div class="project-config">
-            <div class="project-header">
-              <h3>赛事项目配置</h3>
-              <el-button type="primary" @click="addProjectItem" icon="Plus">添加项目</el-button>
+          <!-- 菜单选择弹框 -->
+          <el-dialog v-model="menuSelectDialogVisible" title="选择菜单" width="60%" :close-on-click-modal="false">
+            <div class="menu-select-content">
+              <div class="search-bar" style="margin-bottom: 16px">
+                <el-input v-model="menuSearchKeyword" placeholder="搜索菜单名称" clearable style="width: 300px" />
+              </div>
+
+              <el-table :data="filteredAvailableMenus" border style="width: 100%" @selection-change="handleMenuSelectionChange" max-height="400">
+                <el-table-column type="selection" width="55" />
+                <el-table-column label="菜单名称" prop="name" width="200" />
+                <el-table-column label="图标" prop="pic" width="80">
+                  <template #default="scope">
+                    <el-image v-if="scope.row.pic" :src="scope.row.pic" style="width: 40px; height: 40px" fit="cover" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="颜色" prop="color" width="100">
+                  <template #default="scope">
+                    <div
+                      v-if="scope.row.color"
+                      :style="{ backgroundColor: scope.row.color, width: '30px', height: '20px', borderRadius: '4px' }"
+                    ></div>
+                  </template>
+                </el-table-column>
+                <el-table-column label="跳转类型" prop="jumpType" width="120">
+                  <template #default="scope">
+                    <span>{{ getJumpTypeLabel(scope.row.jumpType) }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="跳转路径" prop="jumpPath" />
+                <el-table-column label="排序" prop="sortNum" width="80" />
+              </el-table>
             </div>
 
-            <el-table :data="projectItems" border style="width: 100%">
-              <el-table-column label="項目类型" prop="projectType">
-                <template #default="scope">
-                  <el-select v-model="scope.row.projectType" placeholder="请选择項目类型" style="width: 100%">
-                    <el-option v-for="dict in game_project_type" :key="dict.value" :label="dict.label" :value="dict.value" />
-                  </el-select>
-                </template>
-              </el-table-column>
-              <el-table-column label="项目名称" prop="projectName">
-                <template #default="scope">
-                  <el-input v-model="scope.row.projectName" placeholder="请输入项目名称" />
-                </template>
-              </el-table-column>
-              <el-table-column label="项目组别" prop="groupType">
-                <template #default="scope">
-                  <el-select v-model="scope.row.groupType" placeholder="请选择项目组别" style="width: 100%">
-                    <el-option v-for="group in groupItems" :key="group.groupId" :label="group.groupName" :value="group.groupName" />
-                  </el-select>
-                </template>
-              </el-table-column>
-              <el-table-column label="排序" prop="orderType">
-                <template #default="scope">
-                  <el-radio-group v-model="scope.row.orderType">
-                    <el-radio value="0">降序</el-radio>
-                    <el-radio value="1">升序</el-radio>
-                  </el-radio-group>
-                </template>
-              </el-table-column>
-              <el-table-column label="比赛地点" prop="location">
-                <template #default="scope">
-                  <el-input v-model="scope.row.location" placeholder="请输入比赛地点" />
-                </template>
-              </el-table-column>
-              <el-table-column label="录取名次" prop="roundType">
-                <template #default="scope">
-                  <el-input v-model="scope.row.roundType" placeholder="请输入录取名次" />
-                </template>
-              </el-table-column>
-              <el-table-column label="分数" prop="scoreValue">
-                <template #default="scope">
-                  <el-input v-model="scope.row.scoreValue" placeholder="请输入分数" />
-                </template>
-              </el-table-column>
-              <el-table-column label="操作">
-                <template #default="scope">
-                  <el-button type="danger" size="small" @click="removeProjectItem(scope.$index)" icon="Delete">移除 </el-button>
-                </template>
-              </el-table-column>
-            </el-table>
-          </div>
+            <template #footer>
+              <span class="dialog-footer">
+                <el-button @click="menuSelectDialogVisible = false">取消</el-button>
+                <el-button type="primary" @click="confirmAddMenuItems">确认添加</el-button>
+              </span>
+            </template>
+          </el-dialog>
         </el-tab-pane>
 
         <!-- 配置信息标签页 -->
@@ -260,15 +224,15 @@
               <el-table-column label="配置类型" prop="configType" width="150">
                 <template #default="scope">
                   <el-select v-model="scope.row.configType" placeholder="请选择配置类型" style="width: 100%">
-                    <el-option
-                      v-for="type in configTypes"
-                      :key="type.typeCode"
-                      :label="type.typeName"
-                      :value="type.typeCode"
-                    />
+                    <el-option v-for="type in configTypes" :key="type.typeCode" :label="type.typeName" :value="type.typeCode" />
                   </el-select>
                 </template>
               </el-table-column>
+              <el-table-column label="配置描述" prop="configDesc">
+                <template #default="scope">
+                  <el-input v-model="scope.row.configDesc" placeholder="请输入配置描述" />
+                </template>
+              </el-table-column>
               <el-table-column label="配置键" prop="configKey" width="200">
                 <template #default="scope">
                   <el-input v-model="scope.row.configKey" placeholder="请输入配置键" />
@@ -279,11 +243,7 @@
                   <image-or-url-input v-model="scope.row.configValue" />
                 </template>
               </el-table-column>
-              <el-table-column label="配置描述" prop="configDesc">
-                <template #default="scope">
-                  <el-input v-model="scope.row.configDesc" placeholder="请输入配置描述" />
-                </template>
-              </el-table-column>
+
               <!-- <el-table-column label="是否启用" prop="isEnabled">
                 <template #default="scope">
                   <el-switch v-model="scope.row.isEnabled" />
@@ -297,71 +257,6 @@
             </el-table>
           </div>
         </el-tab-pane>
-
-        <!-- 参赛队伍标签页 -->
-        <el-tab-pane label="参赛队伍" name="teams">
-          <div class="team-config">
-            <div class="team-header">
-              <h3>参赛队伍</h3>
-              <el-button type="primary" @click="addTeamItem" icon="Plus">新增队伍</el-button>
-            </div>
-
-            <el-table :data="teamItems" border style="width: 100%">
-              <el-table-column label="单位名称" prop="teamName">
-                <template #default="scope">
-                  <el-input v-model="scope.row.teamName" placeholder="请输入单位名称" />
-                </template>
-              </el-table-column>
-              <el-table-column label="编号" prop="teamCode">
-                <template #default="scope">
-                  <el-input v-model="scope.row.teamCode" placeholder="请输入编号" />
-                </template>
-              </el-table-column>
-              <el-table-column label="团队描述" prop="teamDescribe">
-                <template #default="scope">
-                  <el-input v-model="scope.row.teamDescribe" placeholder="请描述一下你的团队吧~" />
-                </template>
-              </el-table-column>
-              <el-table-column label="操作">
-                <template #default="scope">
-                  <el-button type="danger" size="small" @click="removeTeamItem(scope.$index)" icon="Delete">删除 </el-button>
-                </template>
-              </el-table-column>
-            </el-table>
-          </div>
-        </el-tab-pane>
-
-        <!-- 赛事分组标签页 -->
-        <el-tab-pane label="赛事分组" name="groups">
-          <div class="group-config">
-            <div class="group-header">
-              <h3>赛事分组</h3>
-              <el-button type="primary" @click="addGroupItem" icon="Plus">添加分组</el-button>
-            </div>
-
-            <el-table :data="groupItems" border style="width: 100%">
-              <el-table-column label="分组名称" prop="groupName">
-                <template #default="scope">
-                  <el-input v-model="scope.row.groupName" placeholder="请输入分组名称" />
-                </template>
-              </el-table-column>
-              <el-table-column label="成员性别" prop="memberGender">
-                <template #default="scope">
-                  <el-radio-group v-model="scope.row.memberGender">
-                    <el-radio value="0">不分男女</el-radio>
-                    <el-radio value="1">男</el-radio>
-                    <el-radio value="2">女</el-radio>
-                  </el-radio-group>
-                </template>
-              </el-table-column>
-              <el-table-column label="操作">
-                <template #default="scope">
-                  <el-button type="danger" size="small" @click="removeGroupItem(scope.$index)" icon="Delete">删除 </el-button>
-                </template>
-              </el-table-column>
-            </el-table>
-          </div>
-        </el-tab-pane>
       </el-tabs>
 
       <!-- 底部操作按钮 -->
@@ -374,21 +269,14 @@
 </template>
 
 <script setup name="GameEventEdit" lang="ts">
-import { getGameEvent, addGameEvent, updateGameEvent,changeEventDefault } from '@/api/system/gameEvent';
+import { getGameEvent, addGameEvent, updateGameEvent, changeEventDefault } from '@/api/system/gameEvent';
 import { GameEventVO, GameEventForm } from '@/api/system/gameEvent/types';
 import { listGameEventConfig, addGameEventConfig, updateGameEventConfig, delGameEventConfig } from '@/api/system/gameEventConfig';
-import { GameEventConfigVO, GameEventConfigForm } from '@/api/system/gameEventConfig/types';
+import { GameEventConfigForm } from '@/api/system/gameEventConfig/types';
 import { listGameEventConfigType } from '@/api/system/gameEventConfigType'; // 添加导入
 import { GameEventConfigTypeVO } from '@/api/system/gameEventConfigType/types'; // 添加导入
 import { useRoute, useRouter } from 'vue-router';
-import { listGameEventMenu, addGameEventMenu, updateGameEventMenu, delGameEventMenu } from '@/api/system/gameEventMenu';
-import { GameEventMenuVO, GameEventMenuForm } from '@/api/system/gameEventMenu/types';
-import { listGameTeam, addGameTeam, updateGameTeam, delGameTeam } from '@/api/system/gameTeam';
-import { GameTeamVO, GameTeamForm } from '@/api/system/gameTeam/types';
-import { listGameEventGroup, addGameEventGroup, updateGameEventGroup, delGameEventGroup } from '@/api/system/gameEventGroup';
-import { GameEventGroupForm, GameEventGroupVO } from '@/api/system/gameEventGroup/types';
-import { addGameEventProject, listGameEventProject, updateGameEventProject, delGameEventProject } from '@/api/system/gameEventProject';
-import { GameEventProjectForm, GameEventProjectVO } from '@/api/system/gameEventProject/types';
+import { listRelateMenu, getEnabledNavigator, editRelate, type EditRelateMenuParams, GameNavigatorVo } from '@/api/system/common/nav/gameNavigator';
 
 const route = useRoute();
 const router = useRouter();
@@ -445,11 +333,14 @@ const basicRules = {
   endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }]
 };
 
+const currentEventId = ref<number>();
+
 // 初始化页面
 onMounted(async () => {
   const eventId = route.params.id as string;
   if (eventId) {
     isEdit.value = true;
+    currentEventId.value = Number(eventId);
     await loadEventData(eventId);
   } else {
     // 新增模式,加载图片配置模板
@@ -457,7 +348,6 @@ onMounted(async () => {
   }
   // 加载配置类型选项
   await loadConfigTypes();
-  // console.log('Initial image configs:', imageConfigItems.value); // 添加调试信息
 });
 
 // 加载赛事数据
@@ -469,22 +359,16 @@ const loadEventData = async (eventId: string | number) => {
     // 填充基本信息
     Object.assign(basicForm.value, data);
 
-    // 获取有效的赛事ID
-    const effectiveEventId = getEffectiveEventId(eventId);
-
     // 加载图片配置数据
     try {
-      await loadImageConfigData(effectiveEventId);
+      await loadImageConfigData(eventId);
     } catch (error) {
       console.warn('加载图片配置数据失败,继续加载其他数据:', error);
       // 不中断其他数据的加载
     }
 
-    await loadMenuData(effectiveEventId); // 加载菜单数据
-    await loadGroupData(effectiveEventId); // 加载分组数据
-    await loadProjectData(effectiveEventId); // 加载项目数据
-    await loadTeamData(effectiveEventId); // 加载队伍数据
-    await loadConfigData(effectiveEventId); // 加载配置数据
+    await loadMenuData(eventId); // 加载菜单数据
+    await loadConfigData(eventId); // 加载配置数据
   } catch (error) {
     proxy?.$modal.msgError('加载赛事数据失败');
   }
@@ -494,8 +378,6 @@ const loadEventData = async (eventId: string | number) => {
 const handleStatusChange = async (row: GameEventVO) => {
   const text = row.isDefault === '0' ? '启用' : '停用';
   try {
-    await proxy?.$modal.confirm('确认要"' + text + '""' + row.eventName + '"为默认赛事吗?');
-    await changeEventDefault(row.eventId, row.isDefault);
     await loadEventData(row.eventId);
     proxy?.$modal.msgSuccess(text + '成功');
   } catch {
@@ -504,123 +386,130 @@ const handleStatusChange = async (row: GameEventVO) => {
     row.isDefault = row.isDefault === '0' ? '1' : '0';
   }
 };
+// 菜单列表数据
+const menuItems = ref<GameNavigatorVo[]>([]);
+
+// 菜单选择弹框相关数据
+const menuSelectDialogVisible = ref(false);
+const availableMenus = ref<any[]>([]);
+const selectedMenus = ref<any[]>([]);
+const menuSearchKeyword = ref('');
+
+// 过滤可用菜单的计算属性
+const filteredAvailableMenus = computed(() => {
+  // 首先过滤掉已添加的菜单
+  let filteredMenus = availableMenus.value.filter((menu) => {
+    // 检查是否已存在相同的菜单(根据navId比较)
+    return !menuItems.value.some((item) => item.navId === Number(menu.navId));
+  });
+
+  // 然后根据搜索关键词进一步过滤
+  if (menuSearchKeyword.value) {
+    filteredMenus = filteredMenus.filter((menu) =>
+      menu.name?.toLowerCase().includes(menuSearchKeyword.value.toLowerCase())
+    );
+  }
+
+  return filteredMenus;
+});
 
-// 赛事分组数据
-const groupItems = ref<GameEventGroupForm[]>([]);
-// 加载项目分组数据
-const loadGroupData = async (eventId: string | number) => {
+// 加载菜单数据
+const loadMenuData = async (eventId: string | number) => {
   try {
-    const res = await listGameEventGroup({
-      eventId: eventId === '' ? '' : eventId,
+    const res = await listRelateMenu({
+      eventId: eventId,
       pageNum: 1,
       pageSize: 1000,
       orderByColumn: '',
       isAsc: ''
     });
-    groupItems.value = Array.isArray(res.rows) ? res.rows : [];
+    menuItems.value = Array.isArray(res.rows) ? res.rows : [];
   } catch (error) {
-    proxy?.$modal.msgError('加载项目分组数据失败');
-  }
-};
-// 添加赛事分组项
-const addGroupItem = () => {
-  groupItems.value.push({
-    groupId: '',
-    eventId: getEffectiveEventId(),
-    groupName: '',
-    memberGender: ''
-  });
-};
-// 删除赛事分组项
-const removeGroupItem = (index: number) => {
-  const groupItem = groupItems.value[index];
-  if (groupItem.groupId) {
-    delGameEventGroup(groupItem.groupId).then(() => {
-      groupItems.value.splice(index, 1);
-    });
-  } else {
-    groupItems.value.splice(index, 1);
+    proxy?.$modal.msgError('加载菜单数据失败');
   }
 };
-// 保存赛事分组数据
-const saveGroupData = async (eventId: string | number) => {
+
+// 显示菜单选择弹框
+const showMenuSelectDialog = async () => {
   try {
-    for (const item of groupItems.value) {
-      // 确保eventId被正确设置
-      const groupData = { ...item, eventId };
-
-      if (item.groupId) {
-        // 更新现有分组
-        await updateGameEventGroup(groupData);
-      } else if (item.groupName) {
-        // 只有有名称的新分组才保存
-        await addGameEventGroup(groupData);
-      }
-    }
+    // 加载可用的导航菜单
+    const res = await getEnabledNavigator();
+    availableMenus.value = Array.isArray(res.data) ? res.data : [];
+    selectedMenus.value = [];
+    menuSearchKeyword.value = '';
+    menuSelectDialogVisible.value = true;
   } catch (error) {
-    console.error('保存赛事分组数据失败:', error);
-    throw new Error('保存赛事分组失败');
+    proxy?.$modal.msgError('加载可用菜单失败');
   }
 };
 
-// 菜单列表数据
-const menuItems = ref<GameEventMenuForm[]>([]);
-// 加载菜单数据
-const loadMenuData = async (eventId: string | number) => {
-  try {
-    const res = await listGameEventMenu({
-      eventId: eventId === '' ? '' : eventId,
-      pageNum: 1,
-      pageSize: 10,
-      orderByColumn: undefined,
-      isAsc: undefined,
-    });
-    menuItems.value = Array.isArray(res.rows) ? res.rows : [];
-  } catch (error) {
-    proxy?.$modal.msgError('加载菜单数据失败');
+// 处理菜单选择变化
+const handleMenuSelectionChange = (selection: any[]) => {
+  selectedMenus.value = selection;
+};
+
+// 确认添加菜单项
+const confirmAddMenuItems = () => {
+  console.log('selectedMenus:', selectedMenus.value);
+  if (selectedMenus.value.length === 0) {
+    proxy?.$modal.msgWarning('请选择要添加的菜单');
+    return;
   }
+
+  // 将选中的菜单添加到菜单列表中(无需重复检查,因为列表已经过滤掉了重复项)
+  menuItems.value.push(...selectedMenus.value);
+
+  console.log('menuItems:', menuItems.value);
+  menuSelectDialogVisible.value = false;
+  proxy?.$modal.msgSuccess(`成功添加 ${selectedMenus.value.length} 个菜单项`);
 };
-// 添加菜单项
-const addMenuItem = () => {
-  menuItems.value.push({
-    menuId: '',
-    parentId: 0,
-    menuName: '',
-    activityStatus: 0,
-    icon: '',
-    color: '',
-    isFrame: 1,
-    siteLink: '',
-    orderNum: menuItems.value.length + 1,
-    eventId: getEffectiveEventId() // 使用有效的赛事ID
-  });
+
+// 获取跳转类型标签
+const getJumpTypeLabel = (jumpType: number) => {
+  const typeMap = {
+    1: '跳转链接',
+    2: '不跳转',
+    3: '小程序内链',
+    4: '小程序外链',
+    5: 'H5外链',
+    6: '公众号文章',
+    7: '公众号',
+    8: '电话拨号'
+  };
+  return typeMap[jumpType as keyof typeof typeMap] || '未知类型';
 };
+
 // 删除菜单项
-const removeMenuItem = (index: number) => {
-  const menuItem = menuItems.value[index];
-  if (menuItem.menuId) {
-    delGameEventMenu(menuItem.menuId).then(() => {
-      menuItems.value.splice(index, 1);
-    });
-  } else {
+const removeMenuItem = async (index: number) => {
+  try {
+    // 从本地数组中删除元素
     menuItems.value.splice(index, 1);
+
+    // 获取当前赛事ID
+    const eventId = (route.params.id as string) || '';
+
+    // 调用saveMenuData方法同步到后端
+    await saveMenuData(eventId);
+
+    proxy?.$modal.msgSuccess('删除菜单项成功');
+  } catch (error) {
+    console.error('删除菜单项失败:', error);
+    proxy?.$modal.msgError('删除菜单项失败');
   }
 };
+
 // 保存菜单数据
 const saveMenuData = async (eventId: string | number) => {
   try {
-    for (const item of menuItems.value) {
-      // 确保eventId被正确设置
-      const menuData = { ...item, eventId };
-
-      if (item.menuId) {
-        // 更新现有菜单
-        await updateGameEventMenu(menuData);
-      } else if (item.menuName) {
-        // 只有有名称的新菜单才保存
-        await addGameEventMenu(menuData);
-      }
-    }
+    // 提取所有菜单的navId
+    const menus = menuItems.value.map((item) => Number(item.navId)); // 转换为数字类型
+    // 调用editRelate接口保存赛事关联菜单
+    const relateData: EditRelateMenuParams = {
+      eventId: eventId,
+      menus: menus
+    };
+
+    await editRelate(relateData);
   } catch (error) {
     console.error('保存菜单数据失败:', error);
     throw new Error('保存菜单失败');
@@ -640,10 +529,10 @@ const loadImageConfigData = async (eventId: string | number) => {
       orderByColumn: '',
       isAsc: ''
     });
-    
+
     // 获取所有图片配置模板
     const allImageConfigs = Array.isArray(templateRes.rows) ? templateRes.rows : [];
-    
+
     // 查询当前赛事的图片配置数据(如果是默认赛事,eventId为空字符串)
     const eventRes = await listGameEventConfig({
       eventId: eventId === '' ? '' : eventId,
@@ -655,7 +544,7 @@ const loadImageConfigData = async (eventId: string | number) => {
     });
 
     const eventImageConfigs = Array.isArray(eventRes.rows) ? eventRes.rows : [];
-    
+
     // 合并配置:以模板为基础,用当前赛事的数据覆盖
     imageConfigItems.value = allImageConfigs.map((template) => {
       const existingConfig = eventImageConfigs.find((item) => item.configKey === template.configKey);
@@ -668,7 +557,6 @@ const loadImageConfigData = async (eventId: string | number) => {
         configId: existingConfig?.configId
       };
     });
-    
   } catch (error) {
     console.error('加载图片配置数据失败:', error);
     proxy?.$modal.msgError('加载图片配置数据失败: ' + (error as Error).message);
@@ -685,40 +573,11 @@ const refreshImageConfigs = async () => {
     // await loadImageConfigTemplates();
   }
 };
-// 加载图片配置模板(用于新增模式)
-// const loadImageConfigTemplates = async () => {
-//   try {
-//     const res = await listGameEventConfig({
-//       eventId: '',
-//       configType: 'IMAGE',
-//       pageNum: 1,
-//       pageSize: 100
-//     });
-
-//     // console.log('Template response (new mode):', res);
-
-//     const imageData = Array.isArray(res.rows) ? res.rows : []; // 修改此处,确保正确访问数组数据
-//     // console.log('Image data (new mode):', imageData);
-
-//     imageConfigItems.value = imageData.map(item => ({
-//       ...item,
-//       configValue: item.configValue || '', // 确保 configValue 初始化正确
-//       isEnabled: item.isEnabled || '0', // 确保 isEnabled 初始化正确
-//       status: item.status || '0' // 确保 status 初始化正确
-//     }));
-
-//     // console.log('Image config items (new mode):', imageConfigItems.value);
-//   } catch (error) {
-//     console.error('加载图片配置模板失败:', error);
-//     proxy?.$modal.msgError('加载图片配置模板失败: ' + (error as Error).message);
-//     imageConfigItems.value = [];
-//   }
-// };
 // 保存图片配置数据
 const saveImageConfigData = async (eventId?: string | number) => {
   try {
     // 如果是默认赛事操作,使用空字符串作为eventId
-    const targetEventId = eventId === '' ? '' : (eventId || (route.params.id as string));
+    const targetEventId = eventId === '' ? '' : eventId || (route.params.id as string);
     const updates: GameEventConfigForm[] = [];
     const adds: GameEventConfigForm[] = [];
 
@@ -759,68 +618,6 @@ const handleTabClick = () => {
   // 可以在这里添加标签页切换的逻辑
 };
 
-// 项目数据
-const projectItems = ref<GameEventProjectForm[]>([]);
-// 加载项目数据
-const loadProjectData = async (eventId: string | number) => {
-  try {
-    const res = await listGameEventProject({
-      eventId: eventId === '' ? '' : eventId,
-      pageNum: 1,
-      pageSize: 1000,
-      orderByColumn: '',
-      isAsc: ''
-    });
-    projectItems.value = Array.isArray(res.rows) ? res.rows : [];
-  } catch (error) {
-    proxy?.$modal.msgError('加载项目数据失败');
-  }
-};
-// 添加项目项
-const addProjectItem = () => {
-  projectItems.value.push({
-    eventId: getEffectiveEventId(),
-    projectId: '',
-    projectName: '',
-    projectType: '',
-    remark: '',
-    location: '',
-    scoreValue: '',
-    groupType: ''
-  });
-};
-// 删除项目项
-const removeProjectItem = (index: number) => {
-  const projectItem = projectItems.value[index];
-  if (projectItem.projectId) {
-    delGameEventProject(projectItem.projectId).then(() => {
-      projectItems.value.splice(index, 1);
-    });
-  } else {
-    projectItems.value.splice(index, 1);
-  }
-};
-//保存项目数据
-const saveProjectData = async (eventId: string | number) => {
-  try {
-    for (const item of projectItems.value) {
-      // 确保eventId被正确设置
-      const projectData = { ...item, eventId };
-
-      if (item.projectId) {
-        // 更新现有队伍
-        await updateGameEventProject(projectData);
-      } else if (item.projectName) {
-        // 只有有名称的新队伍才保存
-        await addGameEventProject(projectData);
-      }
-    }
-  } catch (error) {
-    console.error('保存赛事项目数据失败:', error);
-    throw new Error('保存赛事项目失败');
-  }
-};
-
 // 配置信息数据
 const configItems = ref<GameEventConfigForm[]>([]);
 // 配置类型选项
@@ -864,7 +661,7 @@ const addConfigItem = () => {
     configKey: '',
     configValue: '',
     configDesc: '',
-    configType: '', // 添加配置类型字段
+    configType: '' // 添加配置类型字段
   });
 };
 
@@ -901,65 +698,6 @@ const saveconfigData = async (eventId: string | number) => {
   }
 };
 
-// 参赛队伍数据
-const teamItems = ref<GameTeamForm[]>([]);
-// 加载队伍数据
-const loadTeamData = async (eventId: string | number) => {
-  try {
-    const res = await listGameTeam({
-      eventId: eventId === '' ? '' : eventId,
-      pageNum: 1,
-      pageSize: 1000,
-      orderByColumn: '',
-      isAsc: ''
-    });
-    teamItems.value = Array.isArray(res.rows) ? res.rows : [];
-  } catch (error) {
-    proxy?.$modal.msgError('加载队伍数据失败');
-  }
-};
-// 添加参赛队伍项
-const addTeamItem = () => {
-  teamItems.value.push({
-    teamName: '',
-    teamCode: '',
-    eventId: getEffectiveEventId(),
-    teamDescribe: '',
-    remark: ''
-  });
-};
-// 删除参赛队伍项
-const removeTeamItem = (index: number) => {
-  const teamItem = teamItems.value[index];
-  if (teamItem.teamId) {
-    delGameTeam(teamItem.teamId).then(() => {
-      teamItems.value.splice(index, 1);
-    });
-  } else {
-    teamItems.value.splice(index, 1);
-  }
-};
-// 保存参赛队伍数据
-const saveTeamData = async (eventId: string | number) => {
-  try {
-    for (const item of teamItems.value) {
-      // 确保eventId被正确设置
-      const teamData = { ...item, eventId };
-
-      if (item.teamId) {
-        // 更新现有队伍
-        await updateGameTeam(teamData);
-      } else if (item.teamName) {
-        // 只有有名称的新队伍才保存
-        await addGameTeam(teamData);
-      }
-    }
-  } catch (error) {
-    console.error('保存参赛队伍数据失败:', error);
-    throw new Error('保存参赛队伍失败');
-  }
-};
-
 // 保存赛事信息
 const saveEvent = async () => {
   try {
@@ -975,39 +713,33 @@ const saveEvent = async () => {
 
     let savedEventId: string;
     // 如果设置为默认赛事,则取消其他赛事的默认状态
-    if(basicForm.value.isDefault === '0'){
-      handleStatusChange({ 
-        eventId: basicForm.value.eventId, 
+    if (basicForm.value.isDefault === '0') {
+      handleStatusChange({
+        eventId: basicForm.value.eventId,
         isDefault: basicForm.value.isDefault,
-        eventName: basicForm.value.eventName 
+        eventName: basicForm.value.eventName
       } as GameEventVO);
     }
 
     // 保存基本信息
-    if (isEdit.value) {
-      await updateGameEvent(formData);
-      savedEventId = route.params.id as string;
-    } else {
+    if (!isEdit.value) {
       const addRes = await addGameEvent(formData);
       // 假设返回的数据中包含新创建的赛事ID
       savedEventId = addRes?.data as string;
+    } else {
+      savedEventId = route.params.id as string;
     }
-
-    // 获取有效的赛事ID用于保存相关数据
-    const effectiveEventId = getEffectiveEventId(savedEventId);
-
     // 保存图片配置数据
-    await saveImageConfigData(effectiveEventId);
+    await saveImageConfigData(savedEventId);
     // 保存赛事配置项数据
-    await saveconfigData(effectiveEventId);
+    await saveconfigData(savedEventId);
     // 保存菜单数据
-    await saveMenuData(effectiveEventId);
-    // 保存参赛队伍数据
-    await saveTeamData(effectiveEventId);
-    // 保存赛事项目分组数据
-    await saveGroupData(effectiveEventId);
-    // 保存赛事项目数据
-    await saveProjectData(effectiveEventId);
+    await saveMenuData(savedEventId);
+
+    // 保存基本信息
+    if (isEdit.value) {
+      await updateGameEvent(formData);
+    }
 
     proxy?.$modal.msgSuccess('保存成功');
 

Разница между файлами не показана из-за своего большого размера
+ 903 - 81
src/views/system/gameEvent/index.vue


+ 6 - 6
src/views/system/gameEventConfig/index.vue

@@ -53,9 +53,9 @@
         <el-table-column label="主键" align="center" prop="configId" v-if="columns[0].visible" />
         <!-- <el-table-column label="赛事ID" align="center" prop="eventId" /> -->
         <el-table-column label="配置类型" align="center" prop="configType" v-if="columns[1].visible" />
-        <el-table-column label="配置键" align="center" prop="configKey" v-if="columns[2].visible" />
-        <el-table-column label="配置值" align="center" prop="configValue" v-if="columns[3].visible" />
-        <el-table-column label="配置描述" align="center" prop="configDesc" v-if="columns[4].visible" />
+        <el-table-column label="配置描述" align="center" prop="configDesc" v-if="columns[2].visible" />
+        <el-table-column label="配置键" align="center" prop="configKey" v-if="columns[3].visible" />
+        <el-table-column label="配置值" align="center" prop="configValue" v-if="columns[4].visible" />
         <el-table-column label="是否启用" align="center" prop="isEnabled" v-if="columns[5].visible">
           <template #default="scope">
             <dict-tag :options="game_yes_no" :value="scope.row.isEnabled"/>
@@ -148,9 +148,9 @@ const total = ref(0);
 const columns = ref<FieldOption[]>([
   { key: 0, label: '主键', visible: true },
   { key: 1, label: '配置类型', visible: true },
-  { key: 2, label: '配置', visible: true },
-  { key: 3, label: '配置', visible: true },
-  { key: 4, label: '配置描述', visible: true },
+  { key: 2, label: '配置描述', visible: true },
+  { key: 3, label: '配置', visible: true },
+  { key: 4, label: '配置', visible: true },
   { key: 5, label: '是否启用', visible: true },
   { key: 6, label: '状态', visible: true },
   { key: 7, label: '备注', visible: true }

+ 450 - 0
src/views/system/gameEventGroup/detail.vue

@@ -0,0 +1,450 @@
+<template>
+  <div class="p-4">
+    <!-- 顶部信息区域 -->
+    <el-card shadow="hover" class="mb-4">
+      <div class="flex justify-between items-center">
+        <div class="text-left">
+          <h2 class="text-xl font-bold text-gray-800 mb-2">{{ projectName }} - {{ groupInfo.groupName }}</h2>
+          <div class="grid grid-cols-3 gap-4 text-sm text-gray-600">
+            <div><span class="font-medium"></span>{{ groupInfo.personNum }}人</div>
+            <div><span class="font-medium">组数:</span>{{ groupInfo.includeGroupNum }}组</div>
+            <div><span class="font-medium">道数:</span>{{ groupInfo.trackNum }}道</div>
+            <div><span class="font-medium">开始时间:</span>{{ groupInfo.beginTime }}</div>
+            <div><span class="font-medium">预计结束时间:</span>{{ groupInfo.endTime }}</div>
+            <div><span class="font-medium">场地数量:</span>{{ groupInfo.fieldNum }}个</div>
+          </div>
+        </div>
+        <div class="text-right">
+          <el-button type="primary" @click="generateGroups" :loading="generating">重新生成分组</el-button>
+          <el-button @click="goBack">返回</el-button>
+        </div>
+      </div>
+    </el-card>
+
+    <!-- 分组详情表格 -->
+    <el-card shadow="hover">
+      <template #header>
+        <div class="flex justify-between items-center">
+          <span class="font-medium">分组详情</span>
+          <div class="text-sm text-gray-500">
+            共 {{ groupInfo.includeGroupNum }} 组,{{ groupInfo.trackNum }} 条道
+          </div>
+        </div>
+      </template>
+
+      <div class="overflow-x-auto">
+        <table class="w-full border-collapse border border-gray-300">
+          <!-- 表头 -->
+          <thead>
+            <tr class="bg-gray-100">
+              <th class="border border-gray-300 px-4 py-2 text-center font-medium">组别</th>
+              <th 
+                v-for="track in groupInfo.trackNum" 
+                :key="track" 
+                class="border border-gray-300 px-4 py-2 text-center font-medium"
+              >
+                第{{ getTrackName(track) }}道
+              </th>
+            </tr>
+          </thead>
+          
+          <!-- 分组内容 -->
+          <tbody>
+            <tr 
+              v-for="groupIndex in groupInfo.includeGroupNum" 
+              :key="groupIndex"
+              class="hover:bg-gray-50"
+            >
+              <td class="border border-gray-300 px-4 py-3 text-center font-medium bg-blue-50">
+                第{{ groupIndex }}组
+              </td>
+              <td 
+                v-for="track in groupInfo.trackNum" 
+                :key="track" 
+                class="border border-gray-300 px-4 py-3 text-center"
+              >
+                 <div v-if="getAthleteByGroupAndTrack(groupIndex, track)" class="space-y-1">
+                   <div class="font-medium text-blue-600">{{ getAthleteByGroupAndTrack(groupIndex, track)?.athleteCode }}</div>
+                   <div class="text-sm">{{ getAthleteByGroupAndTrack(groupIndex, track)?.name }}</div>
+                   <div class="text-xs text-gray-500">{{ getTeamName(getAthleteByGroupAndTrack(groupIndex, track)?.teamId) }}</div>
+                 </div>
+                <div v-else class="text-gray-400 text-sm">-</div>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </el-card>
+
+    <!-- 统计信息 -->
+    <el-card shadow="hover" class="mt-4">
+      <template #header>
+        <span class="font-medium">分组统计</span>
+      </template>
+      
+      <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
+        <div class="text-center p-3 bg-blue-50 rounded-lg">
+          <div class="text-2xl font-bold text-blue-600">{{ totalAthletes }}</div>
+          <div class="text-sm text-gray-600">符合条件的运动员数量</div>
+        </div>
+        <div class="text-center p-3 bg-green-50 rounded-lg">
+          <div class="text-2xl font-bold text-green-600">{{ groupInfo.includeGroupNum }}</div>
+          <div class="text-sm text-gray-600">分组数</div>
+        </div>
+        <div class="text-center p-3 bg-purple-50 rounded-lg">
+          <div class="text-2xl font-bold text-purple-600">{{ groupInfo.trackNum }}</div>
+          <div class="text-sm text-gray-600">道数</div>
+        </div>
+        <div class="text-center p-3 bg-orange-50 rounded-lg">
+          <div class="text-2xl font-bold text-orange-600">{{ groupInfo.personNum }}</div>
+          <div class="text-sm text-gray-600">{{ groupInfo.groupName }}人数</div>
+        </div>
+      </div>
+    </el-card>
+
+    <!-- 调试信息 -->
+    <!-- <el-card shadow="hover" class="mt-4">
+      <template #header>
+        <span class="font-medium">调试信息</span>
+      </template>
+      
+      <div class="space-y-2 text-sm">
+        <div><strong>项目ID:</strong> {{ groupInfo.projectId }}</div>
+        <div><strong>性别要求:</strong> {{ groupInfo.memberGender === '0' ? '不分男女' : groupInfo.memberGender === '1' ? '男' : '女' }}</div>
+        <div><strong>运动员总数:</strong> {{ athletes.length }}</div>
+        <div><strong>符合条件的运动员数:</strong> {{ totalAthletes }}</div>
+        <div><strong>分组结果大小:</strong> {{ groupResult.size }}</div>
+        <div><strong>分组结果:</strong></div>
+        <pre class="bg-gray-100 p-2 rounded text-xs overflow-auto max-h-40">{{ JSON.stringify(Array.from(groupResult.entries()), null, 2) }}</pre>
+      </div>
+    </el-card> -->
+  </div>
+</template>
+
+<script setup name="GameEventGroupDetail" lang="ts">
+import { ref, onMounted, computed, getCurrentInstance } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { getGameEventGroup } from '@/api/system/gameEventGroup';
+import { listGameAthlete } from '@/api/system/gameAthlete';
+import { listGameTeam } from '@/api/system/gameTeam';
+import { listGameEventProject } from '@/api/system/gameEventProject';
+import { GameEventGroupVO } from '@/api/system/gameEventGroup/types';
+import { GameAthleteVO } from '@/api/system/gameAthlete/types';
+import { GameTeamVO } from '@/api/system/gameTeam/types';
+import { GameEventProjectVO } from '@/api/system/gameEventProject/types';
+import type { ComponentInternalInstance } from 'vue';
+
+const route = useRoute();
+const router = useRouter();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+// 分组信息
+const groupInfo = ref<GameEventGroupVO>({} as GameEventGroupVO);
+// 运动员列表
+const athletes = ref<GameAthleteVO[]>([]);
+// 队伍列表
+const teams = ref<GameTeamVO[]>([]);
+// 项目列表
+const projects = ref<GameEventProjectVO[]>([]);
+// 分组结果
+const groupResult = ref<Map<string, GameAthleteVO>>(new Map());
+// 加载状态
+const loading = ref(false);
+// 生成分组状态
+const generating = ref(false);
+// 项目名称(需要从项目信息中获取)
+const projectName = computed(() => {
+  if (!groupInfo.value.projectId) return '';
+  const project = projects.value.find(p => p.projectId === groupInfo.value.projectId);
+  return project?.projectName || '';
+});
+
+// 计算总运动员数
+const totalAthletes = computed(() => {
+  return athletes.value.filter(athlete => {
+    // 检查运动员是否参与该项目
+    if (!athlete.projectList) return false;
+    
+    // 处理项目列表
+    let projectIds: string[] = [];
+    if (Array.isArray(athlete.projectList)) {
+      projectIds = athlete.projectList.map(p => p.toString());
+    } else if (typeof athlete.projectList === 'string') {
+      try {
+        projectIds = JSON.parse(athlete.projectList);
+      } catch (e) {
+        // 如果不是JSON格式,可能是逗号分隔的字符串
+        projectIds = (athlete.projectList as string).split(',').map(p => p.trim());
+      }
+    }
+    
+    const targetProjectId = groupInfo.value.projectId?.toString();
+    const hasProject = projectIds.includes(targetProjectId);
+    
+    if (!hasProject) {
+      return false;
+    }
+    
+    // 检查性别是否匹配
+    if (groupInfo.value.memberGender && groupInfo.value.memberGender !== '0') {
+      // 使用字典来匹配性别,而不是硬编码的字符串
+      if (athlete.gender?.toString() !== groupInfo.value.memberGender?.toString()) {
+        return false;
+      }
+    }
+    
+    return true;
+  }).length;
+});
+
+// 调试:打印运动员数据
+const debugAthletes = computed(() => {
+  return athletes.value.map(athlete => ({
+    id: athlete.athleteId,
+    name: athlete.name,
+    teamId: athlete.teamId,
+    projectList: athlete.projectList,
+    gender: athlete.gender
+  }));
+});
+
+// 获取道次名称
+const getTrackName = (track: number) => {
+  const trackNames = ['一', '二', '三', '四', '五', '六', '七', '八','九','十'];
+  return trackNames[track - 1] || track;
+};
+
+// 根据组别和道次获取运动员
+const getAthleteByGroupAndTrack = (groupIndex: number, track: number) => {
+  const key = `${groupIndex}-${track}`;
+  return groupResult.value.get(key);
+};
+
+// 根据队伍ID获取队伍名称
+const getTeamName = (teamId: string | number | undefined) => {
+  if (!teamId) return '';
+  const team = teams.value.find(t => t.teamId === teamId);
+  return team?.teamName || '';
+};
+
+// 获取分组信息
+const getGroupInfo = async () => {
+  try {
+    loading.value = true;
+    const groupId = route.query.id;
+    if (!groupId || Array.isArray(groupId)) {
+      proxy?.$modal.msgError('分组ID不能为空');
+      return;
+    }
+    
+    const res = await getGameEventGroup(groupId);
+    groupInfo.value = res.data;
+    
+    // 获取运动员、队伍和项目信息
+    await Promise.all([
+      getAthletes(),
+      getTeams(),
+      getProjects()
+    ]);
+    
+    // 生成分组
+    generateGroups();
+  } catch (error) {
+    console.error('获取分组信息失败:', error);
+    proxy?.$modal.msgError('获取分组信息失败');
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 获取运动员列表
+const getAthletes = async () => {
+  try {
+    const res = await listGameAthlete({
+      pageNum: 1,
+      pageSize: 1000,
+      eventId: groupInfo.value.eventId,
+      orderByColumn: '',
+      isAsc: ''
+    });
+    athletes.value = res.rows;
+  } catch (error) {
+    console.error('获取运动员列表失败:', error);
+  }
+};
+
+// 获取队伍列表
+const getTeams = async () => {
+  try {
+    const res = await listGameTeam({
+      pageNum: 1,
+      pageSize: 1000,
+      eventId: groupInfo.value.eventId,
+      orderByColumn: '',
+      isAsc: ''
+    });
+    teams.value = res.rows;
+  } catch (error) {
+    console.error('获取队伍列表失败:', error);
+  }
+};
+
+// 获取项目列表
+const getProjects = async () => {
+  try {
+    const res = await listGameEventProject({
+      pageNum: 1,
+      pageSize: 1000,
+      orderByColumn: '',
+      isAsc: ''
+    });
+    projects.value = res.rows;
+  } catch (error) {
+    console.error('获取项目列表失败:', error);
+  }
+};
+
+// 生成分组
+const generateGroups = async () => {
+  try {
+    generating.value = true;
+    
+    // 清空之前的分组结果
+    groupResult.value.clear();
+    
+    // 筛选符合条件的运动员
+    const eligibleAthletes = athletes.value.filter(athlete => {
+      
+      // 检查是否参与该项目
+      if (!athlete.projectList) {
+        return false;
+      }
+      
+      // 处理项目列表
+      let projectIds: string[] = [];
+      if (Array.isArray(athlete.projectList)) {
+        projectIds = athlete.projectList.map(p => p.toString());
+      } else if (typeof athlete.projectList === 'string') {
+        try {
+          projectIds = JSON.parse(athlete.projectList);
+        } catch (e) {
+          // 如果不是JSON格式,可能是逗号分隔的字符串
+          projectIds = (athlete.projectList as string).split(',').map(p => p.trim());
+        }
+      }
+      
+      const targetProjectId = groupInfo.value.projectId?.toString();
+      const hasProject = projectIds.includes(targetProjectId);
+      
+      if (!hasProject) {
+        return false;
+      }
+      
+      // 检查性别是否匹配
+      if (groupInfo.value.memberGender && groupInfo.value.memberGender !== '0') {
+        // 使用字典来匹配性别,而不是硬编码的字符串
+        if (athlete.gender?.toString() !== groupInfo.value.memberGender?.toString()) {
+          return false;
+        }
+      }
+      
+      return true;
+    });
+    
+    if (eligibleAthletes.length === 0) {
+      proxy?.$modal.msgWarning('没有找到符合条件的运动员');
+      return;
+    }
+    
+    // 随机打乱运动员顺序
+    const shuffledAthletes = [...eligibleAthletes].sort(() => Math.random() - 0.5);
+    
+    // 记录已分配的运动员ID,避免重复分配
+    const assignedAthleteIds = new Set();
+    
+    // 按组别和道次分配运动员
+    for (let groupIndex = 1; groupIndex <= groupInfo.value.includeGroupNum; groupIndex++) {
+      for (let track = 1; track <= groupInfo.value.trackNum; track++) {
+        // 寻找可用的运动员
+        let selectedAthlete = null;
+        let athleteIndex = 0;
+        
+        while (athleteIndex < shuffledAthletes.length && !selectedAthlete) {
+          const candidateAthlete = shuffledAthletes[athleteIndex];
+          
+          // 检查运动员是否已经被分配
+          if (assignedAthleteIds.has(candidateAthlete.athleteId)) {
+            athleteIndex++;
+            continue;
+          }
+          
+          // 检查同一组中是否已有同一队伍的运动员
+          const hasSameTeamInGroup = Array.from(groupResult.value.entries())
+            .some(([key, existingAthlete]) => {
+              const [existingGroup] = key.split('-');
+              return existingGroup === groupIndex.toString() && 
+                     existingAthlete.teamId === candidateAthlete.teamId;
+            });
+          
+          if (!hasSameTeamInGroup) {
+            selectedAthlete = candidateAthlete;
+            // 标记运动员为已分配
+            assignedAthleteIds.add(candidateAthlete.athleteId);
+          }
+          
+          athleteIndex++;
+        }
+        
+        // 如果找到了合适的运动员,分配到当前组和道次
+        if (selectedAthlete) {
+          const key = `${groupIndex}-${track}`;
+          groupResult.value.set(key, selectedAthlete);
+        }
+      }
+    }
+    
+    proxy?.$modal.msgSuccess('分组生成成功');
+  } catch (error) {
+    console.error('生成分组失败:', error);
+    proxy?.$modal.msgError('生成分组失败');
+  } finally {
+    generating.value = false;
+  }
+};
+
+// 返回上一页
+const goBack = () => {
+  router.go(-1);
+};
+
+onMounted(() => {
+  getGroupInfo();
+});
+</script>
+
+<style scoped>
+.overflow-x-auto {
+  overflow-x: auto;
+}
+
+/* 响应式表格 */
+@media (max-width: 768px) {
+  .grid {
+    grid-template-columns: repeat(1, 1fr);
+  }
+  
+  .overflow-x-auto {
+    font-size: 12px;
+  }
+  
+  .px-4 {
+    padding-left: 8px;
+    padding-right: 8px;
+  }
+  
+  .py-3 {
+    padding-top: 6px;
+    padding-bottom: 6px;
+  }
+}
+</style> 

+ 337 - 77
src/views/system/gameEventGroup/index.vue

@@ -4,18 +4,29 @@
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="赛事组别" prop="groupName">
-              <el-input v-model="queryParams.groupName" placeholder="请输入项目组名称" clearable @keyup.enter="handleQuery" />
+            <el-form-item label="项目类型" prop="projectTypeFilter">
+              <el-select v-model="projectTypeFilter" placeholder="请选择项目类型" clearable @change="handleProjectTypeFilterChange">
+                <el-option
+                  v-for="dict in game_project_type"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
             </el-form-item>
-            <!-- <el-form-item label="包含项目列表" prop="projectList">
-              <el-input v-model="queryParams.projectList" placeholder="请输入包含项目列表" clearable @keyup.enter="handleQuery" />
-            </el-form-item> -->
-            <!-- <el-form-item label="排序字段" prop="sortOrder">
-              <el-input v-model="queryParams.sortOrder" placeholder="请输入排序字段" clearable @keyup.enter="handleQuery" />
+            <el-form-item label="项目" prop="projectId">
+              <el-select v-model="queryParams.projectId" placeholder="请选择项目" clearable>
+                <el-option
+                  v-for="project in filteredProjectList"
+                  :key="project.projectId"
+                  :label="project.projectName"
+                  :value="project.projectId"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="组别" prop="groupName">
+              <el-input v-model="queryParams.groupName" placeholder="请输入组别名称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
-            <el-form-item label="排序规则(0升序1降序)" prop="sortRule">
-              <el-input v-model="queryParams.sortRule" placeholder="请输入排序规则(0升序1降序)" clearable @keyup.enter="handleQuery" />
-            </el-form-item> -->
             <el-form-item>
               <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
               <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -41,26 +52,47 @@
               >删除
             </el-button>
           </el-col>
-          <el-col :span="1.5">
+          <!-- <el-col :span="1.5">
             <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:gameEventGroup:export']">导出 </el-button>
-          </el-col>
+          </el-col> -->
           <right-toolbar v-model:showSearch="showSearch" :columns="columns" @queryTable="getList"></right-toolbar>
         </el-row>
       </template>
 
       <el-table v-loading="loading" border :data="gameEventGroupList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="主键" align="center" prop="groupId" v-if="columns[0].visible" />
-        <!-- <el-table-column label="赛事ID" align="center" prop="eventId" /> -->
-        <el-table-column label="赛事组别" align="center" prop="groupName" v-if="columns[1].visible" />
-        <el-table-column label="包含项目" align="center" prop="projectList" v-if="columns[2].visible" />
-        <el-table-column label="成员性别" align="center" prop="memberGender" v-if="columns[3].visible">
+        <el-table-column label="组别id" align="center" prop="groupId" v-if="columns[0].visible" />
+        <el-table-column label="项目类型" align="center" v-if="columns[1].visible">
+          <template #default="scope">
+            <dict-tag :options="game_project_type" :value="getProjectTypeByProjectId(scope.row.projectId) || ''" />
+          </template>
+        </el-table-column>
+        <el-table-column label="项目" align="center" v-if="columns[2].visible">
+          <template #default="scope">
+            {{ getProjectNameByProjectId(scope.row.projectId) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="组别" align="center" prop="groupName" v-if="columns[3].visible" />
+        <el-table-column label="人/组" align="center" prop="personNum" v-if="columns[4].visible" />
+        <el-table-column label="组数" align="center" prop="includeGroupNum" v-if="columns[5].visible" />
+        <el-table-column label="道数" align="center" prop="trackNum" v-if="columns[6].visible" />
+        <el-table-column label="场地数量" align="center" prop="fieldNum" v-if="columns[7].visible" />
+        <el-table-column label="每组用时(分钟)" align="center" prop="duration" v-if="columns[8].visible" />
+        <el-table-column label="比赛时间" align="center" v-if="columns[9].visible">
+          <template #default="scope">
+            {{ scope.row.beginTime }} - {{ scope.row.endTime }}
+          </template>
+        </el-table-column>
+        <el-table-column label="成员性别" align="center" prop="memberGender" v-if="columns[10].visible">
           <template #default="scope">
-            {{ genderMap[scope.row.memberGender] || '未知' }}
+            <dict-tag :options="sys_user_sex" :value="scope.row.memberGender" />
           </template>
         </el-table-column>
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template #default="scope">
+            <el-tooltip content="分组" placement="top">
+              <el-button link type="success" icon="Grid" @click="handleGroup(scope.row)" v-hasPermi="['system:gameEventGroup:edit']"></el-button>
+            </el-tooltip>
             <el-tooltip content="修改" placement="top">
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:gameEventGroup:edit']"></el-button>
             </el-tooltip>
@@ -73,31 +105,125 @@
 
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
+
     <!-- 添加或修改赛事分组对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
-      <el-form ref="gameEventGroupFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="项目组名称" prop="groupName">
-          <el-input v-model="form.groupName" placeholder="请输入项目组名称" />
-        </el-form-item>
-        <el-form-item label="包含项目" prop="projectList">
-          <el-transfer
-            v-model="form.selectedProjects"
-            :data="gameEventProjectList"
-            :titles="['可选项目', '已选项目']"
-            :button-texts="['移除', '添加']"
-            filterable
-            style="width: 100%"
-          />
-        </el-form-item>
-        <el-form-item label="成员性别" prop="memberGender">
-          <el-select v-model="form.memberGender" placeholder="请选择性别">
-            <el-option label="不分男女" value="0"></el-option>
-            <el-option label="男" value="1"></el-option>
-            <el-option label="女" value="2"></el-option>
-          </el-select>
-        </el-form-item>
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="900px" append-to-body>
+      <el-form ref="gameEventGroupFormRef" :model="form" :rules="rules" label-width="120px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="项目类型" prop="projectTypeFilter">
+              <el-select 
+                v-model="formProjectTypeFilter" 
+                placeholder="请选择项目类型" 
+                style="width: 100%"
+                @change="handleFormProjectTypeFilterChange"
+                :disabled="!!form.groupId"
+              >
+                <el-option
+                  v-for="dict in game_project_type"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="项目" prop="projectId">
+              <el-select 
+                v-model="form.projectId" 
+                placeholder="请选择项目" 
+                style="width: 100%"
+                @change="handleFormProjectChange"
+                :disabled="!formProjectTypeFilter || !!form.groupId"
+              >
+                <el-option
+                  v-for="project in filteredFormProjectList"
+                  :key="project.projectId"
+                  :label="project.projectName"
+                  :value="project.projectId"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="组别名称" prop="groupName">
+              <el-input v-model="form.groupName" placeholder="请输入组别名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="成员性别" prop="memberGender">
+              <el-select v-model="form.memberGender" placeholder="请选择性别" style="width: 100%">
+                <el-option
+                  v-for="dict in sys_user_sex"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="人/组" prop="personNum">
+              <el-input-number v-model="form.personNum" :min="1" placeholder="请输入每组人数" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="组数" prop="includeGroupNum">
+              <el-input-number v-model="form.includeGroupNum" :min="1" placeholder="请输入组数" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="道数" prop="trackNum">
+              <el-input-number v-model="form.trackNum" :min="1" placeholder="请输入道数" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="场地数量" prop="fieldNum">
+              <el-input-number v-model="form.fieldNum" :min="1" placeholder="请输入场地数量" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="每组用时(分钟)" prop="duration">
+              <el-input-number v-model="form.duration" :min="1" placeholder="请输入每组用时" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="组别开始时间" prop="beginTime">
+              <el-time-picker
+                v-model="form.beginTime"
+                placeholder="选择开始时间"
+                format="HH:mm"
+                value-format="HH:mm"
+                style="width: 100%"
+                :disabled="!form.projectId"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="预计结束时间" prop="endTime">
+              <el-input v-model="calculatedEndTime" placeholder="自动计算" disabled style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
         <el-form-item label="备注" prop="remark">
-          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注内容" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -111,22 +237,21 @@
 </template>
 
 <script setup name="GameEventGroup" lang="ts">
-import { nextTick, ref, onMounted } from 'vue';
+import { nextTick, ref, onMounted, computed } from 'vue';
+import { useRouter } from 'vue-router';
 import { listGameEventGroup, getGameEventGroup, delGameEventGroup, addGameEventGroup, updateGameEventGroup } from '@/api/system/gameEventGroup';
 import { listGameEventProject } from '@/api/system/gameEventProject';
 import { GameEventGroupVO, GameEventGroupQuery, GameEventGroupForm } from '@/api/system/gameEventGroup/types';
+import { GameEventProjectVO } from '@/api/system/gameEventProject/types';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
 
-// 性别映射
-const genderMap = {
-  '0': '不分男女',
-  '1': '男',
-  '2': '女'
-};
+// 字典数据
+const { game_project_type, sys_user_sex } = toRefs<any>(proxy?.useDict('game_project_type', 'sys_user_sex'));
 
 const gameEventGroupList = ref<GameEventGroupVO[]>([]);
-const gameEventProjectList = ref<Array<{ key: string; label: string }>>([]); // 赛事项目列表(用于穿梭框)
+const projectList = ref<GameEventProjectVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
 const showSearch = ref(true);
@@ -135,12 +260,23 @@ const single = ref(true);
 const multiple = ref(true);
 const total = ref(0);
 
+// 项目类型过滤器(用于查询和表单,不存储在数据中)
+const projectTypeFilter = ref<string>('');
+const formProjectTypeFilter = ref<string>('');
+
 // 列显隐数据
 const columns = ref<FieldOption[]>([
-  { key: 0, label: '主键', visible: false },
-  { key: 1, label: '赛事组别', visible: true },
-  { key: 2, label: '包含项目', visible: true },
-  { key: 3, label: '成员性别', visible: true },
+  { key: 0, label: '组别id', visible: false },
+  { key: 1, label: '项目类型', visible: true },
+  { key: 2, label: '项目', visible: true },
+  { key: 3, label: '组别', visible: true },
+  { key: 4, label: '人/组', visible: true },
+  { key: 5, label: '组数', visible: true },
+  { key: 6, label: '道数', visible: true },
+  { key: 7, label: '场地数量', visible: true },
+  { key: 8, label: '每组用时(分钟)', visible: true },
+  { key: 9, label: '比赛时间', visible: true },
+  { key: 10, label: '成员性别', visible: true },
 ]);
 
 const queryFormRef = ref<ElFormInstance>();
@@ -156,29 +292,86 @@ const initFormData: GameEventGroupForm = {
   eventId: undefined,
   groupName: undefined,
   projectList: undefined,
-  selectedProjects: [], // 添加已选项目列表
+  selectedProjects: [],
   memberGender: undefined,
   sortOrder: undefined,
   sortRule: undefined,
   status: undefined,
-  remark: undefined
+  remark: undefined,
+  includeGroupNum: undefined,
+  projectId: undefined,
+  personNum: undefined,
+  beginTime: undefined,
+  endTime: undefined,
+  trackNum: undefined,
+  fieldNum: undefined,
+  duration: undefined
 };
+
 const data = reactive<PageData<GameEventGroupForm, GameEventGroupQuery>>({
   form: { ...initFormData },
   queryParams: {
     pageNum: 1,
     pageSize: 10,
-    orderByColumn: '',
-    isAsc: ''
+    orderByColumn: undefined,
+    isAsc: undefined,
+    projectId: undefined,
+    groupName: undefined
   },
   rules: {
-    groupName: [{ required: true, message: '项目组名称不能为空', trigger: 'blur' }],
-    memberGender: [{ required: true, message: '成员性别不能为空', trigger: 'change' }]
+    projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
+    groupName: [{ required: true, message: '组别名称不能为空', trigger: 'blur' }],
+    memberGender: [{ required: true, message: '成员性别不能为空', trigger: 'change' }],
+    personNum: [{ required: true, message: '每组人数不能为空', trigger: 'blur' }],
+    includeGroupNum: [{ required: true, message: '组数不能为空', trigger: 'blur' }],
+    trackNum: [{ required: true, message: '道数不能为空', trigger: 'blur' }],
+    fieldNum: [{ required: true, message: '场地数量不能为空', trigger: 'blur' }],
+    duration: [{ required: true, message: '每组用时不能为空', trigger: 'blur' }],
+    beginTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }]
   }
 });
 
 const { queryParams, form, rules } = toRefs(data);
 
+// 过滤后的项目列表(用于查询)
+const filteredProjectList = computed(() => {
+  if (!projectTypeFilter.value) return projectList.value;
+  return projectList.value.filter(project => project.projectType === projectTypeFilter.value);
+});
+
+// 过滤后的项目列表(用于表单)
+const filteredFormProjectList = computed(() => {
+  if (!formProjectTypeFilter.value) return [];
+  return projectList.value.filter(project => project.projectType === formProjectTypeFilter.value);
+});
+
+// 根据项目ID获取项目类型
+const getProjectTypeByProjectId = (projectId: string | number) => {
+  if (!projectId) return '';
+  const project = projectList.value.find(p => p.projectId === projectId);
+  return project?.projectType || '';
+};
+
+// 根据项目ID获取项目名称
+const getProjectNameByProjectId = (projectId: string | number) => {
+  if (!projectId) return '';
+  const project = projectList.value.find(p => p.projectId === projectId);
+  return project?.projectName || '';
+};
+
+// 计算预计结束时间
+const calculatedEndTime = computed(() => {
+  if (!form.value.beginTime || !form.value.duration || !form.value.includeGroupNum) {
+    return '';
+  }
+  
+  const beginTime = new Date(`2000-01-01 ${form.value.beginTime}`);
+  const totalMinutes = form.value.duration * form.value.includeGroupNum;
+  const endTime = new Date(beginTime.getTime() + totalMinutes * 60 * 1000);
+  
+  return endTime.toTimeString().slice(0, 5); // 返回 HH:mm 格式
+});
+
 /** 查询赛事分组列表 */
 const getList = async () => {
   loading.value = true;
@@ -196,10 +389,33 @@ const getProjectList = async () => {
     orderByColumn: '',
     isAsc: ''
   });
-  gameEventProjectList.value = res.rows.map((item) => ({
-    key: String(item.projectId),
-    label: `${item.projectName}`
-  }));
+  projectList.value = res.rows;
+};
+
+// 查询条件中项目类型变化
+const handleProjectTypeFilterChange = () => {
+  queryParams.value.projectId = undefined;
+};
+
+// 表单中项目类型变化
+const handleFormProjectTypeFilterChange = () => {
+  form.value.projectId = undefined;
+  form.value.beginTime = undefined;
+  form.value.endTime = undefined;
+  form.value.trackNum = undefined;
+  form.value.fieldNum = undefined;
+  form.value.duration = undefined;
+};
+
+// 表单中项目变化
+const handleFormProjectChange = () => {
+  if (form.value.projectId) {
+    const selectedProject = projectList.value.find(p => p.projectId === form.value.projectId);
+    if (selectedProject) {
+      // 可以在这里设置一些默认值或者进行其他处理
+      console.log('选中的项目:', selectedProject);
+    }
+  }
 };
 
 /** 取消按钮 */
@@ -211,6 +427,7 @@ const cancel = () => {
 /** 表单重置 */
 const reset = () => {
   form.value = { ...initFormData };
+  formProjectTypeFilter.value = '';
   gameEventGroupFormRef.value?.resetFields();
 };
 
@@ -223,6 +440,7 @@ const handleQuery = () => {
 /** 重置按钮操作 */
 const resetQuery = () => {
   queryFormRef.value?.resetFields();
+  projectTypeFilter.value = '';
   handleQuery();
 };
 
@@ -251,11 +469,12 @@ const handleUpdate = async (row?: GameEventGroupVO) => {
   const res = await getGameEventGroup(_groupId);
   Object.assign(form.value, res.data);
 
-  // 处理项目列表,将逗号分隔的字符串转换为数组
-  if (res.data.projectList) {
-    form.value.selectedProjects = res.data.projectList.split(',');
-  } else {
-    form.value.selectedProjects = [];
+  // 根据项目ID设置项目类型过滤器
+  if (res.data.projectId) {
+    const project = projectList.value.find(p => p.projectId === res.data.projectId);
+    if (project) {
+      formProjectTypeFilter.value = project.projectType;
+    }
   }
 
   dialog.visible = true;
@@ -270,17 +489,52 @@ const handleUpdate = async (row?: GameEventGroupVO) => {
 const submitForm = () => {
   gameEventGroupFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
-      buttonLoading.value = true;
-      // 处理项目列表数据,将数组转换为逗号分隔的字符串
-      const submitForm = { ...form.value };
-      if (submitForm.selectedProjects && submitForm.selectedProjects.length > 0) {
-        submitForm.projectList = submitForm.selectedProjects.join(',');
-      } else {
-        submitForm.projectList = '';
+      // 校验组数和道数的关系
+      if (form.value.includeGroupNum && form.value.trackNum && form.value.personNum) {
+        const totalCapacity = form.value.includeGroupNum * form.value.trackNum;
+        if (totalCapacity < form.value.personNum) {
+          const recommendedGroupNum = Math.ceil(form.value.personNum / form.value.trackNum);
+          proxy?.$modal.msgError(`组数过小请重新输入,推荐组数:${recommendedGroupNum}`);
+          return;
+        }
+      }
+
+      // 自动计算结束时间
+      if (form.value.beginTime && form.value.duration && form.value.includeGroupNum) {
+        const beginTime = new Date(`2000-01-01 ${form.value.beginTime}`);
+        const totalMinutes = form.value.duration * form.value.includeGroupNum;
+        const endTime = new Date(beginTime.getTime() + totalMinutes * 60 * 1000);
+        form.value.endTime = endTime.toTimeString().slice(0, 5);
+      }
+
+      // 验证时间范围
+      if (form.value.beginTime && form.value.endTime) {
+        const beginTime = new Date(`2000-01-01 ${form.value.beginTime}`);
+        const endTime = new Date(`2000-01-01 ${form.value.endTime}`);
+        
+        if (beginTime >= endTime) {
+          proxy?.$modal.msgError('组别结束时间必须晚于开始时间');
+          return;
+        }
+        
+        // 验证组别时间是否在项目时间范围内
+        if (form.value.projectId) {
+          const selectedProject = projectList.value.find(p => p.projectId === form.value.projectId);
+          if (selectedProject && selectedProject.startTime && selectedProject.endTime) {
+            const projectStart = new Date(`2000-01-01 ${selectedProject.startTime}`);
+            const projectEnd = new Date(`2000-01-01 ${selectedProject.endTime}`);
+            
+            if (beginTime < projectStart || endTime > projectEnd) {
+              proxy?.$modal.msgError('组别比赛时间必须在项目比赛时间范围内');
+              return;
+            }
+          }
+        }
       }
-      // 删除selectedProjects属性,因为它不需要提交到后端
-      delete submitForm.selectedProjects;
 
+      buttonLoading.value = true;
+      const submitForm = { ...form.value };
+      
       if (form.value.groupId) {
         await updateGameEventGroup(submitForm).finally(() => (buttonLoading.value = false));
       } else {
@@ -313,7 +567,13 @@ const handleExport = () => {
   );
 };
 
+/** 分组按钮操作 */
+const handleGroup = (row: GameEventGroupVO) => {
+  router.push({ path: '/system/gameEventGroup/detail', query: { id: row.groupId } });
+};
+
 onMounted(() => {
   getList();
+  getProjectList();
 });
 </script>

+ 24 - 14
src/views/system/gameEventProject/index.vue

@@ -7,9 +7,9 @@
             <el-form-item label="项目名称" prop="projectName">
               <el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
-            <el-form-item label="项目组别" prop="groupType">
-              <el-select v-model="queryParams.groupType" placeholder="请选择项目组别" clearable @change="handleQuery">
-                <el-option v-for="group in gameEventGroupList" :key="group.groupName" :label="group.groupName" :value="group.groupName" />
+            <el-form-item label="项目归类" prop="classification">
+              <el-select v-model="queryParams.classification" placeholder="请选择项目归类" clearable @change="handleQuery">
+                <el-option v-for="dict in game_project_classification" :key="dict.value" :label="dict.label" :value="dict.value" />
               </el-select>
             </el-form-item>
             <el-form-item>
@@ -37,9 +37,9 @@
               >删除
             </el-button>
           </el-col>
-          <el-col :span="1.5">
+          <!-- <el-col :span="1.5">
             <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:gameEventProject:export']">导出 </el-button>
-          </el-col>
+          </el-col> -->
           <right-toolbar v-model:showSearch="showSearch" :columns="columns" @queryTable="getList"></right-toolbar>
         </el-row>
       </template>
@@ -53,7 +53,11 @@
             <dict-tag :options="game_project_type" :value="scope.row.projectType || ''" />
           </template>
         </el-table-column>
-        <el-table-column label="项目组别" align="center" prop="groupType" v-if="columns[3].visible" />
+        <el-table-column label="项目归类" align="center" prop="classification" v-if="columns[3].visible" >
+          <template #default="scope">
+            <dict-tag :options="game_project_classification" :value="scope.row.classification || '未知'" />
+          </template>
+        </el-table-column>
         <el-table-column label="比赛场地" align="center" prop="location" v-if="columns[4].visible" />
         <el-table-column label="计算规则" align="center" prop="scoreRule" v-if="columns[5].visible">
           <template #default="scope">
@@ -131,7 +135,11 @@
         <el-form-item label="比赛场地" prop="location">
           <el-input v-model="form.location" placeholder="请输入比赛场地" />
         </el-form-item>
-
+        <el-form-item label="归类" prop="classification">
+          <el-radio-group v-model="form.classification" style="width: 100%">
+            <el-radio v-for="dict in game_project_classification" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-radio-group>
+        </el-form-item>
         <!-- <el-form-item label="参赛组数" prop="groupNum">
           <el-input v-model="form.groupNum" placeholder="请输入参赛组数" />
         </el-form-item>
@@ -200,9 +208,10 @@ import {
 import { listGameEventGroup } from '@/api/system/gameEventGroup';
 import { GameEventProjectVO, GameEventProjectQuery, GameEventProjectForm } from '@/api/system/gameEventProject/types';
 import RefereeGroupDialog from './RefereeGroupDialog.vue';
+import { orderBy } from 'element-plus/es/components/table/src/util';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { game_score_type, game_project_type } = toRefs<any>(proxy?.useDict('game_score_type', 'game_project_type'));
+const { game_score_type, game_project_type, game_project_classification } = toRefs<any>(proxy?.useDict('game_score_type', 'game_project_type','game_project_classification'));
 
 const gameEventGroupList = ref<any[]>([]); // 赛事分组列表
 
@@ -235,7 +244,7 @@ const columns = ref<FieldOption[]>([
   { key: 0, label: '主键', visible: false },
   { key: 1, label: '项目名称', visible: true },
   { key: 2, label: '项目类型', visible: true },
-  { key: 3, label: '项目组别', visible: true },
+  { key: 3, label: '归类', visible: true },
   { key: 4, label: '比赛场地', visible: true },
   { key: 5, label: '计算规则', visible: true },
   { key: 6, label: '开始时间', visible: true },
@@ -251,7 +260,7 @@ const initFormData: GameEventProjectForm = {
   eventId: undefined,
   projectName: undefined,
   projectType: undefined,
-  groupType: undefined,
+  classification: '0',
   refereeGroups: undefined,
   location: undefined,
   startTime: undefined,
@@ -259,7 +268,7 @@ const initFormData: GameEventProjectForm = {
   groupNum: undefined,
   participateNum: undefined,
   roundType: undefined,
-  orderType: undefined,
+  orderType: '0',
   scoreRule: undefined,
   scoreValue: undefined,
   award: undefined,
@@ -273,14 +282,15 @@ const data = reactive<PageData<GameEventProjectForm, GameEventProjectQuery>>({
   queryParams: {
     pageNum: 1,
     pageSize: 10,
+    classification: undefined,
     orderByColumn: undefined,
     isAsc: undefined,
   },
   rules: {
     projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
     projectType: [{ required: true, message: '项目类型不能为空', trigger: 'change' }],
-    groupType: [{ required: true, message: '项目组别不能为空', trigger: 'change' }],
-    location: [{ required: true, message: '比赛场地不能为空', trigger: 'blur' }]
+    classification: [{ required: true, message: '归类不能为空', trigger: 'change' }],
+    orderType: [{ required: true, message: '排序方式不能为空', trigger: 'change' }],
   }
 });
 
@@ -410,7 +420,7 @@ const handleViewRefereeGroup = (refereeGroups: string[], projectName: string) =>
 
 onMounted(() => {
   getList();
-  getGameEventGroupList();
+  // getGameEventGroupList();
 });
 
 </script>

+ 152 - 202
src/views/system/gameEventSchedule/index.vue

@@ -5,14 +5,10 @@
         <div class="flex items-center justify-between">
           <div class="flex items-center gap-4">
             <h3 class="text-lg font-bold">赛事日程安排</h3>
-            <el-button type="primary" @click="loadEventData()" :disabled="!defaultEvent.eventId">
-              加载赛事数据
-            </el-button>
+            <el-button type="primary" @click="loadEventData()" :disabled="!defaultEvent.eventId"> 加载赛事数据 </el-button>
           </div>
           <div>
-            <el-button type="success" @click="exportSchedule" :disabled="!defaultEvent.eventId || scheduleData.length === 0">
-              导出日程表
-            </el-button>
+            <el-button type="success" @click="exportSchedule" :disabled="!defaultEvent.eventId || scheduleData.length === 0"> 导出日程表 </el-button>
           </div>
         </div>
       </template>
@@ -22,9 +18,7 @@
         <div class="flex items-center justify-between">
           <div>
             <h3 class="text-lg font-bold">{{ defaultEvent.eventName }}</h3>
-            <p class="text-gray-600">
-              比赛时间: {{ parseTime(defaultEvent.startTime) }} 至 {{ parseTime(defaultEvent.endTime) }}
-            </p>
+            <p class="text-gray-600">比赛时间: {{ parseTime(defaultEvent.startTime) }} 至 {{ parseTime(defaultEvent.endTime) }}</p>
           </div>
           <div>
             <p>赛事地点: {{ defaultEvent.location }}</p>
@@ -37,23 +31,18 @@
         <template #header>
           <span class="font-bold">项目分组与时间安排</span>
         </template>
-        
+
         <el-tabs v-model="activeGroupTab">
-          <el-tab-pane
-            v-for="group in projectGroups"
-            :key="group.type"
-            :label="`${group.type} (${group.projects.length})`"
-            :name="group.type"
-          >
+          <el-tab-pane v-for="group in projectGroups" :key="group.type" :label="`${group.type} (${group.projects.length})`" :name="group.type">
             <el-table :data="group.projects" border class="mb-4">
-              <el-table-column label="项目名称" prop="projectName" width="150" />
+              <el-table-column label="项目名称" prop="projectName" />
               <!-- <el-table-column label="项目组别" prop="groupType" width="120" /> -->
-              <el-table-column label="项目类型" width="100">
+              <el-table-column label="项目类型">
                 <template #default="scope">
                   <dict-tag :options="game_project_type" :value="scope.row.projectType || ''" />
                 </template>
               </el-table-column>
-              <el-table-column label="比赛日期" width="180">
+              <el-table-column label="比赛日期">
                 <template #default="scope">
                   <el-date-picker
                     v-model="scope.row.scheduleDate"
@@ -65,59 +54,35 @@
                   />
                 </template>
               </el-table-column>
-              <el-table-column label="开始时间" width="180">
+              <el-table-column label="开始时间">
                 <template #default="scope">
-                  <el-time-picker
-                    v-model="scope.row.startTime"
-                    placeholder="开始时间"
-                    format="HH:mm"
-                    value-format="HH:mm"
-                    style="width: 100%"
-                  />
+                  <el-time-picker v-model="scope.row.startTime" placeholder="开始时间" format="HH:mm" value-format="HH:mm" style="width: 100%" />
                 </template>
               </el-table-column>
-              <el-table-column label="结束时间" width="180">
+              <el-table-column label="结束时间">
                 <template #default="scope">
-                  <el-time-picker
-                    v-model="scope.row.endTime"
-                    placeholder="结束时间"
-                    format="HH:mm"
-                    value-format="HH:mm"
-                    style="width: 100%"
-                  />
+                  <el-time-picker v-model="scope.row.endTime" placeholder="结束时间" format="HH:mm" value-format="HH:mm" style="width: 100%" />
                 </template>
               </el-table-column>
-              <el-table-column label="比赛场地" width="150">
+              <el-table-column label="比赛场地">
                 <template #default="scope">
                   <el-input v-model="scope.row.location" placeholder="输入比赛场地" />
                 </template>
               </el-table-column>
-              <el-table-column label="分组数" width="150">
+              <el-table-column label="分组数">
                 <template #default="scope">
-                  <el-input-number
-                    v-model="scope.row.groupNum"
-                    :min="1"
-                    :max="20"
-                    controls-position="right"
-                    size="small"
-                  />
+                  <el-input-number v-model="scope.row.groupNum" :min="1" :max="20" controls-position="right" size="small" />
                 </template>
               </el-table-column>
-              <el-table-column label="每组人数" width="150">
+              <el-table-column label="每组人数">
                 <template #default="scope">
-                  <el-input-number
-                    v-model="scope.row.participateNum"
-                    :min="1"
-                    :max="100"
-                    controls-position="right"
-                    size="small"
-                  />
+                  <el-input-number v-model="scope.row.participateNum" :min="1" :max="100" controls-position="right" size="small" />
                 </template>
               </el-table-column>
             </el-table>
           </el-tab-pane>
         </el-tabs>
-        
+
         <div class="flex justify-end">
           <el-button type="primary" @click="saveSchedule">保存日程安排</el-button>
         </div>
@@ -128,32 +93,25 @@
         <template #header>
           <span class="font-bold">日程预览</span>
         </template>
-        
+
         <el-table :data="scheduleData" border stripe>
-          <el-table-column label="日期" prop="scheduleDate" width="120" />
-          <el-table-column label="时间" width="200">
-            <template #default="scope">
-              {{ scope.row.startTime }} - {{ scope.row.endTime }}
-            </template>
+          <el-table-column label="日期" prop="scheduleDate" />
+          <el-table-column label="时间">
+            <template #default="scope"> {{ scope.row.startTime }} - {{ scope.row.endTime }}</template>
           </el-table-column>
-          <el-table-column label="项目名称" prop="projectName" width="150" />
-          <el-table-column label="项目类型" width="100">
+          <el-table-column label="项目名称" prop="projectName" />
+          <el-table-column label="项目类型">
             <template #default="scope">
               <dict-tag :options="game_project_type" :value="scope.row.projectType || ''" />
             </template>
           </el-table-column>
           <!-- <el-table-column label="项目组别" prop="groupType" width="120" /> -->
-          <el-table-column label="比赛场地" prop="location" width="120" />
-          <el-table-column label="分组数" prop="groupNum" width="80" />
-          <el-table-column label="每组人数" prop="participateNum" width="100" />
-          <el-table-column label="操作" width="100">
+          <el-table-column label="比赛场地" prop="location" />
+          <el-table-column label="分组数" prop="groupNum" />
+          <el-table-column label="每组人数" prop="participateNum" />
+          <el-table-column label="操作">
             <template #default="scope">
-              <el-button 
-                type="danger" 
-                size="small" 
-                @click="deleteSchedule(scope.row)"
-                v-hasPermi="['system:gameeventproject:remove']"
-              >
+              <el-button type="danger" size="small" @click="deleteSchedule(scope.row)" v-hasPermi="['system:gameeventproject:remove']">
                 删除
               </el-button>
             </template>
@@ -165,44 +123,44 @@
 </template>
 
 <script setup name="GameEventSchedule" lang="ts">
-import { ref, reactive, computed, onMounted } from 'vue'
-import { listGameEventProject, updateGameEventProject } from '@/api/system/gameEventProject'
-import { getDefaultEvent } from '@/api/system/gameEvent'
-import { GameEventVO } from '@/api/system/gameEvent/types'
-import { GameEventProjectVO } from '@/api/system/gameEventProject/types'
-import { parseTime } from '@/utils/ruoyi'
-import { ElMessage, ElMessageBox } from 'element-plus'
-import DictTag from '@/components/DictTag/index.vue'
-
-const { proxy } = getCurrentInstance() as any
+import { ref, reactive, computed, onMounted } from 'vue';
+import { listGameEventProject, updateGameEventProject } from '@/api/system/gameEventProject';
+import { getDefaultEvent } from '@/api/system/gameEvent';
+import { GameEventVO } from '@/api/system/gameEvent/types';
+import { GameEventProjectVO } from '@/api/system/gameEventProject/types';
+import { parseTime } from '@/utils/ruoyi';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import DictTag from '@/components/DictTag/index.vue';
+
+const { proxy } = getCurrentInstance() as any;
 const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
 
 // 默认赛事信息
-const defaultEvent = ref<GameEventVO>({} as GameEventVO)
+const defaultEvent = ref<GameEventVO>({} as GameEventVO);
 // 项目分组(按项目类型分组)
-const projectGroups = ref<{type: string, projects: GameEventProjectVO[]}[]>([])
+const projectGroups = ref<{ type: string; projects: GameEventProjectVO[] }[]>([]);
 // 活动的项目分组标签页
-const activeGroupTab = ref('个人')
+const activeGroupTab = ref('个人');
 // 日程数据(用于预览和导出)
-const scheduleData = ref<GameEventProjectVO[]>([])
+const scheduleData = ref<GameEventProjectVO[]>([]);
 
 // 获取默认赛事
 const loadDefaultEvent = async () => {
   try {
-    const res = await getDefaultEvent()
-    defaultEvent.value = res.data
+    const res = await getDefaultEvent();
+    defaultEvent.value = res.data;
   } catch (error) {
-    ElMessage.error('获取默认赛事失败')
+    ElMessage.error('获取默认赛事失败');
   }
-}
+};
 
 // 加载赛事数据
 const loadEventData = async () => {
   if (!defaultEvent.value.eventId) {
-    ElMessage.warning('请先指定默认赛事')
-    return
+    ElMessage.warning('请先指定默认赛事');
+    return;
   }
-  
+
   try {
     // 获取赛事项目
     const projectRes = await listGameEventProject({
@@ -211,95 +169,91 @@ const loadEventData = async () => {
       pageSize: 1000,
       orderByColumn: '',
       isAsc: ''
-    })
-    
+    });
+
     // 按项目类型分组
-    const groups: { [key: string]: GameEventProjectVO[] } = {}
-    projectRes.rows.forEach(project => {
+    const groups: { [key: string]: GameEventProjectVO[] } = {};
+    projectRes.rows.forEach((project) => {
       // 使用字典标签作为分组键
-      const projectTypeDict = game_project_type.value.find((dict: any) => dict.value === project.projectType)
-      const type = projectTypeDict ? projectTypeDict.label : '未知类型'
+      const projectTypeDict = game_project_type.value.find((dict: any) => dict.value === project.projectType);
+      const type = projectTypeDict ? projectTypeDict.label : '未知类型';
       if (!groups[type]) {
-        groups[type] = []
+        groups[type] = [];
       }
       // 初始化时间字段
       // 从数据库加载时,将开始时间和结束时间拆分为日期和时间部分
       if (project.startTime) {
         // 提取日期部分
-        project.scheduleDate = project.startTime.split(' ')[0]
+        project.scheduleDate = project.startTime.split(' ')[0];
         // 只保留时间部分
-        project.startTime = project.startTime.split(' ')[1]?.substring(0, 5) || ''
+        project.startTime = project.startTime.split(' ')[1]?.substring(0, 5) || '';
       }
-      
+
       if (project.endTime) {
         // 只保留时间部分(日期部分应该与startTime相同)
-        project.endTime = project.endTime.split(' ')[1]?.substring(0, 5) || ''
+        project.endTime = project.endTime.split(' ')[1]?.substring(0, 5) || '';
       }
-      
-      if (!project.groupNum) project.groupNum = 1
-      if (!project.participateNum) project.participateNum = 1
-      groups[type].push(project)
-    })
-    
-    projectGroups.value = Object.keys(groups).map(type => ({
+
+      if (!project.groupNum) project.groupNum = 1;
+      if (!project.participateNum) project.participateNum = 1;
+      groups[type].push(project);
+    });
+
+    projectGroups.value = Object.keys(groups).map((type) => ({
       type,
       projects: groups[type]
-    }))
-    
+    }));
+
     // 设置默认活动标签页
     if (projectGroups.value.length > 0) {
-      activeGroupTab.value = projectGroups.value[0].type
+      activeGroupTab.value = projectGroups.value[0].type;
     }
-    
+
     // 更新日程预览数据
-    updateScheduleData()
-    
-    ElMessage.success('赛事数据加载成功')
+    updateScheduleData();
+
+    ElMessage.success('赛事数据加载成功');
   } catch (error) {
-    ElMessage.error('加载赛事数据失败')
+    ElMessage.error('加载赛事数据失败');
   }
-}
+};
 
 // 更新日程预览数据
 const updateScheduleData = () => {
-  const allProjects: GameEventProjectVO[] = []
-  projectGroups.value.forEach(group => {
-    group.projects.forEach(project => {
+  const allProjects: GameEventProjectVO[] = [];
+  projectGroups.value.forEach((group) => {
+    group.projects.forEach((project) => {
       // 只添加已安排时间的项目
       if (project.scheduleDate && project.startTime && project.endTime) {
-        allProjects.push(project)
+        allProjects.push(project);
       }
-    })
-  })
-  
+    });
+  });
+
   // 按日期和时间排序
   scheduleData.value = allProjects.sort((a, b) => {
     if (a.scheduleDate !== b.scheduleDate) {
-      return a.scheduleDate.localeCompare(b.scheduleDate)
+      return a.scheduleDate.localeCompare(b.scheduleDate);
     }
-    return a.startTime.localeCompare(b.startTime)
-  })
-}
+    return a.startTime.localeCompare(b.startTime);
+  });
+};
 
 // 保存日程安排
 const saveSchedule = async () => {
   try {
-    const allProjects: GameEventProjectVO[] = []
-    projectGroups.value.forEach(group => {
-      allProjects.push(...group.projects)
-    })
-    
+    const allProjects: GameEventProjectVO[] = [];
+    projectGroups.value.forEach((group) => {
+      allProjects.push(...group.projects);
+    });
+
     // 保存每个项目的时间安排
     for (const project of allProjects) {
       // 组合日期和时间部分为完整的日期时间格式
-      const startDateTime = project.scheduleDate && project.startTime 
-        ? `${project.scheduleDate} ${project.startTime}:00` 
-        : null;
-      
-      const endDateTime = project.scheduleDate && project.endTime 
-        ? `${project.scheduleDate} ${project.endTime}:00` 
-        : null;
-      
+      const startDateTime = project.scheduleDate && project.startTime ? `${project.scheduleDate} ${project.startTime}:00` : null;
+
+      const endDateTime = project.scheduleDate && project.endTime ? `${project.scheduleDate} ${project.endTime}:00` : null;
+
       await updateGameEventProject({
         projectId: project.projectId,
         startTime: startDateTime,
@@ -311,30 +265,26 @@ const saveSchedule = async () => {
         eventId: defaultEvent.value.eventId,
         projectName: project.projectName,
         projectType: project.projectType,
-        groupType: project.groupType,
-      } as any)
+        groupType: project.groupType
+      } as any);
     }
-    
-    ElMessage.success('日程安排保存成功')
-    updateScheduleData()
+
+    ElMessage.success('日程安排保存成功');
+    updateScheduleData();
   } catch (error) {
-    ElMessage.error('保存日程安排失败')
+    ElMessage.error('保存日程安排失败');
   }
-}
+};
 
 // 删除日程安排
 const deleteSchedule = async (project: GameEventProjectVO) => {
   try {
-    await ElMessageBox.confirm(
-      `确定要删除项目 "${project.projectName}" 的日程安排吗?`,
-      '删除确认',
-      {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning'
-      }
-    )
-    
+    await ElMessageBox.confirm(`确定要删除项目 "${project.projectName}" 的日程安排吗?`, '删除确认', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    });
+
     // 清除该项目的时间安排
     await updateGameEventProject({
       projectId: project.projectId,
@@ -347,45 +297,45 @@ const deleteSchedule = async (project: GameEventProjectVO) => {
       eventId: defaultEvent.value.eventId,
       projectName: project.projectName,
       projectType: project.projectType,
-      groupType: project.groupType,
-    } as any)
-    
+      groupType: project.groupType
+    } as any);
+
     // 更新本地数据
-    const group = projectGroups.value.find(g => {
-      const projectTypeDict = game_project_type.value.find((dict: any) => dict.value === project.projectType)
-      const type = projectTypeDict ? projectTypeDict.label : '未知类型'
-      return g.type === type
-    })
+    const group = projectGroups.value.find((g) => {
+      const projectTypeDict = game_project_type.value.find((dict: any) => dict.value === project.projectType);
+      const type = projectTypeDict ? projectTypeDict.label : '未知类型';
+      return g.type === type;
+    });
     if (group) {
-      const projectItem = group.projects.find(p => p.projectId === project.projectId)
+      const projectItem = group.projects.find((p) => p.projectId === project.projectId);
       if (projectItem) {
-        projectItem.scheduleDate = ''
-        projectItem.startTime = ''
-        projectItem.endTime = ''
+        projectItem.scheduleDate = '';
+        projectItem.startTime = '';
+        projectItem.endTime = '';
       }
     }
-    
-    ElMessage.success('日程安排删除成功')
-    updateScheduleData()
+
+    ElMessage.success('日程安排删除成功');
+    updateScheduleData();
   } catch (error) {
     if (error !== 'cancel') {
-      ElMessage.error('删除日程安排失败')
+      ElMessage.error('删除日程安排失败');
     }
   }
-}
+};
 
 // 导出日程表
 const exportSchedule = () => {
   if (scheduleData.value.length === 0) {
-    ElMessage.warning('暂无日程安排数据可导出')
-    return
+    ElMessage.warning('暂无日程安排数据可导出');
+    return;
   }
-  
+
   // 创建表格数据
-  const headers = ['日期', '时间', '项目名称', '项目类型', '项目组别', '比赛场地', '分组数', '每组人数']
-  let csvContent = headers.join(',') + '\n'
-  
-  scheduleData.value.forEach(item => {
+  const headers = ['日期', '时间', '项目名称', '项目类型', '项目组别', '比赛场地', '分组数', '每组人数'];
+  let csvContent = headers.join(',') + '\n';
+
+  scheduleData.value.forEach((item) => {
     const row = [
       item.scheduleDate,
       `${item.startTime}-${item.endTime}`,
@@ -395,30 +345,30 @@ const exportSchedule = () => {
       item.location,
       item.groupNum,
       item.participateNum
-    ].join(',')
-    csvContent += row + '\n'
-  })
-  
+    ].join(',');
+    csvContent += row + '\n';
+  });
+
   // 创建下载链接
-  const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' })
-  const link = document.createElement('a')
-  const url = URL.createObjectURL(blob)
-  link.setAttribute('href', url)
-  link.setAttribute('download', `赛事日程安排_${defaultEvent.value.eventName}_${parseTime(new Date(), '{y}{m}{d}')}.csv`)
-  link.style.visibility = 'hidden'
-  document.body.appendChild(link)
-  link.click()
-  document.body.removeChild(link)
-}
+  const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
+  const link = document.createElement('a');
+  const url = URL.createObjectURL(blob);
+  link.setAttribute('href', url);
+  link.setAttribute('download', `赛事日程安排_${defaultEvent.value.eventName}_${parseTime(new Date(), '{y}{m}{d}')}.csv`);
+  link.style.visibility = 'hidden';
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+};
 
 onMounted(() => {
   loadDefaultEvent().then(() => {
     // 自动加载默认赛事数据
     if (defaultEvent.value.eventId) {
-      loadEventData()
+      loadEventData();
     }
-  })
-})
+  });
+});
 </script>
 
 <style scoped>
@@ -457,4 +407,4 @@ onMounted(() => {
 .text-gray-600 {
   color: #6b7280;
 }
-</style>
+</style>

+ 42 - 40
src/views/system/gameReferee/index.vue

@@ -77,10 +77,10 @@
         <el-form-item label="密码" prop="password">
           <el-input v-model="form.password" placeholder="请输入密码" />
         </el-form-item>
-        <el-form-item label="赛事项目" prop="projectList">
+        <el-form-item label="赛事项目" prop="projectList2">
           <!-- 赛事项目穿梭框 -->
           <el-transfer
-            v-model="form.projectList"
+            v-model="form.projectList2"
             :data="allProjects"
             :titles="['可选项目', '已选项目']"
             :button-texts="['移除', '添加']"
@@ -175,7 +175,8 @@ const form = reactive({
   groupName: undefined,
   account: undefined,
   password: undefined,
-  projectList: [] as string[], // 存储所负责的项目
+  // projectList: [] as string[], // 存储所负责的项目
+  projectList2: [] as string[], // 存储所负责的项目
   refereeCode: undefined,
   createTime: undefined,
   updateTime: undefined,
@@ -205,20 +206,20 @@ const getList = async () => {
   
   // 查询裁判列表
   const res = await listGameReferee(queryParams);
-  
-  // 为每个裁判添加项目名称列表用于展示
-  for (const referee of res.rows) {
-    if (referee.projectList) {
-      // 加载所有项目信息
-      const projectRes = await listGameEventProject({
+  // 加载所有项目信息
+  const projectRes = await listGameEventProject({
         pageNum: 1,
         pageSize: 10,
         orderByColumn: undefined,
         isAsc: undefined,
       });
-      
+  
+  // 为每个裁判添加项目名称列表用于展示
+  for (const referee of res.rows) {
+    if (referee.projectList2) {
       // 获取项目ID数组
-      const projectIds = referee.projectList.split(',').filter(id => id);
+      // const projectIds = referee.projectList.split(',').filter(id => id);
+      const projectIds = referee.projectList2.filter(id => id);
       
       // 从所有项目中筛选出在projectIds中存在的项目,并提取它们的label值(项目名称)
       const projectNames = projectRes.rows
@@ -250,7 +251,8 @@ const reset = () => {
     groupName: undefined,
     account: undefined,
     password: undefined,
-    projectList: [],
+    // projectList: [],
+    projectList2: [],
     refereeCode: undefined,
     createTime: undefined,
     updateTime: undefined,
@@ -311,12 +313,13 @@ const handleUpdate = async (row?: GameRefereeVO) => {
   await loadProjects();
   
   // 处理项目列表数据格式
-  if (res.data.projectList && typeof res.data.projectList === 'string') {
-    form.projectList = res.data.projectList.split(',').filter(id => id);
-  } else if (Array.isArray(res.data.projectList)) {
-    form.projectList = res.data.projectList;
+  // if (res.data.projectList && typeof res.data.projectList === 'string') {
+  //   form.projectList = res.data.projectList.split(',').filter(id => id);
+  // } else 
+  if (Array.isArray(res.data.projectList2)) {
+    form.projectList2 = res.data.projectList2;
   } else {
-    form.projectList = [];
+    form.projectList2 = [];
   }
   
   dialog.visible = true;
@@ -330,25 +333,29 @@ const submitForm = async () => {
     buttonLoading.value = true;
     // 提交前处理项目列表数据格式
     const submitForm: any = { ...form };
-    if (Array.isArray(form.projectList)) {
-      submitForm.projectList = form.projectList.join(',');
+    // if (Array.isArray(form.projectList)) {
+    //   submitForm.projectList = form.projectList.join(',');
+    // }
+    if (form.projectList2) {
+      submitForm.projectList2 = form.projectList2;
     }
 
     try {
       if (form.refereeId) {
         // 如果是更新操作,需要先获取原裁判信息,然后更新项目表中的裁判组
         const originalReferee = await getGameReferee(form.refereeId);
-        const originalProjectList = originalReferee.data.projectList || '';
-        const newProjectList = submitForm.projectList || '';
+        const originalProjectList = originalReferee.data.projectList2 || [];
+        const newProjectList = submitForm.projectList2 || [];
         
         // 获取当前项目列表中的项目ID集合
-        const currentProjectIds = new Set<string>(newProjectList.split(',').filter(id => id.trim()));
+        // const currentProjectIds = new Set<string>(newProjectList.filter(id => id.trim()));
         
         // 获取原项目列表中的项目ID集合
-        const originalProjectIds = new Set<string>(originalProjectList.split(',').filter(id => id.trim()));
+        // const originalProjectIds = new Set<string>(originalProjectList.filter(id => id.trim()));
         
         // 筛选出在原项目列表中但不在当前项目列表中的项目(即被删除的项目)
-        const removedProjectIds = Array.from(currentProjectIds).filter(id => !originalProjectIds.has(id));
+        // const removedProjectIds = Array.from(currentProjectIds).filter(id => !originalProjectIds.has(id));
+        const removedProjectIds = newProjectList.filter(id => !originalProjectList.includes(id));
         
         // 从被删除的项目中移除该裁判
         for (const projectId of removedProjectIds) {
@@ -371,7 +378,8 @@ const submitForm = async () => {
         }
         
         // 向新项目中添加该裁判
-        const newProjectIds = Array.from<string>(currentProjectIds).filter(id => !originalProjectIds.has(id));
+        // const newProjectIds = Array.from<string>(currentProjectIds).filter(id => !originalProjectIds.has(
+        const newProjectIds = newProjectList.filter(id => !originalProjectList.includes(id));
         for (const projectId of newProjectIds) {
           const projectRes = await getGameEventProject(projectId);
           
@@ -398,8 +406,8 @@ const submitForm = async () => {
         const newRefereeId = refereeResult.data?.refereeId || refereeResult.data;
         
         // 更新所选项目的裁判组字段
-        if (newRefereeId && form.projectList.length > 0) {
-          for (const projectId of form.projectList) {
+        if (newRefereeId && form.projectList2.length > 0) {
+          for (const projectId of form.projectList2) {
             const projectRes = await getGameEventProject(projectId);
             
             if (projectRes) {
@@ -438,21 +446,15 @@ const handleDelete = async (row?: GameRefereeVO) => {
     // 如果是单个删除,需要从项目表中移除该裁判
     if (row) {
       const refereeId = row.refereeId;
-      if (row.projectList) {
-        const projectIds = row.projectList.split(',').filter(id => id.trim());
+      if (row.projectList2) {
+        const projectIds = row.projectList2.filter(id => id.trim());
         
         for (const projectId of projectIds) {
           // 获取当前项目信息
-          const projectRes = await listGameEventProject({
-            projectId: projectId.trim(),
-            pageNum: 1,
-            pageSize: 10,
-            orderByColumn: undefined,
-            isAsc: undefined,
-          });
-          
-          if (projectRes.rows.length > 0) {
-            const project = projectRes.rows[0];
+          const getProject = await getGameEventProject(projectId.trim());
+          const project = getProject.data;
+          if (project) {
+            
             let currentRefereeGroups = project.refereeGroups || [];
             
             // 从裁判组中移除该裁判ID
@@ -487,7 +489,7 @@ const handleGenerateQRCode = async (row: GameRefereeVO) => {
     name: row.name,
     account: row.account,
     password: row.password,
-    projectList: row.projectList,
+    projectList: row.projectList2,
   };
   
   try {

+ 285 - 146
src/views/system/gameScore/gameScoreEdit.vue

@@ -3,24 +3,54 @@
     <!-- 新增:顶部搜索框 -->
     <div class="flex items-center mb-4">
       <el-button type="primary" @click="refreshData"><el-icon><Refresh /></el-icon> 刷新</el-button>
-      <el-input v-model="searchValue" placeholder="输入姓名搜索" style="margin-left: 20px; width: 200px;" clearable @keyup.enter="loadAthleteScores">
+      <el-button type="success" @click="calculateRankings" :loading="rankingLoading">
+        <el-icon><Trophy /></el-icon> 计算排名
+      </el-button>
+      <el-input 
+        v-model="searchValue" 
+        :placeholder="projectClassification === '0' ? '输入运动员姓名搜索' : '输入队伍名称搜索'" 
+        style="margin-left: 20px; width: 200px;" 
+        clearable 
+        @keyup.enter="loadData">
         <template #prefix>
           <el-icon><Search /></el-icon>
         </template>
       </el-input>
+      <div class="ml-4 text-gray-600">
+        <el-tag :type="projectClassification === '0' ? 'success' : 'warning'" class="mr-2">
+          {{ projectClassification === '0' ? '个人项目' : '团体项目' }}
+        </el-tag>
+        <span>{{ projectClassification === '0' ? '管理运动员个人成绩' : '管理队伍团队成绩' }}</span>
+      </div>
     </div>
 
     <el-card shadow="never">
-      <el-table v-loading="loading" border :data="athleteScores">
+      <el-table v-loading="loading" border :data="dataList">
         <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="ID" align="center" prop="projectId" />
+        <el-table-column label="队伍ID" align="center" prop="teamId" />
         <el-table-column label="项目" align="center" prop="projectName" />
-        <el-table-column label="分组" align="center" prop="groupType" />
-        <el-table-column label="单位" align="center" prop="unit" />
-        <el-table-column label="姓名" align="center" prop="name" />
-        <el-table-column label="号码" align="center" prop="athleteCode" />
+        <el-table-column label="项目类型" align="center" prop="projectType" >
+          <template #default="scope">
+            <dict-tag :options="game_project_type" :value="scope.row.projectType" />
+          </template>
+        </el-table-column>
+        <el-table-column label="队伍名称" align="center" prop="teamName" />
+        
+        <!-- 个人项目显示运动员信息 -->
+        <template v-if="projectClassification === '0'">
+          <el-table-column label="姓名" align="center" prop="name" />
+          <el-table-column label="号码" align="center" prop="athleteCode" />
+          <el-table-column label="个人成绩" align="center" prop="individualPerformance" />
+        </template>
+        
+        <!-- 团体项目显示队伍信息 -->
+        <template v-else>
+          <!-- <el-table-column label="队伍名称" align="center" prop="teamName" /> -->
+          <!-- <el-table-column label="队伍编号" align="center" prop="teamCode" /> -->
+          <el-table-column label="团队成绩" align="center" prop="teamPerformance" />
+        </template>
+        
         <el-table-column label="积分" align="center" prop="scorePoint" />
-        <el-table-column label="成绩" align="center" prop="scoreValue" />
         <el-table-column label="排名" align="center" prop="scoreRank" />
         <el-table-column label="更新时间" align="center" prop="updateTime" />
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
@@ -34,17 +64,33 @@
     </el-card>
 
     <!-- 添加或修改裁判对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="400px" append-to-body>
       <el-form ref="scoreFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="积分" prop="scorePoint">
-          <el-input v-model="form.scorePoint" placeholder="输入积分" />
-        </el-form-item>
-        <el-form-item label="成绩" prop="scoreValue">
-          <el-input v-model="form.scoreValue" placeholder="输入成绩" />
-        </el-form-item>
-        <el-form-item label="排名" prop="scoreRank">
-          <el-input v-model="form.scoreRank" placeholder="输入排名" />
-        </el-form-item>
+        <!-- 个人项目显示个人成绩字段 -->
+        <template v-if="projectClassification === '0'">
+          <el-form-item label="个人成绩" prop="individualPerformance">
+            <el-input 
+              v-model="form.individualPerformance" 
+              placeholder="输入个人成绩" 
+              type="number"
+              step="0.01"
+              @input="handleIndividualPerformanceInput"
+              @blur="handleIndividualPerformanceBlur" />
+          </el-form-item>
+        </template>
+        
+        <!-- 团体项目显示团队成绩字段 -->
+        <template v-else>
+          <el-form-item label="团队成绩" prop="teamPerformance">
+            <el-input 
+              v-model="form.teamPerformance" 
+              placeholder="输入团队成绩" 
+              type="number"
+              step="0.01"
+              @input="handleTeamPerformanceInput"
+              @blur="handleTeamPerformanceBlur" />
+          </el-form-item>
+        </template>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -54,59 +100,35 @@
       </template>
     </el-dialog>
 
-    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="loadAthleteScores" />
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="handlePagination({page: queryParams.pageNum, limit: queryParams.pageSize})" />
   </div>
 </template>
 
 <script setup name="GameScoreEdit" lang="ts">
 import { onMounted, ref } from 'vue';
 import { useRoute } from 'vue-router';
-import { listGameAthlete } from '@/api/system/gameAthlete/index'; 
-import { listGameScore, delGameScore, getGameScore, addGameScore, updateGameScore } from '@/api/system/gameScore/index';
+import { getProjectScoreData, updateScoreAndRecalculate } from '@/api/system/gameScore/index';
 import { GameScoreForm } from '@/api/system/gameScore/types';
 import Pagination from '@/components/Pagination/index.vue';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
 
 const route = useRoute();
 const buttonLoading = ref(false);
-const athleteScores = ref<AthleteScore[]>([]);
+const rankingLoading = ref(false);
+const dataList = ref<any[]>([]);
 const loading = ref(true);
 const total = ref(0);
 
 // 定义搜索框状态变量
 const searchValue = ref('');
 
-// 定义一个简化版的类型,只包含需要的属性
-interface AthleteScore {
-  // GameAthleteVO 属性
-  athleteId?: string | number;
-  userId?: string | number;
-  eventId?: string | number;
-  teamId?: string | number;
-  athleteCode?: string;
-  name?: string;
-  unit?: string;
-  groupType?: string;
-  number?: string;
-  // GameEventProjectVO 属性 (排除scoreValue)
-  projectId?: string | number;
-  projectName?: string;
-  projectType?: string;
-  startTime?: string;
-  endTime?: string;
-  // GameScoreVO 属性
-  scoreId?: string | number;
-  scoreValue?: number; 
-  scoreRank?: number;
-  scorePoint?: number;
-  updateTime?: string;
-}
-
 const eventId = route.params.eventId as string;
 const projectId = route.params.projectId as string;
 const projectName = route.params.projectName as string;
-const groupType = route.params.groupType as string;
+const projectType = route.params.projectType as string;
+const projectClassification = route.params.classification as string || '0'; // 默认为个人项目
 
 const queryParams = reactive({
   pageNum: 1,
@@ -114,14 +136,43 @@ const queryParams = reactive({
   eventId: eventId,
   projectId: projectId,
   projectName: projectName,
-  groupType: groupType,
+  projectType: projectType,
+  classification: projectClassification,
 });
 
 //刷新数据方法
 const refreshData = () => {
   searchValue.value = '';
-  loadAthleteScores();
+  loadData(true); // 刷新时自动计算排名
+};
+
+// 计算排名方法
+const calculateRankings = async () => {
+  try {
+    rankingLoading.value = true;
+    
+    // 调用后端接口重新计算排名和积分
+    await updateScoreAndRecalculate({
+      eventId: eventId,
+      projectId: projectId,
+      scoreType: projectClassification === '0' ? 'individual' : 'team',
+      statusFlag: '0',
+      status: '0',
+      remark: '手动触发排名计算'
+    } as GameScoreForm);
+    
+    proxy?.$modal.msgSuccess("排名计算完成");
+    
+    // 重新加载数据以显示新的排名和积分
+    await loadData(false); // 不自动计算排名,避免循环
+  } catch (error) {
+    console.error("排名计算失败:", error);
+    proxy?.$modal.msgError("排名计算失败,请稍后再试");
+  } finally {
+    rankingLoading.value = false;
+  }
 };
+
 const scoreFormRef = ref<ElFormInstance>();
 const dialog = reactive<DialogOption>({
   visible: false,
@@ -129,150 +180,238 @@ const dialog = reactive<DialogOption>({
 });
 
 const form = reactive({
-  scorePoint: 0,
-  scoreValue: 0.0,
-  scoreRank: 0,
+  scorePoint: null,
+  individualPerformance: 0,
+  teamPerformance: 0,
+  scoreRank: null,
   scoreId: 0,
+  athleteId: 0,
+  teamId: 0,
+  eventId: '',
+  projectId: '',
+  athleteCode: '',
+  userId: 0,
+  teamName: '', // 个人项目和团体项目都使用
+  updateTime: '',
 });
 
 const rules = {
-  scorePoint: [
-    { required: true, message: "积分不能为空", trigger: "blur" }
-  ],
-  scoreValue: [
-    { required: true, message: "成绩不能为空", trigger: "blur" }
+  individualPerformance: [
+    { required: true, message: "个人成绩不能为空", trigger: "blur" },
+    { 
+      validator: (rule: any, value: any, callback: any) => {
+        if (value !== '' && value !== null && value !== undefined) {
+          const numValue = parseFloat(value);
+          if (isNaN(numValue)) {
+            callback(new Error('请输入有效的数字'));
+          } else if (numValue < 0) {
+            callback(new Error('成绩不能为负数'));
+          } else {
+            callback();
+          }
+        } else {
+          callback();
+        }
+      }, 
+      trigger: "blur" 
+    }
   ],
-  scoreRank: [
-    { required: true, message: "排名不能为空", trigger: "change" }
+  teamPerformance: [
+    { required: true, message: "团队成绩不能为空", trigger: "blur" },
+    { 
+      validator: (rule: any, value: any, callback: any) => {
+        if (value !== '' && value !== null && value !== undefined) {
+          const numValue = parseFloat(value);
+          if (isNaN(numValue)) {
+            callback(new Error('请输入有效的数字'));
+          } else if (numValue < 0) {
+            callback(new Error('成绩不能为负数'));
+          } else {
+            callback();
+          }
+        } else {
+          callback();
+        }
+      }, 
+      trigger: "blur" 
+    }
   ],
 }
 
+// 成绩输入处理函数
+const handleIndividualPerformanceInput = (value: string) => {
+  // 确保输入的是有效数字
+  if (value && !isNaN(parseFloat(value))) {
+    form.individualPerformance = parseFloat(value);
+  }
+};
+
+const handleIndividualPerformanceBlur = () => {
+  // 失焦时确保数据类型正确
+  if (form.individualPerformance !== null && form.individualPerformance !== undefined) {
+    const numValue = parseFloat(String(form.individualPerformance));
+    if (!isNaN(numValue)) {
+      form.individualPerformance = numValue;
+    } else {
+      form.individualPerformance = 0;
+    }
+  }
+};
+
+const handleTeamPerformanceInput = (value: string) => {
+  // 确保输入的是有效数字
+  if (value && !isNaN(parseFloat(value))) {
+    form.teamPerformance = parseFloat(value);
+  }
+};
+
+const handleTeamPerformanceBlur = () => {
+  // 失焦时确保数据类型正确
+  if (form.teamPerformance !== null && form.teamPerformance !== undefined) {
+    const numValue = parseFloat(String(form.teamPerformance));
+    if (!isNaN(numValue)) {
+      form.teamPerformance = numValue;
+    } else {
+      form.teamPerformance = 0;
+    }
+  }
+};
+
 /** 取消按钮 */
 const cancel = () => {
-  // reset();
   dialog.visible = false;
 }
+
 /** 编辑按钮 */
-const editScore = (row: AthleteScore) => {
+const editScore = (row: any) => {
   dialog.visible = true;
-  // 将当前行数据复制到编辑表单中
+  // 将当前行数据复制到编辑表单中,确保成绩数据类型正确
   Object.assign(form, {
     scoreId: row.scoreId,
-    scorePoint: row.scorePoint,
-    scoreValue: row.scoreValue,
-    scoreRank: row.scoreRank,
+    scorePoint: row.scorePoint || null,
+    individualPerformance: parseFloat(String(row.individualPerformance)) || 0,
+    teamPerformance: parseFloat(String(row.teamPerformance)) || 0,
+    scoreRank: row.scoreRank || null,
     athleteId: row.athleteId,
+    teamId: row.teamId,
     athleteCode: row.athleteCode,
     userId: row.userId,
     eventId: row.eventId,
-    teamId: row.teamId,
     projectId: row.projectId,
-    unit: row.unit,
+    teamName: row.teamName || '', // 统一使用teamName字段
     updateTime: row.updateTime,
   });
 };
 
 /** 提交按钮 */
-const submitForm = () => {
+const submitForm = async () => {
   scoreFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
       try {
         buttonLoading.value = true;
         
-        // 根据 form 中的数据创建提交对象
-        const submitForm: GameScoreForm = {
-          ...form,
-          // updateTime: new Date().toISOString() // 在提交时设置updateTime
-        };
-
-        // 调用更新或添加接口
-        if (form.scoreId) { 
-          await updateGameScore(submitForm);
+        // 确保成绩数据类型正确
+        let individualPerformance = 0;
+        let teamPerformance = 0;
+        
+        if (projectClassification === '0') {
+          // 个人项目
+          individualPerformance = parseFloat(String(form.individualPerformance)) || 0;
+          if (isNaN(individualPerformance)) {
+            individualPerformance = 0;
+          }
         } else {
-          await addGameScore(submitForm);
+          // 团体项目
+          teamPerformance = parseFloat(String(form.teamPerformance)) || 0;
+          if (isNaN(teamPerformance)) {
+            teamPerformance = 0;
+          }
         }
-
+        
+        // 构建提交数据,确保使用正确的scoreId
+        const submitForm: GameScoreForm = {
+          scoreId: form.scoreId, // 确保使用正确的scoreId
+          scorePoint: form.scorePoint || 0,
+          individualPerformance: individualPerformance,
+          teamPerformance: teamPerformance,
+          scoreRank: form.scoreRank || 0,
+          athleteId: form.athleteId,
+          teamId: form.teamId,
+          eventId: eventId,
+          projectId: projectId,
+          scoreType: projectClassification === '0' ? 'individual' : 'team',
+          statusFlag: '0',
+          status: '0',
+          remark: projectClassification === '0' ? '个人项目成绩' : '团体项目成绩'
+        };
+        
+        // 调用后端接口更新成绩并重新计算排名积分
+        await updateScoreAndRecalculate(submitForm);
+        
         proxy?.$modal.msgSuccess("操作成功");
         dialog.visible = false;
-        await loadAthleteScores(); // 重新加载数据以反映更改
+        loadData(false); // 提交成功后不自动计算排名,用户需要手动点击排名按钮
       } catch (error) {
+        console.error("提交失败:", error);
         proxy?.$modal.msgError("操作失败,请稍后再试");
       } finally {
         buttonLoading.value = false;
       }
     }
   });
-}
+};
 
-// 修改:加载运动员成绩列表方法,支持搜索
-const loadAthleteScores = async () => {
+// 加载数据方法,支持搜索
+const loadData = async (autoCalculateRanking = false) => {
   loading.value = true;
-  const resAthletes = await listGameAthlete({
-    eventId: eventId,
-    pageNum: queryParams.pageNum,
-    pageSize: queryParams.pageSize,
-    // 使用name属性替代keyword进行搜索
-    name: searchValue.value,
-    orderByColumn: '',
-    isAsc: ''
-  });
-  const athletes = resAthletes.rows;
-  total.value = resAthletes.total;
-
-  const athleteScoresData: AthleteScore[] = [];
-
-  for (const athlete of athletes) {
-    try {
-      // 使用 listGameScore 替换 getGameScore
-      const resScores = await listGameScore({
-        athleteId: athlete.athleteId,
-        pageNum: queryParams.pageNum,
-        pageSize: queryParams.pageSize,
-        orderByColumn: '',
-        isAsc: ''
-      });
-      const scores = resScores.rows;
-
-      // 找到与当前 athleteId 匹配的成绩记录
-      const score = scores.find(scoreItem => scoreItem.athleteId === athlete.athleteId);
-      //如果运动员的参与项目列表中没有项目,则跳过
-      if (athlete.projectValue === null || athlete.projectValue === '') {
-        continue;
-      }
-      athleteScoresData.push({
-        ...athlete,
-        scoreId: score?.scoreId ?? 0,
-        scorePoint: score?.scorePoint ?? 0,
-        scoreValue: score?.scoreValue ?? 0.0,
-        scoreRank: score?.scoreRank ?? 0,
-        updateTime: score?.updateTime ?? '',
-        eventId: eventId,
-        projectId: projectId, // 直接使用路由传来的 projectId
-        projectName: projectName,
-        groupType: groupType,
-      });
-    } catch (error) {
-      // 如果获取成绩失败,使用默认值
-      athleteScoresData.push({
-        ...athlete,
-        scoreId: 0,
-        scorePoint: 0,
-        scoreValue: 0.0,
-        scoreRank: 0,
-        updateTime: '',
-        eventId: eventId,
-        projectId: projectId, // 直接使用路由传来的 projectId
-        projectName: projectName,
-        groupType: groupType,
-      });
+  
+  try {
+    // 调用后端接口获取综合数据
+    const response = await getProjectScoreData({
+      eventId: eventId,
+      projectId: projectId,
+      classification: projectClassification,
+      searchValue: searchValue.value,
+      pageNum: queryParams.pageNum,
+      pageSize: queryParams.pageSize
+    });
+    
+    // 处理返回的数据
+    const rows = response.rows || [];
+    
+    // 为每行数据添加项目相关信息
+    dataList.value = rows.map((row: any) => ({
+      ...row,
+      projectName: projectName,
+      projectType: projectType,
+    }));
+    
+    total.value = response.total || 0;
+    
+    // 只有在自动计算排名的情况下才进行排名计算
+    if (autoCalculateRanking) {
+      await calculateRankings();
     }
+  } catch (error) {
+    console.error("加载数据失败:", error);
+    proxy?.$modal.msgError("数据加载失败");
+    dataList.value = [];
+    total.value = 0;
+  } finally {
+    loading.value = false;
   }
+};
 
-  athleteScores.value = athleteScoresData;
-  loading.value = false;
+// 分页组件事件处理
+const handlePagination = (paginationData: { page: number, limit: number }) => {
+  console.log('分页事件数据:', paginationData); // 添加调试日志
+  queryParams.pageNum = paginationData.page;
+  queryParams.pageSize = paginationData.limit;
+  console.log('更新后的查询参数:', queryParams); // 添加调试日志
+  loadData(false); // 分页加载不自动计算排名
 };
 
 onMounted(() => {
-  loadAthleteScores();
+  loadData(true); // 页面初始化时自动计算排名
 });
 </script>

+ 29 - 12
src/views/system/gameScore/index.vue

@@ -69,9 +69,22 @@
       <el-table v-loading="loading" border :data="projectList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="ID" align="center" prop="projectId" v-if="columns[0].visible" />
+        <el-table-column label="项目类型" align="center" prop="projectType" v-if="columns[2].visible" >
+          <template #default="scope">
+            <dict-tag :options="game_project_type" :value="scope.row.projectType" />
+          </template>
+        </el-table-column>
+        <el-table-column label="归类" align="center" prop="classification" v-if="columns[3].visible" >
+          <template #default="scope">
+            <el-tag :type="scope.row.classification === '0' ? 'success' : 'warning'">
+              {{ scope.row.classification === '0' ? '个人项目' : '团体项目' }}
+            </el-tag>
+          </template>
+        </el-table-column>
         <el-table-column label="项目" align="center" prop="projectName" v-if="columns[1].visible" />
-        <el-table-column label="分组" align="center" prop="groupType" v-if="columns[2].visible" />
-        <el-table-column label="状态" align="center" prop="status" v-if="columns[3].visible">
+        <!-- <el-table-column label="分组" align="center" prop="groupType" v-if="columns[2].visible" /> -->
+        
+        <el-table-column label="状态" align="center" prop="status" v-if="columns[4].visible">
           <template #default="scope">
             <el-select v-model="scope.row.status" placeholder="请选择状态">
               <el-option label="等待处理" value="0"></el-option>
@@ -79,11 +92,11 @@
             </el-select>
           </template>
         </el-table-column>
-        <el-table-column label="比赛时间" align="center" prop="startTime" v-if="columns[4].visible" />
-        <el-table-column label="更新时间" align="center" prop="updateTime" v-if="columns[5].visible" />
+        <el-table-column label="比赛时间" align="center" prop="startTime" v-if="columns[5].visible" />
+        <el-table-column label="更新时间" align="center" prop="updateTime" v-if="columns[6].visible" />
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template #default="scope">
-            <el-tooltip content="修改成绩" placement="top">
+            <el-tooltip content="查看成绩详情" placement="top">
               <el-button link type="primary" icon="Edit" @click="navigateToEditPage(scope.row)"></el-button>
             </el-tooltip>
           </template>
@@ -104,6 +117,7 @@ import { GameEventVO, GameEventQuery } from '@/api/system/gameEvent/types';
 import { GameEventProjectVO, GameEventProjectQuery } from '@/api/system/gameEventProject/types';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
 const router = useRouter();
 
 // 默认赛事信息
@@ -122,10 +136,11 @@ const total = ref(0);
 const columns = ref<FieldOption[]>([
   { key: 0, label: 'ID', visible: false },
   { key: 1, label: '项目', visible: true },
-  { key: 2, label: '分组', visible: true },
-  { key: 3, label: '状态', visible: true },
-  { key: 4, label: '比赛时间', visible: true },
-  { key: 5, label: '更新时间', visible: true },
+  { key: 2, label: '项目类型', visible: true },
+  { key: 3, label: '归类', visible: true },
+  { key: 4, label: '状态', visible: true },
+  { key: 5, label: '比赛时间', visible: true },
+  { key: 6, label: '更新时间', visible: true },
 ]);
 
 // 下拉框数据
@@ -139,7 +154,8 @@ const initFormData: GameScoreForm = {
   projectId: undefined,
   athleteId: undefined,
   teamId: undefined,
-  scoreValue: undefined,
+  individualPerformance: undefined,
+  teamPerformance: undefined,
   scoreType: undefined,
   scoreRank: undefined,
   scorePoint: undefined,
@@ -306,11 +322,12 @@ const handleSelectionChange = (selection: GameScoreVO[]) => {
 const navigateToEditPage = (row: GameEventProjectVO) => {
   const projectId = row.projectId;
   const projectName = row.projectName;
-  const groupType = row.groupType;
+  const projectType = row.projectType;
   const eventId = row.eventId;
+  const classification = row.classification;
   router.push({
     name: 'GameScoreEdit',
-    params: { projectId,projectName,groupType,eventId}
+    params: { projectId, projectName, projectType, eventId, classification }
   });
 };
 

+ 17 - 16
src/views/system/gameTeam/index.vue

@@ -78,11 +78,11 @@
     <!-- 添加或修改参赛队伍对话框 -->
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
       <el-form ref="gameTeamFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="赛事" prop="eventId">
+        <!-- <el-form-item label="赛事" prop="eventId">
           <el-select v-model="form.eventId" placeholder="请选择赛事" clearable filterable style="width: 100%">
             <el-option v-for="option in eventOptions" :key="option.value" :label="option.label" :value="option.value" />
           </el-select>
-        </el-form-item>
+        </el-form-item> -->
         <el-form-item label="队伍名称" prop="teamName">
           <el-input v-model="form.teamName" placeholder="请输入队伍名称" />
         </el-form-item>
@@ -166,7 +166,7 @@ const eventOptions = ref<Array<{ label: string; value: string | number }>>([]);
 const columns = ref<FieldOption[]>([
   { key: 0, label: '主键', visible: false },
   { key: 1, label: '队伍编号', visible: true },
-  { key: 2, label: '赛事名称', visible: true },
+  { key: 2, label: '赛事名称', visible: false },
   { key: 3, label: '队伍名称', visible: true },
   { key: 4, label: '团队描述', visible: true },
   { key: 5, label: '领队', visible: false },
@@ -211,7 +211,8 @@ const initFormData: GameTeamForm = {
   numberRange: undefined,
   teamDescribe: undefined,
   status: undefined,
-  remark: undefined
+  remark: undefined,
+  athleteList: []
 };
 const data = reactive<PageData<GameTeamForm, GameTeamQuery>>({
   form: { ...initFormData },
@@ -278,17 +279,17 @@ const getList = async () => {
 };
 
 /** 获取赛事选项列表 */
-const getEventOptions = async () => {
-  try {
-    const res = await getEventIdNameMap();
-    eventOptions.value = Object.entries(res.data).map(([key, value]) => ({
-      label: key as string,
-      value: value as string | number
-    }));
-  } catch (error) {
-    console.error('获取赛事列表失败:', error);
-  }
-};
+// const getEventOptions = async () => {
+//   try {
+//     const res = await getEventIdNameMap();
+//     eventOptions.value = Object.entries(res.data).map(([key, value]) => ({
+//       label: key as string,
+//       value: value as string | number
+//     }));
+//   } catch (error) {
+//     console.error('获取赛事列表失败:', error);
+//   }
+// };
 
 /** 取消按钮 */
 const cancel = () => {
@@ -411,6 +412,6 @@ const importTemplate = () => {
 
 onMounted(() => {
   getList();
-  getEventOptions();
+  // getEventOptions();
 });
 </script>

Некоторые файлы не были показаны из-за большого количества измененных файлов