3 Commits a8b77e500d ... 67bf31bdcd

Author SHA1 Message Date
  zhou 67bf31bdcd Merge branch 'dev_zlt' into dev 2 days ago
  wenkai a7c42b5d6e perf:优化生成号码布 3 days ago
  wenkai 3a570ceed9 feat:修改字体 6 days ago

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

+ 2 - 1
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分钟
 });
 
 // 请求拦截器

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

@@ -0,0 +1,646 @@
+<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="背景图片">
+              <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图片">
+              <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);
+    }
+  }
+};
+
+// 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);
+};
+
+// 坐标转换函数:根据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 () => {
+  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 = 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;
+}
+
+.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: 20%;
+  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 - 523
src/views/system/gameEvent/index.vue

@@ -178,6 +178,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,136 +344,6 @@
         </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="microsoft-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 width="100" height="30" viewBox="0 0 100 30">
-                    <rect x="0" y="0" width="2" height="30" fill="black" />
-                    <rect x="4" y="0" width="1" height="30" fill="black" />
-                    <rect x="7" y="0" width="3" height="30" fill="black" />
-                    <rect x="12" y="0" width="1" height="30" fill="black" />
-                    <rect x="15" y="0" width="2" height="30" fill="black" />
-                    <rect x="19" y="0" width="1" height="30" fill="black" />
-                    <rect x="22" y="0" width="3" height="30" fill="black" />
-                    <rect x="27" y="0" width="2" height="30" fill="black" />
-                    <rect x="31" y="0" width="1" height="30" fill="black" />
-                    <rect x="34" y="0" width="2" height="30" fill="black" />
-                    <rect x="37" y="0" width="2" height="30" fill="black" />
-                    <rect x="42" y="0" width="2" height="30" fill="black" />
-                    <rect x="46" y="0" width="2" height="30" fill="black" />
-                    <rect x="49" y="0" width="2" height="30" fill="black" />
-                    <rect x="50" y="0" width="2" height="30" fill="black" />
-                    <rect x="52" y="0" width="2" height="30" fill="black" />
-                    <rect x="54" y="0" width="2" height="30" fill="black" />
-                    <rect x="58" y="0" width="2" height="30" fill="black" />
-                    <rect x="60" y="0" width="2" height="30" fill="black" />
-                  </svg>
-                </div>
-
-                <!-- 赛事名称预览 -->
-                <div
-                  class="event-name-preview"
-                  :style="{
-                    fontSize: Math.round(bibForm.fontSize * 0.8) + 'px',
-                    color: bibForm.fontColorHex,
-                    fontFamily: bibForm.fontName
-                  }"
-                >
-                  赛事名称
-                </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>
 
@@ -482,16 +354,15 @@ import {
   delGameEvent,
   addGameEvent,
   updateGameEvent,
-  generateNumberTable,
-  generateBib,
-  type GenerateBibBo
+  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';
@@ -510,6 +381,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);
@@ -1089,321 +961,15 @@ 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();
+  if (bibViewerDialogRef.value) {
+    bibViewerDialogRef.value.bibDialog.visible = true;
   }
 };
 
-// 重置表单
-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;
-  }
-};
 
 onMounted(() => {
   // 获取默认赛事信息
@@ -1452,86 +1018,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);

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