Browse Source

feat:导出号码布zip、生成二维码、增加权限校验

wenkai 6 days ago
parent
commit
16e45aec01
3 changed files with 679 additions and 18 deletions
  1. 10 0
      prompt.txt
  2. 88 1
      src/api/system/gameEvent/index.ts
  3. 581 17
      src/views/system/gameEvent/index.vue

+ 10 - 0
prompt.txt

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

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

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

+ 581 - 17
src/views/system/gameEvent/index.vue

@@ -58,25 +58,47 @@
           </el-col>
           <!-- 新增的操作按钮,基于默认赛事 -->
           <el-col :span="1.5">
-            <el-button type="warning" plain icon="Download" @click="handleDownloadTemplateDefault" v-hasPermi="['system:gameEvent:download']">下载模板</el-button>
+            <el-button type="warning" plain icon="Download" @click="handleDownloadTemplateDefault" v-hasPermi="['system:gameEvent:download']"
+              >下载模板
+            </el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="info" plain icon="FolderOpened" @click="handleImportRegistrationDefault" v-hasPermi="['system:gameEvent:import']">导入报名</el-button>
+            <el-button type="info" plain icon="FolderOpened" @click="handleImportRegistrationDefault" v-hasPermi="['system:gameEvent:import']"
+              >导入报名
+            </el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="success" plain icon="User" @click="handleAddParticipantDefault" v-hasPermi="['system:gameEvent:addParticipant']">参赛者</el-button>
+            <el-button type="success" plain icon="User" @click="handleAddParticipantDefault" v-hasPermi="['system:gameEvent:addParticipant']"
+              >参赛者
+            </el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="primary" plain icon="Avatar" @click="handleAddRefereeDefault" v-hasPermi="['system:gameEvent:addReferee']">裁判</el-button>
+            <el-button type="primary" plain icon="Avatar" @click="handleAddRefereeDefault" v-hasPermi="['system:gameEvent:addReferee']"
+              >裁判
+            </el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="info" plain icon="View" @click="handlePreviewDefault" v-hasPermi="['system:gameEvent:view']">预览</el-button>
+            <el-button type="info" plain icon="View" @click="handlePreviewDefault" v-hasPermi="['system:gameEvent:view']">预览 </el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="warning" plain icon="DataAnalysis" @click="handleGameDataDefault" v-hasPermi="['system:gameEvent:gameData']">排行榜</el-button>
+            <el-button type="warning" plain icon="DataAnalysis" @click="handleGameDataDefault" v-hasPermi="['system:gameEvent:gameData']"
+              >排行榜
+            </el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="primary" plain icon="EditPen" @click="handleWriteArticleDefault" v-hasPermi="['system:gameEvent:writeArticle']">编写文章</el-button>
+            <el-button type="primary" plain icon="EditPen" @click="handleWriteArticleDefault" v-hasPermi="['system:gameEvent:writeArticle']"
+              >编写文章
+            </el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Download" @click="handleExportNumberTableDefault" v-hasPermi="['system:gameEvent:numberExport']"
+              >导出号码对照表
+            </el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Postcard" @click="handleGenerateBib" v-hasPermi="['system:gameEvent:numberBib']"
+              >生成参赛证
+            </el-button>
           </el-col>
           <right-toolbar v-model:showSearch="showSearch" :columns="columns" @queryTable="getList"></right-toolbar>
         </el-row>
@@ -85,14 +107,12 @@
       <el-table v-loading="loading" border :data="gameEventList" @selection-change="handleSelectionChange">
         <!-- 第一列:多选列 -->
         <el-table-column type="selection" width="55" align="center" />
-        
+
         <!-- 第二列:操作列 -->
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
           <template #default="scope">
             <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:gameEvent:edit']">
-                修改
-              </el-button>
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:gameEvent:edit']"> 修改 </el-button>
             </el-tooltip>
           </template>
         </el-table-column>
@@ -322,16 +342,153 @@
         </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>
+                </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>
+                </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>
 
 <script setup name="GameEvent" lang="ts">
-import { listGameEvent, changeEventDefault, delGameEvent, addGameEvent, updateGameEvent } from '@/api/system/gameEvent';
+import {
+  listGameEvent,
+  changeEventDefault,
+  delGameEvent,
+  addGameEvent,
+  updateGameEvent,
+  generateNumberTable,
+  generateBib,
+  type GenerateBibBo
+} 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 } from 'vue';
+import { ref, nextTick } from 'vue';
 import RefereeForm from '@/views/system/gameEvent/RefereeForm.vue';
 import RankingBoard from './RankingBoard.vue';
 import Editor from '@/components/Editor/index.vue';
@@ -666,9 +823,6 @@ const handleGameData = (row: GameEventVO) => {
   });
 };
 
-const rankingBoardVisible = ref(false);
-const currentEventId = ref<string | undefined>();
-
 // 文章编写相关数据
 const articleDialog = reactive({
   visible: false,
@@ -764,7 +918,6 @@ const loadTabData = async (tabName: string) => {
         }
       }
     } catch (error) {
-      console.log('查询文章数据失败:', error);
       const dataKey = getDataKeyByTabName(tabName);
       if (dataKey && articleData[dataKey]) {
         articleData[dataKey].id = undefined;
@@ -930,10 +1083,347 @@ const handleWriteArticleDefault = async () => {
   handleWriteArticle(defaultEvent);
 };
 
+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
+});
+
+// 生成参赛证按钮处理
+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;
+  }
+};
+
 onMounted(() => {
   // 获取默认赛事信息
   gameEventStore.fetchDefaultEvent();
   getList();
+  generateNumberTable();
+
+  // 强制确保bibForm的值正确初始化
+
+  // 使用nextTick确保在下一个tick执行
+  nextTick(() => {
+    if (bibForm.qRCodeX === null || bibForm.qRCodeX === undefined) {
+      bibForm.qRCodeX = 100; // 像素单位
+    }
+    if (bibForm.qRCodeY === null || bibForm.qRCodeY === undefined) {
+      bibForm.qRCodeY = 200; // 像素单位
+    }
+  });
 });
 
 // 监听路由变化,当从编辑页返回时检查是否需要刷新列表
@@ -977,6 +1467,80 @@ onActivated(() => {
   justify-content: center;
 }
 
+/* 生成参赛证样式 */
+.bib-generator {
+  padding: 20px;
+}
+
+.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);