Browse Source

Merge branch 'dev'

zhou 1 month ago
parent
commit
cf83514dc8

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

@@ -181,9 +181,12 @@ export const generateBib = (bgImage: File, logo: File | null, bibParam: Generate
     data: formData,
     headers: {
       'repeatSubmit': false
+      // 'Content-Type': 'multipart/form-data'
     },
     // 必须设置 responseType 为 'blob' 才能触发文件下载
-    responseType: 'blob'
+    responseType: 'blob',
+    // 增加超时时间到5分钟,避免大文件生成超时
+    timeout: 300000
   });
 };
 

+ 10 - 0
src/api/system/gameEvent/projectProgress.ts

@@ -0,0 +1,10 @@
+import request from '@/utils/request';
+import { ProjectProgressVo } from './types';
+
+// 获取赛事项目进度信息
+export function getProjectProgress(eventId: string | number) {
+  return request({
+    url: `/system/gameEvent/projectProgress/${eventId}`,
+    method: 'get'
+  });
+} 

+ 42 - 0
src/api/system/gameEvent/types.ts

@@ -240,3 +240,45 @@ export interface GameEventQuery extends PageQuery {
    */
   params?: any;
 }
+
+// 项目进度视图对象
+export interface ProjectProgressVo {
+  /** 项目ID */
+  projectId: number;
+  /** 项目名称 */
+  projectName: string;
+  /** 项目类型 */
+  projectType: string;
+  /** 归类(0个人项目/1团体项目) */
+  classification: string;
+  /** 比赛场地 */
+  location: string;
+  /** 项目开始时间 */
+  startTime: string;
+  /** 项目结束时间 */
+  endTime: string;
+  /** 状态(0未开始 1进行中 2已完成) */
+  status: string;
+  /** 状态描述 */
+  statusText: string;
+  /** 组别信息列表 */
+  groups: GroupProgressVo[];
+  /** 项目进度百分比 */
+  progressPercentage: number;
+}
+
+// 组别进度视图对象
+export interface GroupProgressVo {
+  /** 组别ID */
+  groupId: number;
+  /** 组别名称 */
+  groupName: string;
+  /** 组别开始时间 */
+  beginTime: string;
+  /** 组别结束时间 */
+  endTime: string;
+  /** 状态(0未开始 1进行中 2已完成) */
+  status: string;
+  /** 状态描述 */
+  statusText: string;
+}

+ 22 - 0
src/api/system/gameEventGroup/index.ts

@@ -61,3 +61,25 @@ export const delGameEventGroup = (groupId: string | number | Array<string | numb
     method: 'delete'
   });
 };
+
+/**
+ * 生成分组结果
+ * @param groupId
+ */
+export const generateGroups = (groupId: string | number): AxiosPromise<any> => {
+  return request({
+    url: '/system/gameEventGroup/generateGroups/' + groupId,
+    method: 'get'
+  });
+};
+
+/**
+ * 从数据库获取分组结果
+ * @param groupId
+ */
+export const getGroupResultFromDB = (groupId: string | number): AxiosPromise<any> => {
+  return request({
+    url: '/system/gameEventGroup/getGroupResultFromDB/' + groupId,
+    method: 'get'
+  });
+};

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

@@ -133,3 +133,16 @@ export const recalculateRankingsAndPoints = (eventId: string | number, projectId
     params: { eventId, projectId }
   });
 };
+
+/**
+ * 导出成绩汇总表
+ * @param eventId 赛事ID
+ */
+export const exportScoresSummary = (eventId: string | number) => {
+  return request({
+    url: '/system/gameScore/exportScoresSummary',
+    method: 'post',
+    params: { eventId },
+    responseType: 'blob' // 确保以二进制流形式接收响应
+  });
+};

+ 4 - 4
src/layout/components/Navbar.vue

@@ -277,18 +277,18 @@ watch(
     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;
@@ -298,7 +298,7 @@ watch(
       text-overflow: ellipsis;
       white-space: nowrap;
     }
-    
+
     .event-code {
       font-size: 12px;
       height: 20px;

+ 6 - 0
src/router/index.ts

@@ -143,6 +143,12 @@ export const constantRoutes: RouteRecordRaw[] = [
         name: 'GameScoreEdit',
         meta: { title: '修改成绩', icon: 'form' }
       },
+      {
+        path: 'print/:projectId',
+        component: () => import('@/views/system/gameScore/print.vue'),
+        name: 'GameScorePrint',
+        meta: { title: '打印成绩', icon: 'form' }
+      }
     ]
   },
   {

+ 3 - 2
src/utils/request.ts

@@ -28,9 +28,10 @@ axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID;
 // 创建 axios 实例
 const service = axios.create({
   baseURL: import.meta.env.VITE_APP_BASE_API,
+  // baseURL: 'http://192.168.1.126:8080',
   // baseURL: 'http://meet2.sportsrobo.club:8080',
   // baseURL: 'http://localhost:8080',
-  timeout: 50000
+  timeout: 300000 // 增加默认超时时间到5分钟
 });
 
 // 请求拦截器
@@ -123,7 +124,7 @@ service.interceptors.response.use(
     // 获取错误信息
     const msg = errorCode[code] || res.data.msg || errorCode['default'];
     // 二进制数据则直接返回
-    if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
+    if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer' || res.config.responseType === 'blob' || res.config.responseType === 'arraybuffer') {
       return res.data;
     }
     if (code === 401) {

+ 225 - 25
src/views/system/gameEvent/RankingBoardPage.vue

@@ -16,6 +16,7 @@
           <div class="ranking-list ranking-list-full">
             <div v-for="(item, index) in athleteScoreList" :key="index" class="ranking-item">
               <div class="item-content">
+                <span class="item-rank">{{ getRankDisplay(item, index, athleteScoreList) }}</span>
                 <span class="item-name">{{ item.athleteName }}</span>
                 <span class="item-team">{{ item.teamName }}</span>
                 <span class="item-time">{{ item.totalScore }}</span>
@@ -34,7 +35,7 @@
           <div class="ranking-list ranking-list-full">
             <div v-for="(item, index) in teamScores" :key="index" class="ranking-item">
               <div class="item-content">
-                <span class="item-rank">第{{ item.rank }}名</span>
+                <span class="item-rank">{{ getRankDisplay(item, index, teamScores) }}</span>
                 <span class="item-team-name">{{ item.teamName }}</span>
                 <span class="item-score">{{ item.score }}分</span>
               </div>
@@ -56,9 +57,17 @@
           <div class="ranking-list ranking-list-full">
             <div v-for="(item, index) in projectProgress" :key="index" class="ranking-item">
               <div class="item-content">
-                <span class="item-name">{{ item.name }}</span>
-                <span class="item-type">{{ item.type }}</span>
-                <span class="item-time">{{ item.time }}</span>
+                <span class="item-name">{{ item.projectName }}</span>
+                <span class="item-type">{{ getProjectTypeText(item.projectType) }} {{ item.classification === '0' ? '个人' : '团体' }}</span>
+                <span class="item-time">{{ formatTime(item.startTime) }}</span>
+              </div>
+              <!-- 如果有组别信息,显示组别详情 -->
+              <div v-if="item.groups && item.groups.length > 0" class="group-details">
+                <div v-for="group in item.groups" :key="group.groupId" class="group-item">
+                  <span class="group-name">{{ group.groupName }}</span>
+                  <span class="group-time">{{ formatTimeOnly(group.beginTime) }}</span>
+                  <span class="group-status" :class="'status-' + group.status">{{ group.statusText }}</span>
+                </div>
               </div>
             </div>
           </div>
@@ -73,8 +82,10 @@ import { ref, computed, onMounted } from 'vue';
 import { listScoreRanking, listPersonalRanking, listTeamRanking } from '@/api/system/gameEvent/eventRank';
 import { listGameScore } from '@/api/system/gameScore';
 import { listGameTeam } from '@/api/system/gameTeam';
+import { getProjectProgress } from '@/api/system/gameEvent/projectProgress';
 import { useRouter,useRoute } from 'vue-router';
 import { GameTeamVO } from '@/api/system/gameTeam/types';
+import { ProjectProgressVo, GroupProgressVo } from '@/api/system/gameEvent/types';
 
 // 定义队伍积分排行榜的数据结构
 interface TeamScore {
@@ -84,27 +95,19 @@ interface TeamScore {
   rank: number;
 }
 
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
+
 const route = useRoute();
 const eventId = route.params.eventId as string;
 
 const athleteScoreList = ref([]);
 
-const completedTasks = ref(22);
-const totalTasks = ref(33);
-const progressPercentage = computed(() => (completedTasks.value / totalTasks.value) * 100);
-
-const projectProgress = ref([
-  { name: '全部', type: '', time: '' },
-  { name: '砥砺前行', type: '团体趣味u竞赛', time: '16:24' },
-  { name: '同舟共济', type: '团体趣味u竞赛', time: '16:26' },
-  { name: '跳绳', type: '个人趣味', time: '13:25' },
-  { name: '托球跑', type: '个人趣味', time: '15:08' },
-  { name: '100m', type: '田径丙组', time: '10:18' },
-  { name: '200m', type: '田径丙组', time: '10:24' },
-  { name: '400m', type: '田径丙组', time: '09:26' },
-  { name: '800m', type: '田径丙组', time: '10:28' },
-  { name: '1500m', type: '田径丙组', time: '10:17' }
-]);
+const completedTasks = ref(0);
+const totalTasks = ref(0);
+const progressPercentage = computed(() => totalTasks.value > 0 ? (completedTasks.value / totalTasks.value) * 100 : 0);
+
+const projectProgress = ref<ProjectProgressVo[]>([]);
 
 const teamScores = ref<TeamScore[]>([]);
 
@@ -156,14 +159,155 @@ const loadTeamScores = async () => {
   // 按积分从高到低排序
   teamScoreList.sort((a, b) => b.score - a.score);
   
-  // 添加排名
-  teamScoreList.forEach((team, index) => {
-    team.rank = index + 1;
-  });
+  // 添加排名(支持并列排名)
+  let currentRank = 1;
+  for (let i = 0; i < teamScoreList.length; i++) {
+    const team = teamScoreList[i];
+    const currentScore = team.score || 0;
+    
+    // 如果不是第一个,检查是否与前一个积分相同
+    if (i > 0) {
+      const previousScore = teamScoreList[i - 1].score || 0;
+      if (currentScore !== previousScore) {
+        currentRank = i + 1;
+      }
+    }
+    
+    team.rank = currentRank;
+  }
   
   teamScores.value = teamScoreList;
 };
 
+// 加载项目进度信息
+const loadProjectProgress = async () => {
+  try {
+    const res = await getProjectProgress(eventId);
+    projectProgress.value = res.data || [];
+    
+    // 计算已完成和总任务数
+    let completed = 0;
+    let total = 0;
+    
+    projectProgress.value.forEach(project => {
+      if (project.groups && project.groups.length > 0) {
+        // 有组别的项目,统计组别
+        project.groups.forEach(group => {
+          total++;
+          if (group.status === '2') { // 已完成
+            completed++;
+          }
+        });
+      } else {
+        // 没有组别的项目,直接统计项目
+        total++;
+        if (project.status === '2') { // 已完成
+          completed++;
+        }
+      }
+    });
+    
+    completedTasks.value = completed;
+    totalTasks.value = total;
+    
+    // 前端也可以做一次排序确保,虽然后端已经排序了
+    // 按完整时间排序(项目时间或最早组别时间)
+    projectProgress.value.sort((a, b) => {
+      const getEarliestTime = (project: ProjectProgressVo) => {
+        if (project.groups && project.groups.length > 0) {
+          // 有组别,找到最早的组别时间
+          const groupTimes = project.groups
+            .map(g => g.beginTime ? new Date(g.beginTime).getTime() : 0)
+            .filter(time => time > 0);
+          if (groupTimes.length > 0) {
+            return Math.min(...groupTimes);
+          }
+        }
+        // 没有组别或组别时间无效,使用项目时间
+        return project.startTime ? new Date(project.startTime).getTime() : 0;
+      };
+      
+      const aTime = getEarliestTime(a);
+      const bTime = getEarliestTime(b);
+      
+      return aTime - bTime;
+    });
+  } catch (error) {
+    console.error('加载项目进度失败:', error);
+  }
+};
+
+// 获取项目类型文本
+const getProjectTypeText = (projectType: string) => {
+  if (!game_project_type.value || !projectType) return '未知';
+  
+  const typeItem = game_project_type.value.find((item: any) => item.value === projectType);
+  return typeItem ? typeItem.label : '未知';
+};
+
+// 格式化时间显示(包含日期)
+const formatTime = (timeStr: string) => {
+  if (!timeStr) return '-';
+  try {
+    const date = new Date(timeStr);
+    // 检查是否是有效日期
+    if (isNaN(date.getTime())) {
+      return timeStr;
+    }
+    return date.toLocaleString('zh-CN', { 
+      month: '2-digit',
+      day: '2-digit',
+      hour: '2-digit', 
+      minute: '2-digit',
+      hour12: false 
+    });
+  } catch (error) {
+    return timeStr;
+  }
+};
+
+// 格式化时间显示(仅时间,用于组别详情)
+const formatTimeOnly = (timeStr: string) => {
+  if (!timeStr) return '-';
+  try {
+    const date = new Date(timeStr);
+    // 检查是否是有效日期
+    if (isNaN(date.getTime())) {
+      return timeStr;
+    }
+    return date.toLocaleTimeString('zh-CN', { 
+      hour: '2-digit', 
+      minute: '2-digit',
+      hour12: false 
+    });
+  } catch (error) {
+    return timeStr;
+  }
+};
+
+// 获取排名显示文本(支持并列排名,排名数值连续)
+const getRankDisplay = (item: any, index: number, list: any[]) => {
+  // 如果项目有rank字段(如团队排名),直接使用
+  if (item.rank !== undefined) {
+    return `第${item.rank}名`;
+  }
+  
+  // 个人排名逻辑(排名数值连续)
+  // 计算当前项目的实际排名
+  // 排名 = 比当前积分高的不同积分数量 + 1
+  const currentScore = item.totalScore || 0;
+  const higherScores = new Set();
+  
+  for (let i = 0; i < list.length; i++) {
+    if (list[i].totalScore > currentScore) {
+      higherScores.add(list[i].totalScore);
+    }
+  }
+  
+  const actualRank = higherScores.size + 1;
+  return `第${actualRank}名`;
+};
+
 onMounted(async () => {
   const res = await listScoreRanking(eventId);
   // 按照totalScore字段降序排序(分数高的在前面)
@@ -171,6 +315,9 @@ onMounted(async () => {
   
   // 加载队伍积分排行榜
   await loadTeamScores();
+  
+  // 加载项目进度信息
+  await loadProjectProgress();
 });
 </script>
 
@@ -242,7 +389,11 @@ onMounted(async () => {
   justify-content: space-between;
 }
 
-.item-name,
+.item-name {
+  flex: 2;
+  text-align: left;
+}
+
 .item-team,
 .item-time,
 .item-type,
@@ -252,4 +403,53 @@ onMounted(async () => {
   flex: 1;
   text-align: center;
 }
+
+/* 组别详情样式 */
+.group-details {
+  margin-top: 8px;
+  padding-left: 20px;
+  border-left: 2px solid #e0e0e0;
+}
+
+.group-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 4px 0;
+  font-size: 12px;
+  color: #666;
+}
+
+.group-name {
+  flex: 3;
+  text-align: left;
+}
+
+.group-time {
+  flex: 2;
+  text-align: center;
+}
+
+.group-status {
+  flex: 1;
+  text-align: center;
+  padding: 2px 6px;
+  border-radius: 10px;
+  font-size: 10px;
+}
+
+.status-0 {
+  background-color: #f0f0f0;
+  color: #999;
+}
+
+.status-1 {
+  background-color: #e6f7ff;
+  color: #1890ff;
+}
+
+.status-2 {
+  background-color: #f6ffed;
+  color: #52c41a;
+}
 </style>

+ 677 - 0
src/views/system/gameEvent/components/bibViewerDialog.vue

@@ -0,0 +1,677 @@
+<template>
+  <!-- 生成参赛证对话框 -->
+  <el-dialog v-model="bibDialog.visible" title="生成参赛证" width="800px" append-to-body @close="handleCloseBibDialog">
+    <div class="bib-generator">
+      <el-row :gutter="20">
+        <!-- 左侧配置面板 -->
+        <el-col :span="12">
+          <el-form :model="bibForm" label-width="100px">
+            <el-form-item label="背景图片" required>
+              <el-upload ref="bgUploadRef" :limit="1" :auto-upload="false" :on-change="handleBgImageChange" accept="image/*" drag>
+                <el-icon class="el-icon--upload">
+                  <i-ep-upload-filled />
+                </el-icon>
+                <div class="el-upload__text">拖拽背景图片到此处,或<em>点击上传</em></div>
+                <div class="el-upload__tip">图片将自动调整为3:2横屏比例,建议尺寸:600×400px</div>
+              </el-upload>
+            </el-form-item>
+
+            <el-form-item label="Logo图片" required>
+              <el-upload ref="logoUploadRef" :limit="1" :auto-upload="false" :on-change="handleLogoImageChange" accept="image/*" drag>
+                <el-icon class="el-icon--upload">
+                  <i-ep-upload-filled />
+                </el-icon>
+                <div class="el-upload__text">拖拽Logo图片到此处,或<em>点击上传</em></div>
+                <div class="el-upload__tip">建议尺寸:80×80px</div>
+              </el-upload>
+            </el-form-item>
+
+            <el-form-item label="字体设置">
+              <div style="display: flex; gap: 15px; align-items: center">
+                <el-select v-model="bibForm.fontName" placeholder="字体" style="width: 100px">
+                  <el-option label="黑体" value="simhei"></el-option>
+                  <el-option label="宋体" value="simsun"></el-option>
+                  <el-option label="微软雅黑" value="yahei"></el-option>
+                </el-select>
+                <el-input-number v-model="bibForm.fontSize" :min="38" :max="198" placeholder="字体大小" style="width: 140px"></el-input-number>
+                <el-color-picker v-model="bibForm.fontColor" @change="handleFontColorChange"></el-color-picker>
+              </div>
+            </el-form-item>
+          </el-form>
+        </el-col>
+
+        <!-- 右侧预览面板 -->
+        <el-col :span="12">
+          <div class="preview-container" ref="previewContainer">
+            <div class="preview-canvas" :style="{ backgroundImage: bgImageUrl ? `url(${bgImageUrl})` : 'none' }">
+              <!-- Logo元素 -->
+              <div
+                v-if="logoImageUrl"
+                class="draggable-element logo-element"
+                :style="{
+                  left: bibForm.logoX + 'px',
+                  top: bibForm.logoY + 'px'
+                }"
+                @mousedown="startDrag($event, 'logo')"
+              >
+                <img :src="logoImageUrl" alt="Logo" style="max-width: 80px; max-height: 80px" />
+              </div>
+
+              <!-- 示例条形码 -->
+              <div
+                class="draggable-element barcode-element"
+                :style="{
+                  left: bibForm.qRCodeX + 'px',
+                  top: bibForm.qRCodeY + 'px'
+                }"
+                @mousedown="startDrag($event, 'barcode')"
+              >
+                <svg
+                  t="1755833734016"
+                  class="icon"
+                  viewBox="0 0 1024 1024"
+                  version="1.1"
+                  xmlns="http://www.w3.org/2000/svg"
+                  p-id="2399"
+                  width="32"
+                  height="32"
+                >
+                  <path
+                    d="M540.9 866h59v59h-59v-59zM422.8 423.1V98.4H98.1v324.8h59v59h59v-59h206.7z m-265.7-59V157.4h206.7v206.7H157.1z m0 0"
+                    p-id="2400"
+                  ></path>
+                  <path
+                    d="M216.2 216.4h88.6V305h-88.6v-88.6zM600 98.4v324.8h324.8V98.4H600z m265.7 265.7H659V157.4h206.7v206.7z m0 0"
+                    p-id="2401"
+                  ></path>
+                  <path
+                    d="M718.1 216.4h88.6V305h-88.6v-88.6zM216.2 718.3h88.6v88.6h-88.6v-88.6zM98.1 482.2h59v59h-59v-59z m118.1 0h59.1v59h-59.1v-59z m0 0"
+                    p-id="2402"
+                  ></path>
+                  <path
+                    d="M275.2 600.2H98.1V925h324.8V600.2h-88.6v-59h-59v59z m88.6 59.1V866H157.1V659.3h206.7z m118.1-531.4h59v88.6h-59v-88.6z m0 147.6h59v59h-59v-59zM659 482.2H540.9v-88.6h-59v88.6H334.3v59H600v59h59v-118z m0 118h59.1v59H659v-59z m-177.1 0h59v88.6h-59v-88.6z m0 147.7h59V866h-59V747.9zM600 688.8h59V866h-59V688.8z m177.1-88.6h147.6v59H777.1v-59z m88.6-118h59v59h-59v-59z m-147.6 0h118.1v59H718.1v-59z m0 206.6h59v59h-59v-59z m147.6 59.1h-29.5v59h59v-59h29.5v-59h-59v59z m-147.6 59h59V866h-59v-59.1z m59 59.1h147.6v59H777.1v-59z m0 0"
+                    p-id="2403"
+                  ></path>
+                </svg>
+              </div>
+
+              <!-- 赛事名称预览 -->
+              <div
+                class="event-name-preview"
+                :style="{
+                  fontSize: Math.min(28, Math.max(18, bibForm.fontSize * 0.7)) + 'px',
+                  color: 'black',
+                  fontFamily: '黑体'
+                }"
+              >
+                赛事名称
+              </div>
+
+              <!-- 示例数字 1234 -->
+              <div
+                class="draggable-element number-element"
+                :style="{
+                  left: '50%',
+                  top: '50%',
+                  transform: 'translate(-50%, -50%)',
+                  fontSize: Math.min(bibForm.fontSize, 56) + 'px',
+                  color: bibForm.fontColorHex,
+                  fontFamily: bibForm.fontName
+                }"
+              >
+                1234
+              </div>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="handleCloseBibDialog">取 消</el-button>
+        <el-button type="primary" @click="handleGenerateBibFile" :loading="bibDialog.loading">生成参赛证</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, nextTick, getCurrentInstance } from 'vue';
+import { generateBib } from '@/api/system/gameEvent';
+import type { UploadInstance } from 'element-plus';
+
+// 定义组件实例类型
+interface ComponentInternalInstance {
+  proxy?: {
+    $modal: {
+      msgError: (msg: string) => void;
+      msgWarning: (msg: string) => void;
+      msgSuccess: (msg: string) => void;
+    };
+  };
+}
+
+// 获取组件实例
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+// 响应式数据定义
+const bibDialog = reactive({
+  visible: false,
+  loading: false
+});
+
+const bibForm = reactive({
+  logoX: 25,
+  logoY: 25,
+  qRCodeX: 70,
+  qRCodeY: 130,
+  fontName: 'simhei',
+  fontSize: 36,
+  fontColor: '#000000',
+  fontColorHex: '#000000'
+});
+
+const bgImageFile = ref<File | null>(null);
+const logoImageFile = ref<File | null>(null);
+const bgImageUrl = ref<string>('');
+const logoImageUrl = ref<string>('');
+const previewContainer = ref<HTMLElement>();
+const bgImageDimensions = ref<{ width: number; height: number } | null>(null);
+const bgUploadRef = ref<UploadInstance>();
+const logoUploadRef = ref<UploadInstance>();
+
+// 拖拽相关
+const dragState = reactive({
+  isDragging: false,
+  dragTarget: '',
+  startX: 0,
+  startY: 0,
+  startLeft: 0,
+  startTop: 0
+});
+
+// 关闭对话框
+const handleCloseBibDialog = () => {
+  bibDialog.visible = false;
+  resetBibForm();
+
+  // 清除上传的文件
+  if (bgUploadRef.value) {
+    bgUploadRef.value.clearFiles();
+  }
+  if (logoUploadRef.value) {
+    logoUploadRef.value.clearFiles();
+  }
+};
+
+// 重置表单
+const resetBibForm = () => {
+  // 设置默认值(像素单位)- 适配2:3横屏比例
+  bibForm.logoX = 25;
+  bibForm.logoY = 25;
+  bibForm.qRCodeX = 70;
+  bibForm.qRCodeY = 130;
+  bibForm.fontName = 'simhei';
+  bibForm.fontSize = 36;
+  bibForm.fontColor = '#000000';
+  bibForm.fontColorHex = '#000000';
+  bgImageFile.value = null;
+  logoImageFile.value = null;
+  bgImageUrl.value = '';
+  logoImageUrl.value = '';
+
+  // 清除上传的文件
+  if (bgUploadRef.value) {
+    bgUploadRef.value.clearFiles();
+  }
+  if (logoUploadRef.value) {
+    logoUploadRef.value.clearFiles();
+  }
+};
+
+// 背景图片改变处理
+const handleBgImageChange = async (file: any) => {
+  if (file.raw) {
+    // 处理图片比例,转换为3:2横屏(600×400px)
+    const processedFile = await processImageToRatio(file.raw, 3, 2);
+    bgImageFile.value = processedFile;
+
+    const reader = new FileReader();
+    reader.onload = (e) => {
+      bgImageUrl.value = e.target?.result as string;
+    };
+    reader.readAsDataURL(processedFile);
+
+    // 获取处理后的图片尺寸
+    try {
+      const dimensions = await getImageDimensions(processedFile);
+      bgImageDimensions.value = dimensions;
+    } catch (error) {
+      console.error('获取图片尺寸失败:', error);
+    }
+
+    // 添加成功提示
+    proxy?.$modal.msgSuccess('背景图片上传成功');
+  }
+};
+
+// Logo图片改变处理
+const handleLogoImageChange = (file: any) => {
+  if (file.raw) {
+    logoImageFile.value = file.raw;
+    const reader = new FileReader();
+    reader.onload = (e) => {
+      logoImageUrl.value = e.target?.result as string;
+    };
+    reader.readAsDataURL(file.raw);
+
+    // 添加成功提示
+    proxy?.$modal.msgSuccess('Logo图片上传成功');
+  }
+};
+
+// 字体颜色改变处理
+const handleFontColorChange = (color: string) => {
+  bibForm.fontColor = color;
+  bibForm.fontColorHex = color;
+};
+
+// 开始拖拽
+const startDrag = (event: MouseEvent, target: string) => {
+  event.preventDefault();
+  dragState.isDragging = true;
+  dragState.dragTarget = target;
+  dragState.startX = event.clientX;
+  dragState.startY = event.clientY;
+
+  if (target === 'logo') {
+    dragState.startLeft = bibForm.logoX;
+    dragState.startTop = bibForm.logoY;
+  } else if (target === 'barcode') {
+    dragState.startLeft = bibForm.qRCodeX;
+    dragState.startTop = bibForm.qRCodeY;
+  }
+
+  document.addEventListener('mousemove', handleDrag);
+  document.addEventListener('mouseup', stopDrag);
+};
+
+// 处理拖拽
+const handleDrag = (event: MouseEvent) => {
+  if (!dragState.isDragging) return;
+
+  const deltaX = event.clientX - dragState.startX;
+  const deltaY = event.clientY - dragState.startY;
+
+  if (dragState.dragTarget === 'logo') {
+    bibForm.logoX = Math.max(0, dragState.startLeft + deltaX);
+    bibForm.logoY = Math.max(0, dragState.startTop + deltaY);
+  } else if (dragState.dragTarget === 'barcode') {
+    bibForm.qRCodeX = Math.max(0, dragState.startLeft + deltaX);
+    bibForm.qRCodeY = Math.max(0, dragState.startTop + deltaY);
+  }
+};
+
+// 停止拖拽
+const stopDrag = () => {
+  dragState.isDragging = false;
+  dragState.dragTarget = '';
+  document.removeEventListener('mousemove', handleDrag);
+  document.removeEventListener('mouseup', stopDrag);
+};
+
+// 坐标转换函数:根据3:2横屏比例的背景图片尺寸调整坐标
+const convertCoordinatesWithScale = (x: number, y: number): { x: number; y: number } => {
+  // 获取预览容器尺寸
+  const container = previewContainer.value;
+  const previewWidth = container?.clientWidth || container?.offsetWidth || 600;
+  const previewHeight = container?.clientHeight || container?.offsetHeight || 400;
+
+  // 使用实际背景图片尺寸,如果没有则使用默认3:2横屏比例尺寸(600×400px)
+  const actualWidth = bgImageDimensions.value?.width || 600;
+  const actualHeight = bgImageDimensions.value?.height || 400;
+
+  // 计算实际比例
+  const scaleX = actualWidth / previewWidth;
+  const scaleY = actualHeight / previewHeight;
+
+  // 转换坐标并翻转Y轴
+  const adjustedX = x * scaleX;
+  const adjustedY = (previewHeight - y) * scaleY; // 翻转Y轴:previewHeight - y
+
+  return {
+    x: adjustedX,
+    y: adjustedY
+  };
+};
+
+// 新增:获取背景图片实际尺寸的函数
+const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
+  return new Promise((resolve) => {
+    const img = new Image();
+    img.onload = () => {
+      resolve({ width: img.width, height: img.height });
+    };
+    img.src = URL.createObjectURL(file);
+  });
+};
+
+// 处理图片比例转换函数
+const processImageToRatio = (file: File, targetRatio: number, targetRatio2: number): Promise<File> => {
+  return new Promise((resolve) => {
+    const img = new Image();
+    img.onload = () => {
+      const canvas = document.createElement('canvas');
+      const ctx = canvas.getContext('2d');
+
+      if (!ctx) {
+        resolve(file);
+        return;
+      }
+
+      const originalWidth = img.width;
+      const originalHeight = img.height;
+      const targetRatioValue = targetRatio / targetRatio2; // 3/2 = 1.5
+
+      let newWidth, newHeight, sourceX, sourceY, sourceWidth, sourceHeight;
+
+      if (originalWidth / originalHeight > targetRatioValue) {
+        // 原图更宽,需要裁剪宽度
+        newHeight = originalHeight;
+        newWidth = originalHeight * targetRatioValue;
+        sourceX = (originalWidth - newWidth) / 2;
+        sourceY = 0;
+        sourceWidth = newWidth;
+        sourceHeight = originalHeight;
+      } else {
+        // 原图更高,需要裁剪高度
+        newWidth = originalWidth;
+        newHeight = originalWidth / targetRatioValue;
+        sourceX = 0;
+        sourceY = (originalHeight - newHeight) / 2;
+        sourceWidth = originalWidth;
+        sourceHeight = newHeight;
+      }
+
+      // 设置画布尺寸为3:2横屏比例
+      canvas.width = newWidth;
+      canvas.height = newHeight;
+
+      // 绘制裁剪后的图片
+      ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, newWidth, newHeight);
+
+      // 转换为Blob
+      canvas.toBlob(
+        (blob) => {
+          if (blob) {
+            const processedFile = new File([blob], file.name, { type: file.type });
+            resolve(processedFile);
+          } else {
+            resolve(file);
+          }
+        },
+        file.type || 'image/jpeg',
+        0.9
+      );
+    };
+    img.src = URL.createObjectURL(file);
+  });
+};
+
+// 生成参赛证文件
+const handleGenerateBibFile = async () => {
+  // 校验必须上传背景图和logo
+  if (!bgImageFile.value) {
+    proxy?.$modal.msgError('请上传背景图片');
+    return;
+  }
+
+  if (!logoImageFile.value) {
+    proxy?.$modal.msgError('请上传Logo图片');
+    return;
+  }
+
+  bibDialog.loading = true;
+  try {
+    let qRCodeX = bibForm.qRCodeX;
+    let qRCodeY = bibForm.qRCodeY;
+
+    // 如果值为null或undefined,强制使用默认值
+    if (qRCodeX === null || qRCodeX === undefined || isNaN(qRCodeX)) {
+      qRCodeX = 70;
+      console.warn('qRCodeX值异常,使用默认值70px');
+    }
+
+    if (qRCodeY === null || qRCodeY === undefined || isNaN(qRCodeY)) {
+      qRCodeY = 130;
+      console.warn('qRCodeY值异常,使用默认值130px');
+    }
+
+    // 检查背景图片尺寸
+    if (!bgImageDimensions.value) {
+      proxy?.$modal.msgWarning('正在获取背景图片尺寸,请稍后再试');
+      return;
+    }
+
+    // 等待一帧确保所有尺寸都已计算完成
+    await nextTick();
+
+    // Logo坐标(左上角)
+    const logoCoords = convertCoordinatesWithScale(bibForm.logoX || 25, bibForm.logoY || 25);
+
+    // 二维码坐标(左上角)
+    const qrCoords = convertCoordinatesWithScale(qRCodeX, qRCodeY);
+
+    const bibParams = {
+      logoX: logoCoords.x,
+      logoY: logoCoords.y,
+      qRCodeX: qrCoords.x,
+      qRCodeY: qrCoords.y,
+      fontName: bibForm.fontName || 'simhei',
+      fontSize: Math.round((bibForm.fontSize || 36) * 0.75), // 字体大小转换为PDF点并四舍五入为整数
+      fontColor: parseInt((bibForm.fontColor || '#000000').replace('#', ''), 16)
+    };
+
+    // 显示进度提示
+    proxy?.$modal.msgSuccess('正在生成参赛证,请耐心等待...');
+
+    // 添加重试机制
+    let response;
+    let retryCount = 0;
+    const maxRetries = 2;
+
+    while (retryCount <= maxRetries) {
+      try {
+        response = await generateBib(bgImageFile.value, logoImageFile.value, bibParams);
+        break; // 成功则跳出循环
+      } catch (error: any) {
+        retryCount++;
+        if (retryCount > maxRetries) {
+          throw error; // 重试次数用完,抛出错误
+        }
+
+        // 如果是连接中断或超时,等待后重试
+        if (error.message?.includes('timeout') || error.message?.includes('Network Error')) {
+          proxy?.$modal.msgWarning(`生成失败,正在重试 (${retryCount}/${maxRetries})...`);
+          await new Promise((resolve) => setTimeout(resolve, 2000)); // 等待2秒后重试
+          continue;
+        } else {
+          throw error; // 其他错误直接抛出
+        }
+      }
+    }
+
+    // 检查响应是否为有效的blob
+    if (!response || !(response instanceof Blob)) {
+      throw new Error('服务器返回的数据格式不正确');
+    }
+
+    // 处理文件下载
+    const blob = new Blob([response], { type: 'application/zip' });
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = `参赛证_${new Date().getTime()}.zip`;
+    link.click();
+    window.URL.revokeObjectURL(url);
+
+    proxy?.$modal.msgSuccess('参赛证生成成功');
+    handleCloseBibDialog();
+  } catch (error: any) {
+    console.error('生成参赛证失败:', error);
+
+    // 根据错误类型显示不同的错误信息
+    let errorMessage = '生成参赛证失败';
+    if (error.message?.includes('timeout')) {
+      errorMessage = '生成参赛证超时,请稍后重试';
+    } else if (error.message?.includes('Network Error')) {
+      errorMessage = '网络连接异常,请检查网络后重试';
+    } else if (error.message?.includes('CORS')) {
+      errorMessage = '跨域请求失败,请联系管理员';
+    } else if (error.response?.status === 500) {
+      errorMessage = '服务器内部错误,请稍后重试';
+    } else if (error.response?.status === 413) {
+      errorMessage = '文件过大,请压缩图片后重试';
+    }
+
+    proxy?.$modal.msgError(errorMessage);
+  } finally {
+    bibDialog.loading = false;
+  }
+};
+
+// 暴露方法给父组件
+defineExpose({
+  bibDialog,
+  bibForm,
+  bgImageFile,
+  logoImageFile,
+  bgImageUrl,
+  logoImageUrl,
+  bgImageDimensions,
+  handleCloseBibDialog,
+  resetBibForm,
+  handleBgImageChange,
+  handleLogoImageChange,
+  handleFontColorChange,
+  startDrag,
+  handleGenerateBibFile
+});
+</script>
+
+<style scoped lang="scss">
+/* 生成参赛证样式 */
+.bib-generator {
+  padding: 20px;
+}
+
+.bib-generator .el-upload__tip {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 8px;
+  text-align: center;
+}
+
+/* 必填项样式 */
+.bib-generator .el-form-item.is-required .el-form-item__label::before {
+  content: '*';
+  color: #f56c6c;
+  margin-right: 4px;
+}
+
+/* 上传成功状态样式 */
+.bib-generator .el-upload--success {
+  border-color: #67c23a;
+}
+
+.bib-generator .el-upload--success .el-upload__text {
+  color: #67c23a;
+}
+
+.preview-container {
+  border: 2px dashed #ddd;
+  border-radius: 8px;
+  min-height: 200px;
+  max-height: 300px;
+  position: relative;
+  overflow: hidden;
+  aspect-ratio: 3/2;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+}
+
+.preview-canvas {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+  background-color: #f5f5f5;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.draggable-element {
+  position: absolute;
+  cursor: move;
+  user-select: none;
+  z-index: 10;
+}
+
+.draggable-element:hover {
+  opacity: 0.8;
+}
+
+.logo-element {
+  border: 2px dashed transparent;
+}
+
+.logo-element:hover {
+  border-color: #409eff;
+}
+
+.barcode-element {
+  border: 2px dashed transparent;
+  padding: 5px;
+}
+
+.barcode-element:hover {
+  border-color: #67c23a;
+  background-color: rgba(103, 194, 58, 0.1);
+}
+
+.number-element {
+  font-weight: bold;
+  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
+  border: 2px dashed transparent;
+  padding: 4px;
+  white-space: nowrap;
+  user-select: none;
+  pointer-events: none;
+  text-align: center;
+  min-width: 80px;
+}
+
+.event-name-preview {
+  position: absolute;
+  top: 5%;
+  left: 50%;
+  transform: translateX(-50%);
+  font-weight: bold;
+  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
+  user-select: none;
+  pointer-events: none;
+  z-index: 5;
+  text-align: center;
+  white-space: nowrap;
+  max-width: 80%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+</style>

+ 9 - 541
src/views/system/gameEvent/index.vue

@@ -15,9 +15,6 @@
                 <el-option v-for="dict in game_event_type" :key="dict.value" :label="dict.label" :value="dict.value" />
               </el-select>
             </el-form-item>
-            <el-form-item label="开始时间" prop="startTime">
-              <el-date-picker clearable v-model="queryParams.startTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择开始时间" />
-            </el-form-item>
             <el-form-item label="是否默认赛事" prop="isDefault">
               <el-select v-model="queryParams.isDefault" placeholder="请选择是否默认赛事" clearable>
                 <el-option v-for="dict in sys_yes_no" :key="dict.value" :label="dict.label" :value="dict.value" />
@@ -178,6 +175,8 @@
     </el-card>
     <!-- 注册 RefereeForm 组件 -->
     <RefereeForm ref="refereeFormRef" />
+    <!-- 注册 BibViewerDialog 组件 -->
+    <BibViewerDialog ref="bibViewerDialogRef" />
     <!-- 排行榜对话框 -->
     <!-- <el-dialog :title="`赛事 ${currentEventId} 排行榜`" v-model="rankingBoardVisible" width="800px" append-to-body>
       <RankingBoard :eventId="currentEventId" v-if="rankingBoardVisible" />
@@ -342,162 +341,18 @@
         </div>
       </template>
     </el-dialog>
-
-    <!-- 生成参赛证对话框 -->
-    <el-dialog v-model="bibDialog.visible" title="生成参赛证" width="800px" append-to-body @close="handleCloseBibDialog">
-      <div class="bib-generator">
-        <el-row :gutter="20">
-          <!-- 左侧配置面板 -->
-          <el-col :span="12">
-            <el-form :model="bibForm" label-width="100px">
-              <el-form-item label="背景图片">
-                <el-upload ref="bgUploadRef" :limit="1" :auto-upload="false" :on-change="handleBgImageChange" accept="image/*" drag>
-                  <el-icon class="el-icon--upload">
-                    <i-ep-upload-filled />
-                  </el-icon>
-                  <div class="el-upload__text">拖拽背景图片到此处,或<em>点击上传</em></div>
-                  <div class="el-upload__tip">建议尺寸:842×595px (横向A4比例)</div>
-                </el-upload>
-              </el-form-item>
-
-              <el-form-item label="Logo图片">
-                <el-upload ref="logoUploadRef" :limit="1" :auto-upload="false" :on-change="handleLogoImageChange" accept="image/*" drag>
-                  <el-icon class="el-icon--upload">
-                    <i-ep-upload-filled />
-                  </el-icon>
-                  <div class="el-upload__text">拖拽Logo图片到此处,或<em>点击上传</em></div>
-                  <div class="el-upload__tip">建议尺寸:80×80px</div>
-                </el-upload>
-              </el-form-item>
-
-              <el-form-item label="字体设置">
-                <div style="display: flex; gap: 15px; align-items: center">
-                  <el-select v-model="bibForm.fontName" placeholder="字体" style="width: 100px">
-                    <el-option label="黑体" value="simhei"></el-option>
-                    <el-option label="宋体" value="simsun"></el-option>
-                    <el-option label="微软雅黑" value="yahei"></el-option>
-                  </el-select>
-                  <el-input-number v-model="bibForm.fontSize" :min="38" :max="198" placeholder="字体大小" style="width: 140px"></el-input-number>
-                  <el-color-picker v-model="bibForm.fontColor" @change="handleFontColorChange"></el-color-picker>
-                </div>
-              </el-form-item>
-            </el-form>
-          </el-col>
-
-          <!-- 右侧预览面板 -->
-          <el-col :span="12">
-            <div class="preview-container" ref="previewContainer">
-              <div class="preview-canvas" :style="{ backgroundImage: bgImageUrl ? `url(${bgImageUrl})` : 'none' }">
-                <!-- Logo元素 -->
-                <div
-                  v-if="logoImageUrl"
-                  class="draggable-element logo-element"
-                  :style="{
-                    left: bibForm.logoX + 'px',
-                    top: bibForm.logoY + 'px'
-                  }"
-                  @mousedown="startDrag($event, 'logo')"
-                >
-                  <img :src="logoImageUrl" alt="Logo" style="max-width: 80px; max-height: 80px" />
-                </div>
-
-                <!-- 示例条形码 -->
-                <div
-                  class="draggable-element barcode-element"
-                  :style="{
-                    left: bibForm.qRCodeX + 'px',
-                    top: bibForm.qRCodeY + 'px'
-                  }"
-                  @mousedown="startDrag($event, 'barcode')"
-                >
-                  <svg
-                    t="1755833734016"
-                    class="icon"
-                    viewBox="0 0 1024 1024"
-                    version="1.1"
-                    xmlns="http://www.w3.org/2000/svg"
-                    p-id="2399"
-                    width="32"
-                    height="32"
-                  >
-                    <path
-                      d="M540.9 866h59v59h-59v-59zM422.8 423.1V98.4H98.1v324.8h59v59h59v-59h206.7z m-265.7-59V157.4h206.7v206.7H157.1z m0 0"
-                      p-id="2400"
-                    ></path>
-                    <path
-                      d="M216.2 216.4h88.6V305h-88.6v-88.6zM600 98.4v324.8h324.8V98.4H600z m265.7 265.7H659V157.4h206.7v206.7z m0 0"
-                      p-id="2401"
-                    ></path>
-                    <path
-                      d="M718.1 216.4h88.6V305h-88.6v-88.6zM216.2 718.3h88.6v88.6h-88.6v-88.6zM98.1 482.2h59v59h-59v-59z m118.1 0h59.1v59h-59.1v-59z m0 0"
-                      p-id="2402"
-                    ></path>
-                    <path
-                      d="M275.2 600.2H98.1V925h324.8V600.2h-88.6v-59h-59v59z m88.6 59.1V866H157.1V659.3h206.7z m118.1-531.4h59v88.6h-59v-88.6z m0 147.6h59v59h-59v-59zM659 482.2H540.9v-88.6h-59v88.6H334.3v59H600v59h59v-118z m0 118h59.1v59H659v-59z m-177.1 0h59v88.6h-59v-88.6z m0 147.7h59V866h-59V747.9zM600 688.8h59V866h-59V688.8z m177.1-88.6h147.6v59H777.1v-59z m88.6-118h59v59h-59v-59z m-147.6 0h118.1v59H718.1v-59z m0 206.6h59v59h-59v-59z m147.6 59.1h-29.5v59h59v-59h29.5v-59h-59v59z m-147.6 59h59V866h-59v-59.1z m59 59.1h147.6v59H777.1v-59z m0 0"
-                      p-id="2403"
-                    ></path>
-                  </svg>
-                </div>
-
-                <!-- 赛事名称预览 -->
-                <div
-                  class="event-name-preview"
-                  :style="{
-                    fontSize: 32 + 'px',
-                    color: 'black',
-                    fontFamily: '黑体'
-                  }"
-                >
-                  赛事名称
-                </div>
-
-                <!-- 示例数字 1234 -->
-                <div
-                  class="draggable-element number-element"
-                  :style="{
-                    left: '50%',
-                    top: '50%',
-                    transform: 'translate(-50%, -50%)',
-                    fontSize: bibForm.fontSize + 'px',
-                    color: bibForm.fontColorHex,
-                    fontFamily: bibForm.fontName
-                  }"
-                >
-                  1234
-                </div>
-              </div>
-            </div>
-          </el-col>
-        </el-row>
-      </div>
-
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="handleCloseBibDialog">取 消</el-button>
-          <el-button type="primary" @click="handleGenerateBibFile" :loading="bibDialog.loading">生成参赛证</el-button>
-        </div>
-      </template>
-    </el-dialog>
   </div>
 </template>
 
 <script setup name="GameEvent" lang="ts">
-import {
-  listGameEvent,
-  changeEventDefault,
-  delGameEvent,
-  addGameEvent,
-  updateGameEvent,
-  generateNumberTable,
-  generateBib,
-  type GenerateBibBo
-} from '@/api/system/gameEvent';
+import { listGameEvent, changeEventDefault, delGameEvent, addGameEvent, updateGameEvent, generateNumberTable } from '@/api/system/gameEvent';
 import { GameEventVO, GameEventQuery, GameEventForm } from '@/api/system/gameEvent/types';
 import { getEventMdByEventAndType, editEventMd } from '@/api/system/eventMd';
 import { EventMdVO, EventMdForm } from '@/api/system/eventMd/types';
 import { useRouter } from 'vue-router';
-import { ref, nextTick } from 'vue';
+import { ref } from 'vue';
 import RefereeForm from '@/views/system/gameEvent/RefereeForm.vue';
+import BibViewerDialog from '@/views/system/gameEvent/components/bibViewerDialog.vue';
 import RankingBoard from './RankingBoard.vue';
 import Editor from '@/components/Editor/index.vue';
 import { useTagsViewStore } from '@/store/modules/tagsView';
@@ -516,6 +371,7 @@ interface RefereeFormInstance {
 }
 
 const refereeFormRef = ref<(InstanceType<typeof RefereeForm> & RefereeFormInstance) | null>(null);
+const bibViewerDialogRef = ref<InstanceType<typeof BibViewerDialog> | null>(null);
 
 const gameEventList = ref<GameEventVO[]>([]);
 const buttonLoading = ref(false);
@@ -1095,319 +951,12 @@ const handleExportNumberTableDefault = async () => {
   await proxy?.download('system/number/export', {}, `号码对照表_${new Date().getTime()}.xlsx`);
 };
 
-// 生成参赛证相关
-const bibDialog = reactive({
-  visible: false,
-  loading: false
-});
-
-const bibForm = reactive({
-  logoX: 50,
-  logoY: 50,
-  qRCodeX: 100,
-  qRCodeY: 200,
-  fontName: 'simhei',
-  fontSize: 36,
-  fontColor: '#000000',
-  fontColorHex: '#000000'
-});
-
-const bgImageFile = ref<File | null>(null);
-const logoImageFile = ref<File | null>(null);
-const bgImageUrl = ref<string>('');
-const logoImageUrl = ref<string>('');
-const previewContainer = ref<HTMLElement>();
-const bgImageDimensions = ref<{ width: number; height: number } | null>(null);
-const bgUploadRef = ref<ElUploadInstance>();
-const logoUploadRef = ref<ElUploadInstance>();
-
-// 拖拽相关
-const dragState = reactive({
-  isDragging: false,
-  dragTarget: '',
-  startX: 0,
-  startY: 0,
-  startLeft: 0,
-  startTop: 0
-});
+// 生成参赛证相关 - 已移动到 BibViewerDialog 组件中
 
 // 生成参赛证按钮处理
 const handleGenerateBib = () => {
-  // 强制设置默认值,不使用条件判断(像素单位)
-  bibForm.logoX = bibForm.logoX || 50;
-  bibForm.logoY = bibForm.logoY || 50;
-  bibForm.qRCodeX = bibForm.qRCodeX || 100; // 修正:使用独立的默认值
-  bibForm.qRCodeY = bibForm.qRCodeY || 200; // 修正:使用独立的默认值
-  bibForm.fontName = bibForm.fontName || 'simhei';
-  bibForm.fontSize = bibForm.fontSize || 36;
-  bibForm.fontColor = bibForm.fontColor || '#000000';
-  bibForm.fontColorHex = bibForm.fontColorHex || '#000000';
-  bibDialog.visible = true;
-};
-
-// 关闭对话框
-const handleCloseBibDialog = () => {
-  bibDialog.visible = false;
-  resetBibForm();
-
-  // 清除上传的文件
-  if (bgUploadRef.value) {
-    bgUploadRef.value.clearFiles();
-  }
-  if (logoUploadRef.value) {
-    logoUploadRef.value.clearFiles();
-  }
-};
-
-// 重置表单
-const resetBibForm = () => {
-  // 设置默认值(像素单位)
-  bibForm.logoX = 50;
-  bibForm.logoY = 50;
-  bibForm.qRCodeX = 100;
-  bibForm.qRCodeY = 200;
-  bibForm.fontName = 'simhei';
-  bibForm.fontSize = 36;
-  bibForm.fontColor = '#000000';
-  bibForm.fontColorHex = '#000000';
-  bgImageFile.value = null;
-  logoImageFile.value = null;
-  bgImageUrl.value = '';
-  logoImageUrl.value = '';
-
-  // 清除上传的文件
-  if (bgUploadRef.value) {
-    bgUploadRef.value.clearFiles();
-  }
-  if (logoUploadRef.value) {
-    logoUploadRef.value.clearFiles();
-  }
-};
-
-// 背景图片改变处理
-const handleBgImageChange = async (file: any) => {
-  if (file.raw) {
-    bgImageFile.value = file.raw;
-    const reader = new FileReader();
-    reader.onload = (e) => {
-      bgImageUrl.value = e.target?.result as string;
-    };
-    reader.readAsDataURL(file.raw);
-
-    // 获取背景图片的实际尺寸
-    try {
-      const dimensions = await getImageDimensions(file.raw);
-      bgImageDimensions.value = dimensions;
-    } catch (error) {
-      console.error('获取图片尺寸失败:', error);
-    }
-  }
-};
-
-// Logo图片改变处理
-const handleLogoImageChange = (file: any) => {
-  if (file.raw) {
-    logoImageFile.value = file.raw;
-    const reader = new FileReader();
-    reader.onload = (e) => {
-      logoImageUrl.value = e.target?.result as string;
-    };
-    reader.readAsDataURL(file.raw);
-  }
-};
-
-// 字体颜色改变处理
-const handleFontColorChange = (color: string) => {
-  bibForm.fontColor = color;
-  bibForm.fontColorHex = color;
-};
-
-// 开始拖拽
-const startDrag = (event: MouseEvent, target: string) => {
-  event.preventDefault();
-  dragState.isDragging = true;
-  dragState.dragTarget = target;
-  dragState.startX = event.clientX;
-  dragState.startY = event.clientY;
-
-  if (target === 'logo') {
-    dragState.startLeft = bibForm.logoX;
-    dragState.startTop = bibForm.logoY;
-  } else if (target === 'barcode') {
-    dragState.startLeft = bibForm.qRCodeX;
-    dragState.startTop = bibForm.qRCodeY;
-  }
-
-  document.addEventListener('mousemove', handleDrag);
-  document.addEventListener('mouseup', stopDrag);
-};
-
-// 处理拖拽
-const handleDrag = (event: MouseEvent) => {
-  if (!dragState.isDragging) return;
-
-  const deltaX = event.clientX - dragState.startX;
-  const deltaY = event.clientY - dragState.startY;
-
-  if (dragState.dragTarget === 'logo') {
-    bibForm.logoX = Math.max(0, dragState.startLeft + deltaX);
-    bibForm.logoY = Math.max(0, dragState.startTop + deltaY);
-  } else if (dragState.dragTarget === 'barcode') {
-    bibForm.qRCodeX = Math.max(0, dragState.startLeft + deltaX);
-    bibForm.qRCodeY = Math.max(0, dragState.startTop + deltaY);
-  }
-};
-
-// 停止拖拽
-const stopDrag = () => {
-  dragState.isDragging = false;
-  dragState.dragTarget = '';
-  document.removeEventListener('mousemove', handleDrag);
-  document.removeEventListener('mouseup', stopDrag);
-};
-
-// 坐标转换函数:根据实际背景图片尺寸调整坐标
-const convertCoordinatesWithScale = (x: number, y: number): { x: number; y: number } => {
-  // 获取预览容器尺寸,使用更精确的方法
-  const container = previewContainer.value;
-  const previewWidth = container?.clientWidth || container?.offsetWidth || 400;
-  const previewHeight = container?.clientHeight || container?.offsetHeight || 400;
-
-  // 使用实际背景图片尺寸,如果没有则使用默认A4尺寸
-  const actualWidth = bgImageDimensions.value?.width || 595;
-  const actualHeight = bgImageDimensions.value?.height || 842;
-
-  // 计算实际比例
-  const scaleX = actualWidth / previewWidth;
-  const scaleY = actualHeight / previewHeight;
-
-  // 微调系数,用于补偿微小偏差
-  const fineTuneX = 1.15; // 进一步缩小X坐标,让元素向左移动
-  const fineTuneY = 0.9; // 进一步放大Y坐标,让元素向下移动
-
-  // 根据实际效果图,需要调整坐标系统
-  // 实际效果显示Logo在左上角,二维码在左下角
-  const adjustedX = x * scaleX * fineTuneX;
-  const adjustedY = (previewHeight - y) * scaleY * fineTuneY;
-
-  // 添加额外的偏移量调整
-  const offsetX = 8; // 向右偏移8pt(进一步减少向右偏移)
-  const offsetY = 65; // 向下偏移65pt(进一步增加向下偏移)
-
-  // 根据元素类型进行特殊调整
-  let finalX = adjustedX + offsetX;
-  let finalY = adjustedY + offsetY;
-
-  // 如果是Logo,进行特殊调整
-  if (x < 100 && y < 100) {
-    // 假设Logo在左上角区域
-    finalX += 5; // Logo额外向右偏移
-    finalY -= 10; // Logo额外向下偏移
-  }
-
-  // 如果是二维码,进行特殊调整
-  if (x > 200 && y > 200) {
-    // 假设二维码在右下角区域
-    finalX -= 3; // 二维码额外向左偏移
-    finalY += 5; // 二维码额外向下偏移
-  }
-
-  return {
-    x: finalX,
-    y: finalY
-  };
-};
-
-// 新增:获取背景图片实际尺寸的函数
-const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
-  return new Promise((resolve) => {
-    const img = new Image();
-    img.onload = () => {
-      resolve({ width: img.width, height: img.height });
-    };
-    img.src = URL.createObjectURL(file);
-  });
-};
-
-// 生成参赛证文件
-const handleGenerateBibFile = async () => {
-  if (!bgImageFile.value) {
-    proxy?.$modal.msgError('请上传背景图片');
-    return;
-  }
-
-  bibDialog.loading = true;
-  try {
-    let qRCodeX = bibForm.qRCodeX;
-    let qRCodeY = bibForm.qRCodeY;
-
-    // 如果值为null或undefined,强制使用默认值
-    if (qRCodeX === null || qRCodeX === undefined || isNaN(qRCodeX)) {
-      qRCodeX = 100;
-      console.warn('qRCodeX值异常,使用默认值100px');
-    }
-
-    if (qRCodeY === null || qRCodeY === undefined || isNaN(qRCodeY)) {
-      qRCodeY = 200;
-      console.warn('qRCodeY值异常,使用默认值200px');
-    }
-
-    // 获取预览容器的高度用于坐标转换
-    const containerHeight = previewContainer.value?.clientHeight || 400;
-
-    // 检查背景图片尺寸
-    if (!bgImageDimensions.value) {
-      proxy?.$modal.msgWarning('正在获取背景图片尺寸,请稍后再试');
-      return;
-    }
-
-    // 等待一帧确保所有尺寸都已计算完成
-    await nextTick();
-
-    // Logo坐标(左上角)
-    const logoCoords = convertCoordinatesWithScale(bibForm.logoX || 50, bibForm.logoY || 50);
-
-    // 二维码坐标(左上角)
-    const qrCoords = convertCoordinatesWithScale(qRCodeX, qRCodeY);
-
-    const bibParams = {
-      logoX: logoCoords.x,
-      logoY: logoCoords.y,
-      qRCodeX: qrCoords.x,
-      qRCodeY: qrCoords.y,
-      fontName: bibForm.fontName || 'simhei',
-      fontSize: Math.round((bibForm.fontSize || 36) * 0.75), // 字体大小转换为PDF点并四舍五入为整数
-      fontColor: parseInt((bibForm.fontColor || '#000000').replace('#', ''), 16)
-    };
-
-    // 最后一次检查,确保二维码坐标不为null
-    if (bibParams.qRCodeX === null || bibParams.qRCodeY === undefined) {
-      bibParams.qRCodeX = 148.75; // 100px * 1.4875
-      console.error('最后一次修复:qRCodeX仍为null,设置为100px转换后的值');
-    }
-    if (bibParams.qRCodeY === null || bibParams.qRCodeY === undefined) {
-      bibParams.qRCodeY = 421; // 200px * 2.105
-      console.error('最后一次修复:qRCodeY仍为null,设置为200px转换后的值');
-    }
-
-    const response = await generateBib(bgImageFile.value, logoImageFile.value, bibParams);
-
-    // 处理文件下载 - response已经是blob数据,不需要再访问.data属性
-    const blob = new Blob([response as any], { type: 'application/zip' });
-    const url = window.URL.createObjectURL(blob);
-    const link = document.createElement('a');
-    link.href = url;
-    link.download = `参赛证_${new Date().getTime()}.zip`;
-    link.click();
-    window.URL.revokeObjectURL(url);
-
-    proxy?.$modal.msgSuccess('参赛证生成成功');
-    handleCloseBibDialog();
-  } catch (error) {
-    console.error('生成参赛证失败:', error);
-    proxy?.$modal.msgError('生成参赛证失败');
-  } finally {
-    bibDialog.loading = false;
+  if (bibViewerDialogRef.value) {
+    bibViewerDialogRef.value.bibDialog.visible = true;
   }
 };
 
@@ -1458,87 +1007,6 @@ onActivated(() => {
   justify-content: center;
 }
 
-/* 生成参赛证样式 */
-.bib-generator {
-  padding: 20px;
-}
-
-.bib-generator .el-upload__tip {
-  color: #909399;
-  font-size: 12px;
-  margin-top: 8px;
-  text-align: center;
-}
-
-.preview-container {
-  border: 2px dashed #ddd;
-  border-radius: 8px;
-  min-height: 400px;
-  position: relative;
-  overflow: hidden;
-}
-
-.preview-canvas {
-  width: 100%;
-  height: 400px;
-  position: relative;
-  background-size: cover;
-  background-position: center;
-  background-repeat: no-repeat;
-  background-color: #f5f5f5;
-}
-
-.draggable-element {
-  position: absolute;
-  cursor: move;
-  user-select: none;
-  z-index: 10;
-}
-
-.draggable-element:hover {
-  opacity: 0.8;
-}
-
-.logo-element {
-  border: 2px dashed transparent;
-}
-
-.logo-element:hover {
-  border-color: #409eff;
-}
-
-.barcode-element {
-  border: 2px dashed transparent;
-  padding: 5px;
-}
-
-.barcode-element:hover {
-  border-color: #67c23a;
-  background-color: rgba(103, 194, 58, 0.1);
-}
-
-.number-element {
-  font-weight: bold;
-  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
-  border: 2px dashed transparent;
-  padding: 5px;
-  white-space: nowrap;
-  user-select: none;
-  pointer-events: none;
-}
-
-.event-name-preview {
-  position: absolute;
-  top: 20px;
-  left: 50%;
-  transform: translateX(-50%);
-  font-weight: bold;
-  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
-  user-select: none;
-  pointer-events: none;
-  z-index: 5;
-}
-
 .operation-buttons .el-button:hover {
   transform: translateY(-1px);
   box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);

+ 96 - 225
src/views/system/gameEventGroup/detail.vue

@@ -15,7 +15,7 @@
           </div>
         </div>
         <div class="text-right">
-          <el-button type="primary" @click="generateGroups" :loading="generating">重新生成分组</el-button>
+          <el-button type="primary" @click="regenerateGroups" :loading="generating">重新生成分组</el-button>
           <el-button @click="goBack">返回</el-button>
         </div>
       </div>
@@ -95,43 +95,26 @@
           <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-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 class="text-center p-3 bg-orange-50 rounded-lg">
+          <div class="text-2xl font-bold text-orange-600">{{ roundType }}</div>
+          <div class="text-sm text-gray-600">录取人数</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 { ref, onMounted, 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 { getGameEventGroup, generateGroups, getGroupResultFromDB } from '@/api/system/gameEventGroup';
 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();
@@ -140,73 +123,18 @@ 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 groupResult = ref<Map<string, any>>(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 projectName = ref('');
+const roundType = ref(0);
+// 总运动员数
+const totalAthletes = ref(0);
 
 // 获取道次名称
 const getTrackName = (track: number) => {
@@ -222,8 +150,9 @@ const getAthleteByGroupAndTrack = (groupIndex: number, track: number) => {
 
 // 根据队伍ID获取队伍名称
 const getTeamName = (teamId: string | number | undefined) => {
-  if (!teamId) return '';
-  const team = teams.value.find(t => t.teamId === teamId);
+  if (!groupResult.value.has('teams')) return '';
+  const teams = groupResult.value.get('teams') || [];
+  const team = teams.find((t: any) => t.teamId === teamId);
   return team?.teamName || '';
 };
 
@@ -240,15 +169,8 @@ const getGroupInfo = async () => {
     const res = await getGameEventGroup(groupId);
     groupInfo.value = res.data;
     
-    // 获取运动员、队伍和项目信息
-    await Promise.all([
-      getAthletes(),
-      getTeams(),
-      getProjects()
-    ]);
-    
-    // 生成分组
-    generateGroups();
+    // 优先从数据库读取分组数据
+    await loadGroupResultFromDB();
   } catch (error) {
     console.error('获取分组信息失败:', error);
     proxy?.$modal.msgError('获取分组信息失败');
@@ -257,153 +179,97 @@ const getGroupInfo = async () => {
   }
 };
 
-// 获取运动员列表
-const getAthletes = async () => {
+// 从数据库加载分组结果
+const loadGroupResultFromDB = 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;
+    const groupId = route.query.id;
+    if (!groupId || Array.isArray(groupId)) {
+      proxy?.$modal.msgError('分组ID不能为空');
+      return;
+    }
+    
+    const res = await getGroupResultFromDB(groupId);
+    const data = res.data;
+    
+    if (data.success) {
+      // 更新分组结果
+      groupResult.value.clear();
+      
+      // 设置分组结果
+      if (data.groupResult) {
+        Object.entries(data.groupResult).forEach(([key, athlete]) => {
+          groupResult.value.set(key, athlete);
+        });
+      }
+      
+      if (data.totalAthletes !== undefined) {
+        totalAthletes.value = data.totalAthletes;
+      }
+      
+      // 设置项目信息(从分组信息中获取)
+      if (groupInfo.value.projectId) {
+        // 这里可以根据需要设置项目名称和录取人数
+        projectName.value = '项目名称'; // 需要根据实际情况获取
+        roundType.value = 0; // 需要根据实际情况获取
+      }
+      
+      console.log('从数据库加载分组结果成功');
+    } else {
+      // 数据库中没有数据,自动生成分组
+      console.log('数据库中没有分组数据,自动生成分组');
+      await generateGroupsData();
+    }
   } catch (error) {
-    console.error('获取项目列表失败:', error);
+    console.error('从数据库加载分组结果失败:', error);
+    // 加载失败时,自动生成分组
+    await generateGroupsData();
   }
 };
 
 // 生成分组
-const generateGroups = async () => {
+const generateGroupsData = async () => {
   try {
     generating.value = true;
     
-    // 清空之前的分组结果
-    groupResult.value.clear();
+    const groupId = route.query.id;
+    if (!groupId || Array.isArray(groupId)) {
+      proxy?.$modal.msgError('分组ID不能为空');
+      return;
+    }
+    
+    const res = await generateGroups(groupId);
+    const data = res.data;
     
-    // 筛选符合条件的运动员
-    const eligibleAthletes = athletes.value.filter(athlete => {
+    if (data.success) {
+      // 更新分组结果
+      groupResult.value.clear();
       
-      // 检查是否参与该项目
-      if (!athlete.projectList) {
-        return false;
+      // 设置分组结果
+      if (data.groupResult) {
+        Object.entries(data.groupResult).forEach(([key, athlete]) => {
+          groupResult.value.set(key, athlete);
+        });
       }
       
-      // 处理项目列表
-      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());
-        }
+      // 设置其他数据
+      if (data.project) {
+        projectName.value = data.project.projectName || '';
+        roundType.value = data.project.roundType || 0;
       }
       
-      const targetProjectId = groupInfo.value.projectId?.toString();
-      const hasProject = projectIds.includes(targetProjectId);
-      
-      if (!hasProject) {
-        return false;
+      if (data.totalAthletes !== undefined) {
+        totalAthletes.value = data.totalAthletes;
       }
       
-      // 检查性别是否匹配
-      if (groupInfo.value.memberGender && groupInfo.value.memberGender !== '0') {
-        // 使用字典来匹配性别,而不是硬编码的字符串
-        if (athlete.gender?.toString() !== groupInfo.value.memberGender?.toString()) {
-          return false;
-        }
+      // 设置队伍信息
+      if (data.teams) {
+        groupResult.value.set('teams', data.teams);
       }
       
-      return true;
-    });
-    
-    if (eligibleAthletes.length === 0) {
-      proxy?.$modal.msgWarning('没有找到符合条件的运动员');
-      return;
+      proxy?.$modal.msgSuccess('分组生成成功');
+    } else {
+      proxy?.$modal.msgWarning(data.message || '生成分组失败');
     }
-    
-    // 随机打乱运动员顺序
-    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('生成分组失败');
@@ -412,6 +278,11 @@ const generateGroups = async () => {
   }
 };
 
+// 重新生成分组
+const regenerateGroups = async () => {
+  await generateGroupsData();
+};
+
 // 返回上一页
 const goBack = () => {
   router.go(-1);

+ 41 - 19
src/views/system/gameEventGroup/index.vue

@@ -112,9 +112,9 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="项目类型" prop="projectTypeFilter">
-              <el-select 
-                v-model="formProjectTypeFilter" 
-                placeholder="请选择项目类型" 
+              <el-select
+                v-model="formProjectTypeFilter"
+                placeholder="请选择项目类型"
                 style="width: 100%"
                 @change="handleFormProjectTypeFilterChange"
                 :disabled="!!form.groupId"
@@ -130,9 +130,9 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="项目" prop="projectId">
-              <el-select 
-                v-model="form.projectId" 
-                placeholder="请选择项目" 
+              <el-select
+                v-model="form.projectId"
+                placeholder="请选择项目"
                 style="width: 100%"
                 @change="handleFormProjectChange"
                 :disabled="!formProjectTypeFilter || !!form.groupId"
@@ -147,7 +147,7 @@
             </el-form-item>
           </el-col>
         </el-row>
-        
+
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="组别名称" prop="groupName">
@@ -364,14 +364,33 @@ 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 beginTime = new Date(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 格式
+
+  return endTime.toLocaleString('zh-CN', {
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    hour12: false
+  });
 });
 
+// 禁用日期(限制在项目时间范围内)
+const disabledDate = (time: Date) => {
+  if (!form.value.projectId) return false;
+  const project = projectList.value.find(p => p.projectId === form.value.projectId);
+  if (!project || !project.startTime || !project.endTime) return false;
+
+  const projectStart = new Date(project.startTime);
+  const projectEnd = new Date(project.endTime);
+
+  // 禁用项目时间范围外的日期
+  return time < projectStart || time > projectEnd;
+};
+
 /** 查询赛事分组列表 */
 const getList = async () => {
   loading.value = true;
@@ -405,6 +424,9 @@ const handleFormProjectTypeFilterChange = () => {
   form.value.trackNum = undefined;
   form.value.fieldNum = undefined;
   form.value.duration = undefined;
+
+  // 重新获取项目列表以更新过滤
+  getProjectList();
 };
 
 // 表单中项目变化
@@ -509,21 +531,21 @@ const submitForm = () => {
 
       // 验证时间范围
       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}`);
-        
+        const beginTime = new Date(form.value.beginTime);
+        const endTime = new Date(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}`);
-            
+            const projectStart = new Date(selectedProject.startTime);
+            const projectEnd = new Date(selectedProject.endTime);
+
             if (beginTime < projectStart || endTime > projectEnd) {
               proxy?.$modal.msgError('组别比赛时间必须在项目比赛时间范围内');
               return;
@@ -534,7 +556,7 @@ const submitForm = () => {
 
       buttonLoading.value = true;
       const submitForm = { ...form.value };
-      
+
       if (form.value.groupId) {
         await updateGameEventGroup(submitForm).finally(() => (buttonLoading.value = false));
       } else {

+ 8 - 9
src/views/system/gameEventProject/index.vue

@@ -76,10 +76,10 @@
         </el-table-column>
         <el-table-column label="裁判组" align="center" prop="refereeGroup" v-if="columns[8].visible">
           <template #default="scope">
-            <el-button 
-              v-if="scope.row.refereeGroups" 
-              type="primary" 
-              size="small" 
+            <el-button
+              v-if="scope.row.refereeGroups"
+              type="primary"
+              size="small"
               @click="handleViewRefereeGroup(scope.row.refereeGroups, scope.row.projectName)"
             >
               查看裁判组 ({{ scope.row.refereeGroups.length }}人)
@@ -191,7 +191,7 @@
         </div>
       </template>
     </el-dialog>
-    
+
     <!-- 裁判组查看对话框 -->
     <RefereeGroupDialog ref="refereeGroupDialogRef" />
   </div>
@@ -208,7 +208,6 @@ 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, game_project_classification } = toRefs<any>(proxy?.useDict('game_score_type', 'game_project_type','game_project_classification'));
@@ -250,9 +249,9 @@ const columns = ref<FieldOption[]>([
   { 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 },
-  { key: 11, label: '积分分值', visible: true },
+  { key: 9, label: '录取名次', visible: true },
+  { key: 10, label: '积分分值', visible: true },
+  { key: 11, label: '排序方式', visible: true },
 ]);
 
 const initFormData: GameEventProjectForm = {

+ 4 - 4
src/views/system/gameEventSchedule/index.vue

@@ -265,7 +265,7 @@ const saveSchedule = async () => {
         eventId: defaultEvent.value.eventId,
         projectName: project.projectName,
         projectType: project.projectType,
-        groupType: project.groupType
+        // groupType: project.groupType
       } as any);
     }
 
@@ -297,7 +297,7 @@ const deleteSchedule = async (project: GameEventProjectVO) => {
       eventId: defaultEvent.value.eventId,
       projectName: project.projectName,
       projectType: project.projectType,
-      groupType: project.groupType
+      // groupType: project.groupType
     } as any);
 
     // 更新本地数据
@@ -332,7 +332,7 @@ const exportSchedule = () => {
   }
 
   // 创建表格数据
-  const headers = ['日期', '时间', '项目名称', '项目类型', '项目组别', '比赛场地', '分组数', '每组人数'];
+  const headers = ['日期', '时间', '项目名称', '项目类型', '比赛场地', '分组数', '每组人数'];
   let csvContent = headers.join(',') + '\n';
 
   scheduleData.value.forEach((item) => {
@@ -341,7 +341,7 @@ const exportSchedule = () => {
       `${item.startTime}-${item.endTime}`,
       item.projectName,
       item.projectType === '0' ? '个人' : '团体',
-      item.groupType,
+      // item.groupType,
       item.location,
       item.groupNum,
       item.participateNum

+ 2 - 2
src/views/system/gameScore/gameScoreEdit.vue

@@ -191,7 +191,7 @@ const form = reactive({
   projectId: '',
   athleteCode: '',
   userId: 0,
-  teamName: '', // 个人项目和团体项目都使用
+  teamName: '',
   updateTime: '',
 });
 
@@ -298,7 +298,7 @@ const editScore = (row: any) => {
     userId: row.userId,
     eventId: row.eventId,
     projectId: row.projectId,
-    teamName: row.teamName || '', // 统一使用teamName字段
+    teamName: row.teamName || '',
     updateTime: row.updateTime,
   });
 };

+ 315 - 110
src/views/system/gameScore/index.vue

@@ -4,16 +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="eventId">
-              <el-select v-model="queryParams.eventId" placeholder="请选择赛事" clearable filterable @change="handleEventChange">
-                <el-option
-                  v-for="event in eventList"
-                  :key="event.eventId"
-                  :label="event.eventName"
-                  :value="event.eventId">
-                </el-option>
-              </el-select>
-            </el-form-item> -->
             <el-form-item label="项目" prop="projectId">
               <el-select v-model="queryParams.projectId" placeholder="请选择项目" clearable filterable>
                 <el-option
@@ -43,14 +33,11 @@
             <el-button type="primary" @click="refreshData">刷新</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="primary" @click="printScores">打印成绩(仅有名次)</el-button>
+            <el-button type="primary" @click="printScores">打印成绩(前3名)</el-button>
           </el-col>
           <el-col :span="1.5">
             <el-button type="primary" @click="exportScoresNames">导出成绩(全部)</el-button>
           </el-col>
-          <el-col :span="1.5">
-            <el-button type="primary" @click="exportNumberMapping">导出号码对照表</el-button>
-          </el-col>
           <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
         </el-row>
       </template>
@@ -83,7 +70,7 @@
         </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[4].visible">
           <template #default="scope">
             <el-select v-model="scope.row.status" placeholder="请选择状态">
@@ -109,19 +96,26 @@
 </template>
 
 <script setup name="GameScore" lang="ts">
-import { listGameScore, getGameScore, delGameScore, addGameScore, updateGameScore } from '@/api/system/gameScore';
+import { listGameScore, getGameScore, delGameScore, addGameScore, updateGameScore, getProjectScoreData } from '@/api/system/gameScore';
 import { getDefaultEvent } from '@/api/system/gameEvent'
 import { listGameEventProject } from '@/api/system/gameEventProject';
+import { getGameTeam } from '@/api/system/gameTeam';
+import { getGameAthlete } from '@/api/system/gameAthlete';
 import { GameScoreVO, GameScoreQuery, GameScoreForm } from '@/api/system/gameScore/types';
 import { GameEventVO, GameEventQuery } from '@/api/system/gameEvent/types';
 import { GameEventProjectVO, GameEventProjectQuery } from '@/api/system/gameEventProject/types';
+import { GameTeamVO } from '@/api/system/gameTeam/types';
+import { GameAthleteVO } from '@/api/system/gameAthlete/types';
+import { ElMessage } from 'element-plus';
+import { useGameEventStore } from '@/store/modules/gameEvent';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
 const router = useRouter();
 
 // 默认赛事信息
-const defaultEvent = ref<GameEventVO>({} as GameEventVO)
+// const defaultEvent = ref<GameEventVO>({} as GameEventVO)
+const gameEventStore = useGameEventStore();
 const gameScoreList = ref<GameScoreVO[]>([]);
 
 const buttonLoading = ref(false);
@@ -179,113 +173,324 @@ const data = reactive<PageData<GameScoreForm, GameScoreQuery>>({
 });
 
 const { queryParams, form, rules } = toRefs(data);
-// 添加额外的ref用于处理默认事件ID
-const defaultEventId = computed(() => defaultEvent.value?.eventId);
-
-// 监听默认事件变化
-watchEffect(() => {
-  if (defaultEventId.value) {
-    form.value.eventId = defaultEventId.value;
-    queryParams.value.eventId = defaultEventId.value;
-  }
-});
-
-// 获取默认赛事
-const getDefaultEventInfo = async () => {
-  try {
-    const res = await getDefaultEvent()
-    defaultEvent.value = res.data
-  } catch (error) {
-    ElMessage.error('获取默认赛事失败')
-  }
-}
 
 /** 查询成绩列表 */
 const getList = async () => {
-  if (!queryParams.value.eventId) {
-    proxy?.$modal.msgWarning('未指定默认赛事');
-    loading.value = false;
-    return;
-  }
-  // loading.value = true;
+  // if (!queryParams.value.eventId) {
+  //   proxy?.$modal.msgWarning('未指定默认赛事');
+  //   loading.value = false;
+  //   return;
+  // }
+  loading.value = true;
   const res = await listGameScore(queryParams.value);
   gameScoreList.value = res.rows;
   total.value = res.total;
   loading.value = false;
 }
 
-/** 查询所有赛事列表 */
-// const getEventList = async () => {
-//   const res = await listGameEvent({
-//     // status: '0', // 只查询正常状态的赛事
-//     pageNum: 1,
-//     pageSize: 1000
-//   } as GameEventQuery);
-//   eventList.value = res.rows;
-  
-//   // 如果有赛事数据,默认选择第一个
-//   if (res.rows && res.rows.length > 0) {
-//     queryParams.value.eventId = res.rows[0].eventId;
-//     // 触发赛事变更事件,加载相关项目、队伍、运动员数据
-//     handleEventChange(queryParams.value.eventId);
-//     await getScoreStatus(queryParams.value.projectId);
-//   }
-// }
-
-/** 根据赛事ID查询项目列表 */
-// const getProjectList = async (eventId?: string | number) => {
-//   if (!eventId) return;
-//   const res = await listGameEventProject({
-//     eventId: eventId,
-//     // status: '0', // 只查询正常状态的项目
-//     pageNum: 1,
-//     pageSize: 1000
-//   } as GameEventProjectQuery);
-//   projectList.value = res.rows;
-// }
-
-
-/** 赛事变更事件 */
-// const handleEventChange = async (eventId: string | number | undefined) => {
-//   if (!eventId) {
-//     // 清空相关下拉框数据
-//     projectList.value = [];
-//     return;
-//   }
-  
-//   // 加载相关数据
-//   await Promise.all([
-//     getProjectList(eventId),
-//   ]);
-// }
-
-// const getScoreStatus = async (projectId: string | number) => {
-//   const score = await listGameScore({
-//     projectId: projectId,
-//     pageNum: 1,
-//     pageSize: 100
-//   });
-//   if (!score || !score.rows.length) {
-//     return '0';
-//   }
-//   return '1';
-// };
 /**
  * 刷新数据
  */
 const refreshData = async () => {
   await loadProjects();
 };
+// 打印成绩
 const printScores = async () => {
-  console.log('打印成绩逻辑待实现');
-  // await loadProjects();
+  try {
+    // 显示加载状态
+    const loadingInstance = ElLoading.service({
+      lock: true,
+      text: '正在准备打印数据...',
+      background: 'rgba(0, 0, 0, 0.7)'
+    });
+
+    let projectsToPrint = [];
+
+    // 如果有选择项目,则打印选中的项目
+    if (ids.value.length > 0) {
+      projectsToPrint = projectList.value.filter(project =>
+        ids.value.includes(project.projectId)
+      );
+    } else {
+      // 如果没有选择项目,提示用户是否打印所有项目
+      try {
+        await proxy?.$modal.confirm('未选择项目,是否打印当前页面所有项目?');
+        projectsToPrint = [...projectList.value];
+      } catch {
+        // 用户取消操作
+        loadingInstance.close();
+        return;
+      }
+    }
+
+    if (projectsToPrint.length === 0) {
+      proxy?.$modal.msgWarning('没有可打印的项目');
+      loadingInstance.close();
+      return;
+    }
+
+    // 为每个项目获取成绩数据
+    const projectsWithScores = await Promise.all(
+      projectsToPrint.map(async (project) => {
+        try {
+          const scoreRes = await getProjectScoreData({
+            eventId: project.eventId,
+            projectId: project.projectId,
+            classification: project.classification,
+            pageNum: 1,
+            pageSize: 1000
+          });
+
+          // 获取成绩数据并补充队伍和运动员信息
+          const scores = scoreRes.rows || [];
+
+          // 按积分排序,取前3名
+          const sortedScores = scores
+            .filter(score => score.scorePoint && score.scorePoint > 0) // 只显示有积分的成绩
+            .sort((a: any, b: any) => (b.scorePoint || 0) - (a.scorePoint || 0)) // 按积分降序排列
+            .slice(0, 3); // 只取前3名
+
+          const scoresWithDetails = await Promise.all(
+            sortedScores.map(async (score: any) => {
+              let teamName = '-';
+              let athleteName = '-';
+
+              try {
+                // 获取队伍信息
+                if (score.teamId) {
+                  const teamRes = await getGameTeam(score.teamId);
+                  teamName = teamRes.data.teamName || `队伍${score.teamId}`;
+                }
+
+                // 获取运动员信息
+                if (score.athleteId) {
+                  const athleteRes = await getGameAthlete(score.athleteId);
+                  athleteName = athleteRes.data.name || `运动员${score.athleteId}`;
+                }
+              } catch (error) {
+                console.warn('获取队伍或运动员信息失败:', error);
+              }
+
+              return {
+                ...score,
+                teamName,
+                athleteName
+              };
+            })
+          );
+
+          return {
+            ...project,
+            scores: scoresWithDetails
+          };
+        } catch (error) {
+          console.error(`获取项目 ${project.projectName} 成绩失败:`, error);
+          return {
+            ...project,
+            scores: []
+          };
+        }
+      })
+    );
+
+    // 关闭加载状态
+    loadingInstance.close();
+
+    // 构建打印HTML内容
+    const printHtml = buildPrintHtml(projectsWithScores);
+
+    // 使用 Blob 和 URL.createObjectURL 来避免弹窗拦截问题
+    const blob = new Blob([printHtml], { type: 'text/html' });
+    const url = URL.createObjectURL(blob);
+
+    // 创建隐藏的 iframe 来处理打印
+    const iframe = document.createElement('iframe');
+    iframe.style.position = 'absolute';
+    iframe.style.top = '-9999px';
+    iframe.style.left = '-9999px';
+    document.body.appendChild(iframe);
+
+    iframe.onload = () => {
+      try {
+        // 打印完成后清理
+        setTimeout(() => {
+          document.body.removeChild(iframe);
+          URL.revokeObjectURL(url);
+        }, 1000);
+      } catch (error) {
+        console.error('清理打印资源失败:', error);
+      }
+    };
+
+    // 在 iframe 中加载并打印
+    const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
+    if (iframeDoc) {
+      iframeDoc.open();
+      iframeDoc.write(printHtml);
+      iframeDoc.close();
+
+      // 等待内容加载后打印
+      setTimeout(() => {
+        try {
+          iframe.contentWindow?.focus();
+          iframe.contentWindow?.print();
+        } catch (error) {
+          proxy?.$modal.msgError('打印失败,请检查浏览器设置');
+          console.error('打印失败:', error);
+        }
+      }, 500);
+    } else {
+      proxy?.$modal.msgError('无法创建打印窗口,请检查浏览器设置');
+      document.body.removeChild(iframe);
+      URL.revokeObjectURL(url);
+    }
+  } catch (error) {
+    if (error === 'cancel') {
+      // 用户取消操作
+      return;
+    }
+    console.error('打印失败:', error);
+    proxy?.$modal.msgError('打印失败');
+  }
 };
-const exportNumberMapping = async () => {
-  console.log('导出号码对照表逻辑待实现');
-  // await loadProjects();
+
+/**
+ * 构建打印HTML内容
+ */
+const buildPrintHtml = (projects: any[]) => {
+  const printTime = new Date().toLocaleString('zh-CN');
+
+  let html = `
+    <!DOCTYPE html>
+    <html>
+    <head>
+      <meta charset="UTF-8">
+      <title>赛事成绩打印 - 前3名</title>
+      <style>
+        body { font-family: Arial, sans-serif; margin: 20px; }
+        .print-header { text-align: center; margin-bottom: 30px; border-bottom: 2px solid #333; padding-bottom: 20px; }
+        .print-header h1 { margin: 0 0 10px 0; font-size: 24px; color: #333; }
+        .print-header p { margin: 0; color: #666; font-size: 14px; }
+        .project-section { margin-bottom: 40px; page-break-inside: avoid; }
+        .project-title { font-size: 18px; font-weight: bold; margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-left: 4px solid #409eff; }
+        .title-label { color: #f56c6c; font-weight: bold; }
+        .score-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
+        .score-table th, .score-table td { border: 1px solid #ddd; padding: 8px 12px; text-align: center; }
+        .score-table th { background: #f8f9fa; font-weight: bold; color: #333; }
+        .score-table td { color: #333; }
+        .rank-1 { background-color: #fff7e6; font-weight: bold; }
+        .rank-2 { background-color: #f6ffed; }
+        .rank-3 { background-color: #f0f9ff; }
+        @media print { .project-section { page-break-inside: avoid; } }
+      </style>
+    </head>
+    <body>
+      <div class="print-header">
+        <h1>赛事管理系统 - 成绩打印(前3名)</h1>
+        <p>打印时间: ${printTime}</p>
+      </div>
+  `;
+
+  // 为每个项目添加成绩表格
+  projects.forEach(project => {
+    const scores = project.scores || [];
+
+    html += `
+      <div class="project-section">
+        <div class="project-title">
+          <span class="title-label">${project.projectId}</span>
+          <span class="title-label">${getProjectTypeName(project.projectType)} ${project.projectName}</span>
+        </div>
+
+        <table class="score-table">
+          <thead>
+            <tr>
+              <th class="title-label">类别</th>
+              <th>名次</th>
+              <th class="title-label">队伍</th>
+              <th class="title-label">运动员编号</th>
+              <th class="title-label">姓名</th>
+              <th class="title-label">成绩</th>
+              <th class="title-label">积分</th>
+            </tr>
+          </thead>
+          <tbody>
+    `;
+
+    if (scores.length > 0) {
+      scores.forEach((score: any, index: number) => {
+        const rankClass = index === 0 ? 'rank-1' : index === 1 ? 'rank-2' : 'rank-3';
+        html += `
+          <tr class="${rankClass}">
+            <td>${score.classification === '0' ? '个人项目' : '团体项目'}</td>
+            <td>第${index + 1}名</td>
+            <td>${score.teamName || '-'}</td>
+            <td>${score.athleteId || '-'}</td>
+            <td>${score.athleteName || '-'}</td>
+            <td>${formatScore(score.individualPerformance || score.teamPerformance)}</td>
+            <td>${score.scorePoint || 0}</td>
+          </tr>
+        `;
+      });
+    } else {
+      html += `
+        <tr>
+          <td colspan="7" style="text-align: center; color: #999;">暂无成绩数据</td>
+        </tr>
+      `;
+    }
+
+    html += `
+          </tbody>
+        </table>
+      </div>
+    `;
+  });
+
+  html += `
+    </body>
+    </html>
+  `;
+
+  return html;
+};
+
+/**
+ * 格式化成绩显示
+ */
+const formatScore = (score: number | string) => {
+  if (score === null || score === undefined || score === '') return '-';
+  return score.toString();
+};
+
+/**
+ * 获取项目类型名称
+ */
+const getProjectTypeName = (type: string) => {
+  if (!game_project_type.value || !type) return '未知';
+
+  const typeItem = game_project_type.value.find((item: any) => item.value === type);
+  return typeItem ? typeItem.label : '未知';
 };
-const exportScoresNames = async () => { 
-  console.log('导出成绩逻辑待实现');
+
+const exportScoresNames = () => {
+  // 获取默认赛事ID
+  const event = gameEventStore.defaultEventInfo;
+  const eventId = event?.eventId;
+
+  if (!eventId) {
+    proxy?.$modal.msgWarning('未指定赛事,无法导出');
+    return;
+  }
+
+  // 使用 proxy?.download 方式导出
+  proxy?.download(
+    'system/gameScore/exportScoresSummary',
+    {
+      eventId: eventId
+    },
+    `成绩汇总表_${new Date().toLocaleDateString()}.xlsx`
+  );
 };
 
 /**
@@ -308,7 +513,7 @@ const handleQuery = () => {
 const resetQuery = () => {
   queryFormRef.value?.resetFields();
   // 保留默认赛事ID
-  queryParams.value.eventId = defaultEvent.value?.eventId;
+  // queryParams.value.eventId = defaultEvent.value?.eventId;
   handleQuery();
 };
 
@@ -332,9 +537,9 @@ const navigateToEditPage = (row: GameEventProjectVO) => {
 };
 
 onMounted(() => {
-  getDefaultEventInfo().then(() => {
+  // getDefaultEventInfo().then(() => {
     getList();
     refreshData();
-  });
+  // });
 });
-</script>
+</script>

+ 285 - 0
src/views/system/gameScore/print.vue

@@ -0,0 +1,285 @@
+<template>
+  <div class="print-container">
+    <!-- 打印按钮 -->
+    <div class="print-actions" v-if="!isPrinting">
+      <el-button type="primary" @click="printPage">打印</el-button>
+      <el-button @click="goBack">返回</el-button>
+    </div>
+
+    <!-- 打印内容 -->
+    <div class="print-content" ref="printContent">
+      <div class="print-header">
+        <h1>赛事管理系统 - 成绩打印(前3名)</h1>
+        <p>打印时间: {{ printTime }}</p>
+      </div>
+
+      <!-- 项目成绩表格 -->
+      <div v-for="project in projectScores" :key="project.projectId" class="project-section">
+        <div class="project-title">
+          <span class="title-label">竞赛序员</span> {{ project.projectId }} 
+          <span class="title-label">项目类型-项目名称</span> {{ project.projectTypeName }} {{ project.projectName }}
+        </div>
+        
+        <table class="score-table">
+          <thead>
+            <tr>
+              <th class="title-label">类别</th>
+              <th>名次</th>
+              <th class="title-label">队伍</th>
+              <th class="title-label">运动员编号</th>
+              <th class="title-label">姓名</th>
+              <th class="title-label">成绩</th>
+              <th class="title-label">积分</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr v-for="(score, index) in project.scores.slice(0, 3)" :key="score.scoreId" 
+                :class="index === 0 ? 'rank-1' : index === 1 ? 'rank-2' : 'rank-3'">
+              <td>{{ score.classification === '0' ? '个人项目' : '团体项目' }}</td>
+              <td>第{{ index + 1 }}名</td>
+              <td>{{ score.teamName || '-' }}</td>
+              <td>{{ score.athleteId || '-' }}</td>
+              <td>{{ score.athleteName || '-' }}</td>
+              <td>{{ formatScore(score.individualPerformance || score.teamPerformance) }}</td>
+              <td>{{ score.scorePoint || 0 }}</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="GameScorePrint">
+import { ref, onMounted, computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { getProjectScoreData } from '@/api/system/gameScore'
+import { getGameEventProject } from '@/api/system/gameEventProject'
+import { GameScoreVO } from '@/api/system/gameScore/types'
+import { GameEventProjectVO } from '@/api/system/gameEventProject/types'
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
+
+const route = useRoute()
+const router = useRouter()
+
+// 响应式数据
+const isPrinting = ref(false)
+const printContent = ref<HTMLElement>()
+const projectScores = ref<any[]>([])
+const loading = ref(false)
+
+// 计算属性
+const printTime = computed(() => {
+  return new Date().toLocaleString('zh-CN')
+})
+
+// 格式化成绩显示
+const formatScore = (score: number | string) => {
+  if (score === null || score === undefined || score === '') return '-'
+  return score.toString()
+}
+
+// 获取项目成绩数据
+const loadProjectScores = async () => {
+  try {
+    loading.value = true
+    
+    // 从路由参数获取项目ID
+    const projectId = route.params.projectId as string
+    if (!projectId) {
+      ElMessage.error('项目ID不能为空')
+      return
+    }
+
+    // 获取项目信息
+    const projectRes = await getGameEventProject(projectId)
+    const project = projectRes.data
+
+    // 获取项目成绩数据
+    const scoreRes = await getProjectScoreData({
+      eventId: project.eventId,
+      projectId: project.projectId,
+      classification: project.classification,
+      pageNum: 1,
+      pageSize: 1000 // 获取所有成绩
+    })
+
+    // 处理数据格式
+    const scores = scoreRes.rows || []
+    const sortedScores = scores.sort((a: GameScoreVO, b: GameScoreVO) => {
+      return (a.scoreRank || 0) - (b.scoreRank || 0)
+    })
+
+    // 构建项目成绩数据结构
+    projectScores.value = [{
+      projectId: project.projectId,
+      projectName: project.projectName,
+      projectTypeName: getProjectTypeName(project.projectType),
+      // groupTypeName: getGroupTypeName(project.groupType),
+      classification: project.classification,
+      scores: sortedScores.map(score => ({
+        ...score,
+        teamName: getTeamName(score.teamId),
+        athleteName: getAthleteName(score.athleteId)
+      }))
+    }]
+
+  } catch (error) {
+    console.error('加载项目成绩失败:', error)
+    ElMessage.error('加载项目成绩失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 获取项目类型名称
+const getProjectTypeName = (type: string) => {
+  if (!game_project_type.value || !type) return '未知';
+  
+  const typeItem = game_project_type.value.find((item: any) => item.value === type);
+  return typeItem ? typeItem.label : '未知';
+}
+
+// 获取分组类型名称
+// const getGroupTypeName = (type: string) => {
+//   const groupMap: Record<string, string> = {
+//     '1': '甲组',
+//     '2': '乙组',
+//     '3': '丙组',
+//     '4': '丁组'
+//   }
+//   return groupMap[type] || '未知组'
+// }
+
+// 获取队伍名称(这里需要根据实际API调整)
+const getTeamName = (teamId: string | number) => {
+  // 实际项目中应该调用队伍API获取名称
+  return `队伍${teamId}`
+}
+
+// 获取运动员姓名(这里需要根据实际API调整)
+const getAthleteName = (athleteId: string | number) => {
+  // 实际项目中应该调用运动员API获取姓名
+  return `运动员${athleteId}`
+}
+
+// 打印页面
+const printPage = () => {
+  isPrinting.value = true
+  
+  // 等待DOM更新后执行打印
+  nextTick(() => {
+    window.print()
+    isPrinting.value = false
+  })
+}
+
+// 返回上一页
+const goBack = () => {
+  router.go(-1)
+}
+
+// 页面加载时获取数据
+onMounted(() => {
+  loadProjectScores()
+})
+</script>
+
+<style scoped>
+.print-container {
+  padding: 20px;
+  background: white;
+}
+
+.print-actions {
+  margin-bottom: 20px;
+  text-align: center;
+}
+
+.print-header {
+  text-align: center;
+  margin-bottom: 30px;
+  border-bottom: 2px solid #333;
+  padding-bottom: 20px;
+}
+
+.print-header h1 {
+  margin: 0 0 10px 0;
+  font-size: 24px;
+  color: #333;
+}
+
+.print-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.project-section {
+  margin-bottom: 40px;
+  page-break-inside: avoid;
+}
+
+.project-title {
+  font-size: 18px;
+  font-weight: bold;
+  margin-bottom: 15px;
+  padding: 10px;
+  background: #f5f5f5;
+  border-left: 4px solid #409eff;
+}
+
+.title-label {
+  color: #f56c6c;
+  font-weight: bold;
+}
+
+.score-table {
+  width: 100%;
+  border-collapse: collapse;
+  margin-bottom: 20px;
+}
+
+.score-table th,
+.score-table td {
+  border: 1px solid #ddd;
+  padding: 8px 12px;
+  text-align: center;
+}
+
+.score-table th {
+  background: #f8f9fa;
+  font-weight: bold;
+  color: #333;
+}
+
+.score-table td {
+  color: #333;
+}
+
+.rank-1 { background-color: #fff7e6; font-weight: bold; }
+.rank-2 { background-color: #f6ffed; }
+.rank-3 { background-color: #f0f9ff; }
+
+/* 打印样式 */
+@media print {
+  .print-actions {
+    display: none;
+  }
+  
+  .print-container {
+    padding: 0;
+  }
+  
+  .project-section {
+    page-break-inside: avoid;
+  }
+  
+  .score-table th {
+    background: #f8f9fa !important;
+    -webkit-print-color-adjust: exact;
+  }
+}
+</style> 

+ 12 - 7
src/views/system/gameTeam/index.vue

@@ -47,17 +47,22 @@
 
       <el-table v-loading="loading" border :data="gameTeamList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="主键" align="center" prop="teamId" v-if="columns[0].visible" />
-        <el-table-column label="队伍编号" align="center" prop="teamCode" v-if="columns[1].visible" />
+        <!--        <el-table-column label="主键" align="center" prop="teamId" v-if="columns[0].visible" />-->
+        <!--        <el-table-column label="队伍编号" align="center" prop="teamCode" v-if="columns[1].visible" />-->
         <el-table-column label="赛事名称" align="center" prop="eventName" v-if="columns[2].visible" />
         <el-table-column label="队伍名称" align="center" prop="teamName" v-if="columns[3].visible" />
-        <el-table-column label="团队描述" align="center" prop="teamDescribe" v-if="columns[4].visible" />
+        <el-table-column label="团队描述" align="center" prop="teamDescribe" v-if="columns[4].visible">
+          <template #default="scope">
+            <span v-if="scope.row.teamDescribe">{{ scope.row.teamDescribe }}</span>
+            <span v-else>该团队暂无描述</span>
+          </template>
+        </el-table-column>
         <el-table-column label="领队" align="center" prop="leader" v-if="columns[5].visible" />
         <el-table-column label="人数" align="center" prop="athleteNum" v-if="columns[6].visible" />
         <el-table-column label="号码段" align="center" prop="numberRange" v-if="columns[7].visible" />
         <el-table-column label="状态" align="center" prop="status" v-if="columns[8].visible">
           <template #default="scope">
-            <dict-tag :options="sys_normal_disable" :value="scope.row.status"/>
+            <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
           </template>
         </el-table-column>
         <el-table-column label="备注" align="center" prop="remark" v-if="columns[9].visible" />
@@ -169,9 +174,9 @@ const columns = ref<FieldOption[]>([
   { key: 2, label: '赛事名称', visible: false },
   { key: 3, label: '队伍名称', visible: true },
   { key: 4, label: '团队描述', visible: true },
-  { key: 5, label: '领队', visible: false },
-  { key: 6, label: '人数', visible: false },
-  { key: 7, label: '号码段', visible: false },
+  { key: 5, label: '领队', visible: true },
+  { key: 6, label: '人数', visible: true },
+  { key: 7, label: '号码段', visible: true },
   { key: 8, label: '状态', visible: false },
   { key: 9, label: '备注', visible: false }
 ]);

+ 10 - 1
vite.config.ts

@@ -26,9 +26,18 @@ export default defineConfig(({ mode, command }) => {
         [env.VITE_APP_BASE_API]: {
           // target: 'http://meet2.sportsrobo.club:8080',
           target: 'http://localhost:8080',
+          // target: 'http://192.168.1.126:8080',
           changeOrigin: true,
           ws: true,
-          rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
+          rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), ''),
+          configure: (proxy, options) => {
+            // 添加CORS头
+            proxy.on('proxyReq', (proxyReq, req, res) => {
+              proxyReq.setHeader('Access-Control-Allow-Origin', '*');
+              proxyReq.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+              proxyReq.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, clientid');
+            });
+          }
         }
       }
     },