6 次代码提交 3a570ceed9 ... 52785edc2a

作者 SHA1 备注 提交日期
  wenkai 52785edc2a feat:成绩模块 2 天之前
  zhou 67bf31bdcd Merge branch 'dev_zlt' into dev 2 天之前
  zhou a8b77e500d refactor(system): 优化游戏团队列表显示字段 2 天之前
  zhou 98029c5e06 feat(system): 添加成绩汇总表导出功能 3 天之前
  wenkai a7c42b5d6e perf:优化生成号码布 3 天之前
  zhou c8bf896d16 feat(system): 实现成绩打印功能并优化成绩管理页面 3 天之前

+ 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
   });
 };
 

+ 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' // 确保以二进制流形式接收响应
+  });
+};

+ 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' }
+      }
     ]
   },
   {

+ 4 - 3
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,8 +124,8 @@ service.interceptors.response.use(
     // 获取错误信息
     const msg = errorCode[code] || res.data.msg || errorCode['default'];
     // 二进制数据则直接返回
-    if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
-      return res.data;
+    if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer' || res.config.responseType === 'blob' || res.config.responseType === 'arraybuffer') {
+      return res;
     }
     if (code === 401) {
       // prettier-ignore

+ 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);

+ 26 - 48
src/views/system/gameEventGroup/index.vue

@@ -6,22 +6,12 @@
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
             <el-form-item label="项目类型" prop="projectTypeFilter">
               <el-select v-model="projectTypeFilter" placeholder="请选择项目类型" clearable @change="handleProjectTypeFilterChange">
-                <el-option
-                  v-for="dict in game_project_type"
-                  :key="dict.value"
-                  :label="dict.label"
-                  :value="dict.value"
-                />
+                <el-option v-for="dict in game_project_type" :key="dict.value" :label="dict.label" :value="dict.value" />
               </el-select>
             </el-form-item>
             <el-form-item label="项目" prop="projectId">
               <el-select v-model="queryParams.projectId" placeholder="请选择项目" clearable>
-                <el-option
-                  v-for="project in filteredProjectList"
-                  :key="project.projectId"
-                  :label="project.projectName"
-                  :value="project.projectId"
-                />
+                <el-option v-for="project in filteredProjectList" :key="project.projectId" :label="project.projectName" :value="project.projectId" />
               </el-select>
             </el-form-item>
             <el-form-item label="组别" prop="groupName">
@@ -79,9 +69,7 @@
         <el-table-column label="场地数量" align="center" prop="fieldNum" v-if="columns[7].visible" />
         <el-table-column label="每组用时(分钟)" align="center" prop="duration" v-if="columns[8].visible" />
         <el-table-column label="比赛时间" align="center" v-if="columns[9].visible">
-          <template #default="scope">
-            {{ scope.row.beginTime }} - {{ scope.row.endTime }}
-          </template>
+          <template #default="scope"> {{ scope.row.beginTime }} - {{ scope.row.endTime }} </template>
         </el-table-column>
         <el-table-column label="成员性别" align="center" prop="memberGender" v-if="columns[10].visible">
           <template #default="scope">
@@ -112,27 +100,22 @@
         <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"
               >
-                <el-option
-                  v-for="dict in game_project_type"
-                  :key="dict.value"
-                  :label="dict.label"
-                  :value="dict.value"
-                />
+                <el-option v-for="dict in game_project_type" :key="dict.value" :label="dict.label" :value="dict.value" />
               </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="项目" prop="projectId">
-              <el-select 
-                v-model="form.projectId" 
-                placeholder="请选择项目" 
+              <el-select
+                v-model="form.projectId"
+                placeholder="请选择项目"
                 style="width: 100%"
                 @change="handleFormProjectChange"
                 :disabled="!formProjectTypeFilter || !!form.groupId"
@@ -147,7 +130,7 @@
             </el-form-item>
           </el-col>
         </el-row>
-        
+
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="组别名称" prop="groupName">
@@ -157,12 +140,7 @@
           <el-col :span="12">
             <el-form-item label="成员性别" prop="memberGender">
               <el-select v-model="form.memberGender" placeholder="请选择性别" style="width: 100%">
-                <el-option
-                  v-for="dict in sys_user_sex"
-                  :key="dict.value"
-                  :label="dict.label"
-                  :value="dict.value"
-                />
+                <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -276,7 +254,7 @@ const columns = ref<FieldOption[]>([
   { key: 7, label: '场地数量', visible: true },
   { key: 8, label: '每组用时(分钟)', visible: true },
   { key: 9, label: '比赛时间', visible: true },
-  { key: 10, label: '成员性别', visible: true },
+  { key: 10, label: '成员性别', visible: true }
 ]);
 
 const queryFormRef = ref<ElFormInstance>();
@@ -336,26 +314,26 @@ const { queryParams, form, rules } = toRefs(data);
 // 过滤后的项目列表(用于查询)
 const filteredProjectList = computed(() => {
   if (!projectTypeFilter.value) return projectList.value;
-  return projectList.value.filter(project => project.projectType === projectTypeFilter.value);
+  return projectList.value.filter((project) => project.projectType === projectTypeFilter.value);
 });
 
 // 过滤后的项目列表(用于表单)
 const filteredFormProjectList = computed(() => {
   if (!formProjectTypeFilter.value) return [];
-  return projectList.value.filter(project => project.projectType === formProjectTypeFilter.value);
+  return projectList.value.filter((project) => project.projectType === formProjectTypeFilter.value);
 });
 
 // 根据项目ID获取项目类型
 const getProjectTypeByProjectId = (projectId: string | number) => {
   if (!projectId) return '';
-  const project = projectList.value.find(p => p.projectId === projectId);
+  const project = projectList.value.find((p) => p.projectId === projectId);
   return project?.projectType || '';
 };
 
 // 根据项目ID获取项目名称
 const getProjectNameByProjectId = (projectId: string | number) => {
   if (!projectId) return '';
-  const project = projectList.value.find(p => p.projectId === projectId);
+  const project = projectList.value.find((p) => p.projectId === projectId);
   return project?.projectName || '';
 };
 
@@ -364,11 +342,11 @@ const calculatedEndTime = computed(() => {
   if (!form.value.beginTime || !form.value.duration || !form.value.includeGroupNum) {
     return '';
   }
-  
+
   const beginTime = new Date(`2000-01-01 ${form.value.beginTime}`);
   const totalMinutes = form.value.duration * form.value.includeGroupNum;
   const endTime = new Date(beginTime.getTime() + totalMinutes * 60 * 1000);
-  
+
   return endTime.toTimeString().slice(0, 5); // 返回 HH:mm 格式
 });
 
@@ -410,7 +388,7 @@ const handleFormProjectTypeFilterChange = () => {
 // 表单中项目变化
 const handleFormProjectChange = () => {
   if (form.value.projectId) {
-    const selectedProject = projectList.value.find(p => p.projectId === form.value.projectId);
+    const selectedProject = projectList.value.find((p) => p.projectId === form.value.projectId);
     if (selectedProject) {
       // 可以在这里设置一些默认值或者进行其他处理
       console.log('选中的项目:', selectedProject);
@@ -471,7 +449,7 @@ const handleUpdate = async (row?: GameEventGroupVO) => {
 
   // 根据项目ID设置项目类型过滤器
   if (res.data.projectId) {
-    const project = projectList.value.find(p => p.projectId === res.data.projectId);
+    const project = projectList.value.find((p) => p.projectId === res.data.projectId);
     if (project) {
       formProjectTypeFilter.value = project.projectType;
     }
@@ -511,19 +489,19 @@ 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}`);
-        
+
         if (beginTime >= endTime) {
           proxy?.$modal.msgError('组别结束时间必须晚于开始时间');
           return;
         }
-        
+
         // 验证组别时间是否在项目时间范围内
         if (form.value.projectId) {
-          const selectedProject = projectList.value.find(p => p.projectId === form.value.projectId);
+          const selectedProject = projectList.value.find((p) => p.projectId === form.value.projectId);
           if (selectedProject && selectedProject.startTime && selectedProject.endTime) {
             const projectStart = new Date(`2000-01-01 ${selectedProject.startTime}`);
             const projectEnd = new Date(`2000-01-01 ${selectedProject.endTime}`);
-            
+
             if (beginTime < projectStart || endTime > projectEnd) {
               proxy?.$modal.msgError('组别比赛时间必须在项目比赛时间范围内');
               return;
@@ -534,7 +512,7 @@ const submitForm = () => {
 
       buttonLoading.value = true;
       const submitForm = { ...form.value };
-      
+
       if (form.value.groupId) {
         await updateGameEventGroup(submitForm).finally(() => (buttonLoading.value = false));
       } else {

+ 47 - 37
src/views/system/gameEventProject/index.vue

@@ -53,7 +53,7 @@
             <dict-tag :options="game_project_type" :value="scope.row.projectType || ''" />
           </template>
         </el-table-column>
-        <el-table-column label="项目归类" align="center" prop="classification" v-if="columns[3].visible" >
+        <el-table-column label="项目归类" align="center" prop="classification" v-if="columns[3].visible">
           <template #default="scope">
             <dict-tag :options="game_project_classification" :value="scope.row.classification || '未知'" />
           </template>
@@ -76,15 +76,15 @@
         </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 }}人)
             </el-button>
-            <span v-else style="color: #999;">暂无裁判</span>
+            <span v-else style="color: #999">暂无裁判</span>
           </template>
         </el-table-column>
         <!-- <el-table-column label="参赛组数" align="center" prop="groupNum" />
@@ -191,7 +191,7 @@
         </div>
       </template>
     </el-dialog>
-    
+
     <!-- 裁判组查看对话框 -->
     <RefereeGroupDialog ref="refereeGroupDialogRef" />
   </div>
@@ -208,10 +208,11 @@ 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'));
+const { game_score_type, game_project_type, game_project_classification } = toRefs<any>(
+  proxy?.useDict('game_score_type', 'game_project_type', 'game_project_classification')
+);
 
 const gameEventGroupList = ref<any[]>([]); // 赛事分组列表
 
@@ -252,7 +253,7 @@ const columns = ref<FieldOption[]>([
   { key: 8, label: '裁判组', visible: true },
   { key: 9, label: '轮次', visible: true },
   { key: 10, label: '排序方式', visible: true },
-  { key: 11, label: '积分分值', visible: true },
+  { key: 11, label: '积分分值', visible: true }
 ]);
 
 const initFormData: GameEventProjectForm = {
@@ -284,13 +285,13 @@ const data = reactive<PageData<GameEventProjectForm, GameEventProjectQuery>>({
     pageSize: 10,
     classification: undefined,
     orderByColumn: undefined,
-    isAsc: undefined,
+    isAsc: undefined
   },
   rules: {
     projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
     projectType: [{ required: true, message: '项目类型不能为空', trigger: 'change' }],
     classification: [{ required: true, message: '归类不能为空', trigger: 'change' }],
-    orderType: [{ required: true, message: '排序方式不能为空', trigger: 'change' }],
+    orderType: [{ required: true, message: '排序方式不能为空', trigger: 'change' }]
   }
 });
 
@@ -311,7 +312,7 @@ const getGameEventGroupList = async () => {
     pageNum: 1,
     pageSize: 10,
     orderByColumn: undefined,
-    isAsc: undefined,
+    isAsc: undefined
   });
   gameEventGroupList.value = res.data;
 };
@@ -342,7 +343,7 @@ const resetQuery = () => {
 
 /** 多选框选中数据 */
 const handleSelectionChange = (selection: any[]) => {
-  ids.value = selection.map(item => item.projectId);
+  ids.value = selection.map((item) => item.projectId);
   single.value = selection.length !== 1;
   multiple.value = !selection.length;
 };
@@ -358,7 +359,7 @@ const handleAdd = () => {
 const handleUpdate = (row?: GameEventProjectVO) => {
   reset();
   const projectId = row?.projectId || ids.value.at(0);
-  getGameEventProject(projectId).then(response => {
+  getGameEventProject(projectId).then((response) => {
     Object.assign(form.value, response.data);
     dialog.visible = true;
     dialog.title = '修改赛事项目';
@@ -367,25 +368,29 @@ const handleUpdate = (row?: GameEventProjectVO) => {
 
 /** 提交按钮 */
 const submitForm = () => {
-  gameEventProjectFormRef.value?.validate(valid => {
+  gameEventProjectFormRef.value?.validate((valid) => {
     if (valid) {
       buttonLoading.value = true;
       if (form.value.projectId !== undefined) {
-        updateGameEventProject(form.value).then(response => {
-          proxy?.$modal.msgSuccess('修改成功');
-          dialog.visible = false;
-          getList();
-        }).finally(() => {
-          buttonLoading.value = false;
-        });
+        updateGameEventProject(form.value)
+          .then((response) => {
+            proxy?.$modal.msgSuccess('修改成功');
+            dialog.visible = false;
+            getList();
+          })
+          .finally(() => {
+            buttonLoading.value = false;
+          });
       } else {
-        addGameEventProject(form.value).then(response => {
-          proxy?.$modal.msgSuccess('新增成功');
-          dialog.visible = false;
-          getList();
-        }).finally(() => {
-          buttonLoading.value = false;
-        });
+        addGameEventProject(form.value)
+          .then((response) => {
+            proxy?.$modal.msgSuccess('新增成功');
+            dialog.visible = false;
+            getList();
+          })
+          .finally(() => {
+            buttonLoading.value = false;
+          });
       }
     }
   });
@@ -394,12 +399,18 @@ const submitForm = () => {
 /** 删除按钮操作 */
 const handleDelete = (row?: GameEventProjectVO) => {
   const projectIds = row?.projectId || ids.value;
-  proxy?.$modal.confirm('是否确认删除赛事项目编号为"' + projectIds + '"的数据项?').then(function () {
-    return delGameEventProject(projectIds);
-  }).then(() => {
-    getList();
-    proxy?.$modal.msgSuccess('删除成功');
-  }).catch(() => {proxy?.$modal.msgSuccess('删除失败');});
+  proxy?.$modal
+    .confirm('是否确认删除赛事项目编号为"' + projectIds + '"的数据项?')
+    .then(function () {
+      return delGameEventProject(projectIds);
+    })
+    .then(() => {
+      getList();
+      proxy?.$modal.msgSuccess('删除成功');
+    })
+    .catch(() => {
+      proxy?.$modal.msgSuccess('删除失败');
+    });
 };
 
 /** 导出按钮操作 */
@@ -422,5 +433,4 @@ onMounted(() => {
   getList();
   // getGameEventGroupList();
 });
-
 </script>

+ 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,
   });
 };

+ 417 - 128
src/views/system/gameScore/index.vue

@@ -4,23 +4,9 @@
       <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
-                  v-for="project in projectList"
-                  :key="project.projectId"
-                  :label="project.projectName"
-                  :value="project.projectId">
+                <el-option v-for="project in projectList" :key="project.projectId" :label="project.projectName" :value="project.projectId">
                 </el-option>
               </el-select>
             </el-form-item>
@@ -43,14 +29,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>
@@ -69,12 +52,12 @@
       <el-table v-loading="loading" border :data="projectList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="ID" align="center" prop="projectId" v-if="columns[0].visible" />
-        <el-table-column label="项目类型" align="center" prop="projectType" v-if="columns[2].visible" >
+        <el-table-column label="项目类型" align="center" prop="projectType" v-if="columns[2].visible">
           <template #default="scope">
             <dict-tag :options="game_project_type" :value="scope.row.projectType" />
           </template>
         </el-table-column>
-        <el-table-column label="归类" align="center" prop="classification" v-if="columns[3].visible" >
+        <el-table-column label="归类" align="center" prop="classification" v-if="columns[3].visible">
           <template #default="scope">
             <el-tag :type="scope.row.classification === '0' ? 'success' : 'warning'">
               {{ scope.row.classification === '0' ? '个人项目' : '团体项目' }}
@@ -83,7 +66,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 +92,34 @@
 </template>
 
 <script setup name="GameScore" lang="ts">
-import { listGameScore, getGameScore, delGameScore, addGameScore, updateGameScore } from '@/api/system/gameScore';
-import { getDefaultEvent } from '@/api/system/gameEvent'
+import {
+  listGameScore,
+  getGameScore,
+  delGameScore,
+  addGameScore,
+  updateGameScore,
+  getProjectScoreData,
+  exportScoresSummary
+} 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 { ElLoading, 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);
@@ -140,7 +138,7 @@ const columns = ref<FieldOption[]>([
   { key: 3, label: '归类', visible: true },
   { key: 4, label: '状态', visible: true },
   { key: 5, label: '比赛时间', visible: true },
-  { key: 6, label: '更新时间', visible: true },
+  { key: 6, label: '更新时间', visible: true }
 ]);
 
 // 下拉框数据
@@ -165,9 +163,9 @@ const initFormData: GameScoreForm = {
   statusFlag: undefined,
   status: undefined,
   remark: undefined
-}
+};
 const data = reactive<PageData<GameScoreForm, GameScoreQuery>>({
-  form: {...initFormData},
+  form: { ...initFormData },
   queryParams: {
     pageNum: 1,
     pageSize: 10,
@@ -179,113 +177,404 @@ 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) => {
+  const typeMap: Record<string, string> = {
+    '1': '田径',
+    '2': '游泳',
+    '3': '球类',
+    '4': '其他'
+  };
+  return typeMap[type] || '未知';
 };
-const exportScoresNames = async () => { 
-  console.log('导出成绩逻辑待实现');
+
+const exportScoresNames = async () => {
+  try {
+    // 显示加载状态
+    const loadingInstance = ElLoading.service({
+      lock: true,
+      text: '正在导出成绩汇总表...',
+      background: 'rgba(0, 0, 0, 0.7)'
+    });
+
+    // 获取默认赛事ID
+    const event = gameEventStore.defaultEventInfo;
+    const eventId = event?.eventId;
+
+    if (!eventId) {
+      proxy?.$modal.msgWarning('未指定赛事,无法导出');
+      loadingInstance.close();
+      return;
+    }
+
+    // 调用导出接口
+    const response = await exportScoresSummary(eventId);
+
+    // 校验响应是否为有效的二进制数据
+    if (!response || !response.data || !(response.data instanceof Blob)) {
+      proxy?.$modal.msgError('导出失败:服务器返回数据异常');
+      loadingInstance.close();
+      return;
+    }
+
+    // 创建Blob时,明确指定类型
+    const blob = new Blob([response.data], {
+      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+    });
+
+    // 验证Blob大小是否合理(防止空文件)
+    if (blob.size === 0) {
+      proxy?.$modal.msgError('导出失败:生成的文件为空');
+      loadingInstance.close();
+      return;
+    }
+
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = `成绩汇总表_${new Date().toLocaleDateString()}.xlsx`;
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+
+    loadingInstance.close();
+    proxy?.$modal.msgSuccess('导出成功');
+  } catch (error) {
+    console.error('导出失败:', error);
+    let errorMessage = '未知错误';
+
+    if (error instanceof Error) {
+      errorMessage = error.message;
+    } else if (typeof error === 'string') {
+      errorMessage = error;
+    } else if (error && typeof error === 'object' && 'message' in error) {
+      errorMessage = String(error.message);
+    }
+
+    // 尝试获取更详细的错误信息
+    if (error && typeof error === 'object' && 'response' in error) {
+      const response = (error as any).response;
+      if (response && response.data) {
+        try {
+          if (response.data instanceof Blob) {
+            // 如果是blob,尝试读取错误信息
+            const reader = new FileReader();
+            reader.onload = function (e) {
+              try {
+                const text = e.target?.result as string;
+                const errorObj = JSON.parse(text);
+                if (errorObj.msg) {
+                  errorMessage = errorObj.msg;
+                }
+              } catch (parseError) {
+                console.warn('无法解析错误响应:', parseError);
+              }
+            };
+            reader.readAsText(response.data);
+          } else if (typeof response.data === 'string') {
+            errorMessage = response.data;
+          } else if (response.data.msg) {
+            errorMessage = response.data.msg;
+          }
+        } catch (parseError) {
+          console.warn('解析错误响应失败:', parseError);
+        }
+      }
+    }
+
+    proxy?.$modal.msgError('导出失败:' + errorMessage);
+  }
 };
 
 /**
@@ -293,7 +582,7 @@ const exportScoresNames = async () => {
  */
 const loadProjects = async () => {
   loading.value = true;
-  console.log('加载项目列表: ',queryParams.value);
+  console.log('加载项目列表: ', queryParams.value);
   const res = await listGameEventProject(queryParams.value);
   projectList.value = res.rows;
   total.value = res.total;
@@ -308,16 +597,16 @@ const handleQuery = () => {
 const resetQuery = () => {
   queryFormRef.value?.resetFields();
   // 保留默认赛事ID
-  queryParams.value.eventId = defaultEvent.value?.eventId;
+  // queryParams.value.eventId = defaultEvent.value?.eventId;
   handleQuery();
 };
 
 /** 多选框选中数据 */
 const handleSelectionChange = (selection: GameScoreVO[]) => {
-  ids.value = selection.map(item => item.projectId); // 使用eventId作为标识
+  ids.value = selection.map((item) => item.projectId); // 使用eventId作为标识
   single.value = selection.length != 1;
   multiple.value = !selection.length;
-}
+};
 
 const navigateToEditPage = (row: GameEventProjectVO) => {
   const projectId = row.projectId;
@@ -332,9 +621,9 @@ const navigateToEditPage = (row: GameEventProjectVO) => {
 };
 
 onMounted(() => {
-  getDefaultEventInfo().then(() => {
-    getList();
-    refreshData();
-  });
+  // 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 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) => {
+  const typeMap: Record<string, string> = {
+    '1': '田径',
+    '2': '游泳',
+    '3': '球类',
+    '4': '其他'
+  }
+  return typeMap[type] || '未知'
+}
+
+// 获取分组类型名称
+// 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> 

+ 3 - 3
src/views/system/gameTeam/index.vue

@@ -169,9 +169,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');
+            });
+          }
         }
       }
     },