Browse Source

feat(gameEvent): 实现参赛证生成任务管理功能

- 新增参赛证生成任务API接口,支持创建、查询、暂停、删除和下载任务
- 添加任务列表页面,展示任务状态、创建时间等信息
- 实现任务状态轮询更新机制,实时监控任务进度
- 在参赛证预览组件中增加文件上传删除功能和百分比坐标系统
- 修改参赛证生成逻辑,从直接下载改为异步任务处理模式
- 调整预览画布样式和元素定位方式,提高布局准确性
- 更新路由配置,添加任务列表页面访问路径
zhou 2 days ago
parent
commit
0d6c47c55b

+ 72 - 0
src/api/system/gameEvent/task.ts

@@ -0,0 +1,72 @@
+import request from '@/utils/request';
+
+// 创建参赛证生成任务
+export const createBibTask = (bgImage: File, logo: File | null, taskName: string, bibParam: any) => {
+  const formData = new FormData();
+  formData.append('bgImage', bgImage);
+  if (logo) {
+    formData.append('logo', logo);
+  }
+  formData.append('taskName', taskName);
+  formData.append('bibParam', new Blob([JSON.stringify(bibParam)], { type: 'application/json' }));
+  
+  return request({
+    url: '/system/number/createBibTask',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
+
+// 查询任务列表
+export const getTaskList = (params: any) => {
+  return request({
+    url: '/system/number/taskList',
+    method: 'get',
+    params
+  });
+};
+
+// 暂停任务
+export const pauseTask = (taskId: number) => {
+  return request({
+    url: `/system/number/pauseTask/${taskId}`,
+    method: 'post'
+  });
+};
+
+// 删除任务
+export const deleteTask = (taskId: number) => {
+  return request({
+    url: `/system/number/task/${taskId}`,
+    method: 'delete'
+  });
+};
+
+// 下载任务结果
+export const downloadTask = (taskId: number) => {
+  return request({
+    url: `/system/number/downloadTask/${taskId}`,
+    method: 'get',
+    responseType: 'blob' // 重要:设置响应类型为blob
+  }).then((response: any) => {
+    // 创建blob对象
+    const blob = new Blob([response], { type: 'application/zip' });
+    // 创建下载链接
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = `参赛证_任务${taskId}.zip`;
+    link.style.display = 'none';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    // 释放URL对象
+    window.URL.revokeObjectURL(url);
+  }).catch((error) => {
+    console.error('下载失败:', error);
+    throw error;
+  });
+};

+ 6 - 0
src/router/index.ts

@@ -135,6 +135,12 @@ export const constantRoutes: RouteRecordRaw[] = [
         name: 'ArticleEditor',
         component: () => import('@/views/system/gameEvent/components/ArticleEditor.vue'),
         meta: { title: '富文本编辑' }
+      },
+      {
+        path: 'taskList',
+        component: () => import('@/views/system/gameEvent/TaskList.vue'),
+        name: 'BibTaskList',
+        meta: { title: '参赛证任务列表', icon: 'list' }
       }
     ]
   },

+ 274 - 0
src/views/system/gameEvent/TaskList.vue

@@ -0,0 +1,274 @@
+<template>
+  <div class="app-container">
+    <el-card>
+      <template #header>
+        <div class="card-header">
+          <span>参赛证生成任务</span>
+        </div>
+      </template>
+      
+      <el-table v-loading="loading" :data="taskList" border>
+        <el-table-column label="序号" type="index" width="60" />
+        <el-table-column label="任务名称" prop="taskName" />
+        <el-table-column label="赛事名称" prop="eventName" width="150">
+          <template #default="scope">
+            {{ scope.row.eventName || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" prop="status" width="100">
+          <template #default="scope">
+            <el-tag :type="getStatusType(scope.row.status)">
+              {{ getStatusText(scope.row.status) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" prop="createTime" width="180">
+          <template #default="scope">
+            {{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
+          </template>
+        </el-table-column>
+        <el-table-column label="完成时间" prop="finishTime" width="180">
+          <template #default="scope">
+            {{ scope.row.finishTime ? parseTime(scope.row.finishTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="200">
+          <template #default="scope">
+            <el-button 
+              v-if="scope.row.status === '0'" 
+              type="warning" 
+              size="small" 
+              @click="handleStopTask(scope.row.taskId)"
+            >
+              停止
+            </el-button>
+            <el-button 
+              v-if="scope.row.status === '2'" 
+              type="success" 
+              size="small" 
+              :loading="downloadingTasks.has(scope.row.taskId)"
+              @click="handleDownload(scope.row.taskId)"
+            >
+              {{ downloadingTasks.has(scope.row.taskId) ? '下载中...' : '下载' }}
+            </el-button>
+            <el-button 
+              type="danger" 
+              size="small" 
+              @click="handleDelete(scope.row.taskId)"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      
+      <pagination 
+        v-show="total > 0" 
+        :total="total" 
+        v-model:page="queryParams.pageNum" 
+        v-model:limit="queryParams.pageSize" 
+        @pagination="getList" 
+      />
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue';
+import { useRouter } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { getTaskList, pauseTask, deleteTask, downloadTask } from '@/api/system/gameEvent/task';
+
+const router = useRouter();
+
+const loading = ref(false);
+const taskList = ref([]);
+const total = ref(0);
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 10
+});
+
+// 下载状态管理
+const downloadingTasks = ref(new Set<number>());
+
+// 轮询定时器
+let pollingTimer: NodeJS.Timeout | null = null;
+
+// 存储上次轮询时的任务状态,用于比较状态变化
+const lastTaskStates = ref(new Map<number, string>());
+
+// 获取任务列表
+const getList = async () => {
+  loading.value = true;
+  try {
+    const response = await getTaskList(queryParams.value);
+    taskList.value = response.rows;
+    total.value = response.total;
+    
+    // 记录当前任务状态,用于后续轮询比较
+    const currentTaskStates = new Map<number, string>();
+    response.rows.forEach(task => {
+      currentTaskStates.set(task.taskId, task.status);
+    });
+    lastTaskStates.value = currentTaskStates;
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 新建任务
+const handleCreateTask = () => {
+  router.push('/system/gameEvent');
+};
+
+// 停止任务
+const handleStopTask = async (taskId: number) => {
+  try {
+    await ElMessageBox.confirm('确定要停止这个任务吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    });
+    
+    await pauseTask(taskId);
+    ElMessage.success('任务已停止');
+    getList();
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error('停止失败');
+    }
+  }
+};
+
+// 下载任务结果
+const handleDownload = async (taskId: number) => {
+  downloadingTasks.value.add(taskId);
+  try {
+    await downloadTask(taskId);
+    ElMessage.success('下载完成');
+  } catch (error) {
+    ElMessage.error('下载失败');
+  } finally {
+    downloadingTasks.value.delete(taskId);
+  }
+};
+
+// 删除任务
+const handleDelete = async (taskId: number) => {
+  try {
+    await ElMessageBox.confirm('确定要删除这个任务吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    });
+    
+    await deleteTask(taskId);
+    ElMessage.success('删除成功');
+    getList();
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error('删除失败');
+    }
+  }
+};
+
+// 状态相关方法
+const getStatusType = (status: string) => {
+  const statusMap = {
+    '0': 'warning',  // 运行中
+    '1': 'info',     // 暂停
+    '2': 'success',  // 完成
+    '3': 'danger'    // 失败
+  };
+  return statusMap[status] || 'info';
+};
+
+const getStatusText = (status: string) => {
+  const statusMap = {
+    '0': '运行中',
+    '1': '暂停',
+    '2': '完成',
+    '3': '失败'
+  };
+  return statusMap[status] || '未知';
+};
+
+// 开始轮询
+const startPolling = () => {
+  if (pollingTimer) {
+    clearInterval(pollingTimer);
+  }
+  pollingTimer = setInterval(async () => {
+    // 只轮询有运行中任务的情况
+    const hasRunningTasks = taskList.value.some(task => task.status === '0');
+    if (hasRunningTasks) {
+      await checkTaskStatusChanges();
+    }
+  }, 15000); // 每15秒检查一次
+};
+
+// 检查任务状态变化
+const checkTaskStatusChanges = async () => {
+  try {
+    const response = await getTaskList(queryParams.value);
+    const currentTasks = response.rows;
+    
+    // 检查是否有任务状态发生变化
+    let hasStatusChanged = false;
+    const currentTaskStates = new Map<number, string>();
+    
+    currentTasks.forEach(task => {
+      const taskId = task.taskId;
+      const currentStatus = task.status;
+      const lastStatus = lastTaskStates.value.get(taskId);
+      
+      currentTaskStates.set(taskId, currentStatus);
+      
+      // 如果状态发生变化,标记需要更新
+      if (lastStatus && lastStatus !== currentStatus) {
+        hasStatusChanged = true;
+        console.log(`任务 ${taskId} 状态从 ${lastStatus} 变为 ${currentStatus}`);
+      }
+    });
+    
+    // 只有当状态发生变化时才更新UI
+    if (hasStatusChanged) {
+      taskList.value = currentTasks;
+      total.value = response.total;
+      console.log('检测到任务状态变化,更新UI');
+    }
+    
+    // 更新状态记录
+    lastTaskStates.value = currentTaskStates;
+    
+  } catch (error) {
+    console.error('检查任务状态失败:', error);
+  }
+};
+
+// 停止轮询
+const stopPolling = () => {
+  if (pollingTimer) {
+    clearInterval(pollingTimer);
+    pollingTimer = null;
+  }
+};
+
+onMounted(() => {
+  getList();
+  startPolling();
+});
+
+onUnmounted(() => {
+  stopPolling();
+});
+</script>
+
+<style scoped>
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 186 - 130
src/views/system/gameEvent/components/bibViewerDialog.vue

@@ -7,23 +7,35 @@
         <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>
+              <div class="upload-container">
+                <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>
+                <div v-if="bgImageFile" class="file-info">
+                  <span class="file-name">{{ bgImageFile.name }}</span>
+                  <el-button type="text" @click="removeBgImage" class="remove-btn">删除</el-button>
+                </div>
+              </div>
             </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>
+              <div class="upload-container">
+                <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>
+                <div v-if="logoImageFile" class="file-info">
+                  <span class="file-name">{{ logoImageFile.name }}</span>
+                  <el-button type="text" @click="removeLogoImage" class="remove-btn">删除</el-button>
+                </div>
+              </div>
             </el-form-item>
 
             <el-form-item label="字体设置">
@@ -80,14 +92,18 @@
         <!-- 右侧预览面板 -->
         <el-col :span="12">
           <div class="preview-container" ref="previewContainer">
-            <div class="preview-canvas" :style="{ backgroundImage: bgImageUrl ? `url(${bgImageUrl})` : 'none' }">
+            <div class="preview-canvas" :style="{ 
+              backgroundImage: bgImageUrl ? `url(${bgImageUrl})` : 'none',
+              backgroundSize: 'contain',
+              backgroundRepeat: 'no-repeat'
+            }">
               <!-- Logo元素 -->
               <div
                 v-if="logoImageUrl"
                 class="draggable-element logo-element"
                 :style="{
-                  left: bibForm.logoX + 'px',
-                  top: bibForm.logoY + 'px',
+                  left: bibForm.logoX + '%',
+                  top: bibForm.logoY + '%',
                   transform: `translateX(-50%) scale(${bibForm.logoScale})`,
                   transformOrigin: 'top left'
                 }"
@@ -102,8 +118,8 @@
               <div
                 class="draggable-element barcode-element"
                 :style="{
-                  left: bibForm.qRCodeX + 'px',
-                  top: bibForm.qRCodeY + 'px',
+                  left: (bibForm.qRCodeX || 11.67) + '%',
+                  top: (bibForm.qRCodeY || 32.5) + '%',
                   transform: `translateX(-50%) scale(${bibForm.barcodeScale})`,
                   transformOrigin: 'top left',
                 }"
@@ -148,8 +164,8 @@
                   fontSize: Math.min(28, Math.max(18, bibForm.fontSize * 0.7)) + 'px',
                   color: 'black',
                   fontFamily: '黑体',
-                  left: bibForm.eventX + '%',
-                  top: bibForm.eventY + '%',
+                  left: (bibForm.eventX || 50) + '%',
+                  top: (bibForm.eventY || 5) + '%', // 移除Y轴翻转,与PDF生成保持一致
                   transform: `translateX(-50%) scale(${bibForm.eventScale})`,
                   transformOrigin: 'top center'
                 }"
@@ -164,8 +180,8 @@
               <div
                 class="draggable-element number-element"
                 :style="{
-                  left: bibForm.numberX + '%',
-                  top: bibForm.numberY + '%',
+                  left: (bibForm.numberX || 50) + '%',
+                  top: (bibForm.numberY || 50) + '%', // 移除Y轴翻转,与PDF生成保持一致
                   transform: `translate(-50%, -50%) scale(${bibForm.numberScale})`,
                   fontSize: Math.min(bibForm.fontSize, 56) + 'px',
                   color: bibForm.fontColorHex,
@@ -187,7 +203,7 @@
     <template #footer>
       <div class="dialog-footer">
         <el-button @click="handleCloseBibDialog">取 消</el-button>
-        <el-button type="primary" @click="handleGenerateBibFile" :loading="bibDialog.loading">生成参赛证</el-button>
+        <el-button type="primary" @click="handleCreateTask" :loading="bibDialog.loading">创建任务</el-button>
       </div>
     </template>
   </el-dialog>
@@ -195,8 +211,10 @@
 
 <script setup lang="ts">
 import { ref, reactive, nextTick, getCurrentInstance } from 'vue';
-import { generateBib } from '@/api/system/gameEvent';
+import { useRouter } from 'vue-router';
+import { createBibTask } from '@/api/system/gameEvent/task';
 import type { UploadInstance } from 'element-plus';
+import { parseTime } from '@/utils/ruoyi';
 
 // 定义组件实例类型
 interface ComponentInternalInstance {
@@ -211,6 +229,11 @@ interface ComponentInternalInstance {
 
 // 获取组件实例
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
+
+// 预览框尺寸常量(与后端保持一致)
+const PREVIEW_WIDTH = 600;
+const PREVIEW_HEIGHT = 400;
 
 // 响应式数据定义
 const bibDialog = reactive({
@@ -221,20 +244,21 @@ const bibDialog = reactive({
 const selectedElement = ref<string>(''); // 当前选中的元素
 
 const bibForm = reactive({
-  logoX: 25,
-  logoY: 25,
-  qRCodeX: 70,
-  qRCodeY: 130,
+  // 统一使用百分比坐标系统 (0-100)
+  logoX: 4.17,      // 25/600 * 100 = 4.17%
+  logoY: 6.25,      // 25/400 * 100 = 6.25%
+  qRCodeX: 11.67,   // 70/600 * 100 = 11.67%
+  qRCodeY: 32.5,    // 130/400 * 100 = 32.5%
   fontName: 'simhei',
   fontSize: 36,
   fontColor: '#000000',
   fontColorHex: '#000000',
   eventName: '',
-  // 添加位置字段
-  numberX: 50,  // 百分比位置
-  numberY: 50,  // 百分比位置
-  eventX: 50,   // 百分比位置
-  eventY: 5,    // 百分比位置
+  // 位置字段(百分比)
+  numberX: 50,      // 百分比位置
+  numberY: 50,      // 百分比位置
+  eventX: 50,       // 百分比位置
+  eventY: 5,        // 百分比位置
   // 添加元素尺寸控制字段
   logoScale: 1,
   barcodeScale: 1,
@@ -317,11 +341,11 @@ const resetElementScale = () => {
 
 // 重置表单
 const resetBibForm = () => {
-  // 设置默认值(像素单位)- 适配2:3横屏比例
-  bibForm.logoX = 25;
-  bibForm.logoY = 25;
-  bibForm.qRCodeX = 70;
-  bibForm.qRCodeY = 130;
+  // 设置默认值(百分比单位)- 适配3:2横屏比例
+  bibForm.logoX = 4.17;      // 25/600 * 100 = 4.17%
+  bibForm.logoY = 6.25;      // 25/400 * 100 = 6.25%
+  bibForm.qRCodeX = 11.67;   // 70/600 * 100 = 11.67%
+  bibForm.qRCodeY = 32.5;    // 130/400 * 100 = 32.5%
   bibForm.fontName = 'simhei';
   bibForm.fontSize = 36;
   bibForm.fontColor = '#000000';
@@ -397,6 +421,27 @@ const handleLogoImageChange = (file: any) => {
   }
 };
 
+// 删除背景图片
+const removeBgImage = () => {
+  bgImageFile.value = null;
+  bgImageUrl.value = '';
+  bgImageDimensions.value = { width: 0, height: 0 };
+  if (bgUploadRef.value) {
+    bgUploadRef.value.clearFiles();
+  }
+  proxy?.$modal.msgSuccess('背景图片已删除');
+};
+
+// 删除Logo图片
+const removeLogoImage = () => {
+  logoImageFile.value = null;
+  logoImageUrl.value = '';
+  if (logoUploadRef.value) {
+    logoUploadRef.value.clearFiles();
+  }
+  proxy?.$modal.msgSuccess('Logo图片已删除');
+};
+
 // 字体颜色改变处理
 const handleFontColorChange = (color: string) => {
   bibForm.fontColor = color;
@@ -441,25 +486,23 @@ const handleDrag = (event: MouseEvent) => {
   const deltaY = event.clientY - dragState.startY;
 
   const container = previewContainer.value;
-  const previewWidth = container?.clientWidth || container?.offsetWidth || 600;
-  const previewHeight = container?.clientHeight || container?.offsetHeight || 400;
+  const previewWidth = container?.clientWidth || container?.offsetWidth || PREVIEW_WIDTH;
+  const previewHeight = container?.clientHeight || container?.offsetHeight || PREVIEW_HEIGHT;
 
+  // 统一使用百分比坐标系统
+  const newX = Math.max(0, Math.min(100, dragState.startLeft + (deltaX / previewWidth) * 100));
+  const newY = Math.max(0, Math.min(100, dragState.startTop + (deltaY / previewHeight) * 100));
+  
   if (dragState.dragTarget === 'logo') {
-    bibForm.logoX = Math.max(0, dragState.startLeft + deltaX);
-    bibForm.logoY = Math.max(0, dragState.startTop + deltaY);
+    bibForm.logoX = newX;
+    bibForm.logoY = newY;
   } else if (dragState.dragTarget === 'barcode') {
-    bibForm.qRCodeX = Math.max(0, dragState.startLeft + deltaX);
-    bibForm.qRCodeY = Math.max(0, dragState.startTop + deltaY);
+    bibForm.qRCodeX = newX;
+    bibForm.qRCodeY = newY;
   } else if (dragState.dragTarget === 'number') {
-    // 修复:实际更新位置数据
-    const newX = Math.max(0, Math.min(100, dragState.startLeft + (deltaX / previewWidth) * 100));
-    const newY = Math.max(0, Math.min(100, dragState.startTop + (deltaY / previewHeight) * 100));
     bibForm.numberX = newX;
     bibForm.numberY = newY;
   } else if (dragState.dragTarget === 'event') {
-    // 修复:实际更新位置数据
-    const newX = Math.max(0, Math.min(100, dragState.startLeft + (deltaX / previewWidth) * 100));
-    const newY = Math.max(0, Math.min(100, dragState.startTop + (deltaY / previewHeight) * 100));
     bibForm.eventX = newX;
     bibForm.eventY = newY;
   }
@@ -477,8 +520,8 @@ const stopDrag = () => {
 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;
+  const previewWidth = container?.clientWidth || container?.offsetWidth || PREVIEW_WIDTH;
+  const previewHeight = container?.clientHeight || container?.offsetHeight || PREVIEW_HEIGHT;
 
   // 使用实际背景图片尺寸,如果没有则使用默认3:2横屏比例尺寸(600×400px)
   const actualWidth = bgImageDimensions.value?.width || 600;
@@ -571,25 +614,22 @@ const processImageToRatio = (file: File, targetRatio: number, targetRatio2: numb
   });
 };
 
-// 生成参赛证文件
-const handleGenerateBibFile = async () => {
-  // 校验必须上传背景图和logo
+// 创建参赛证生成任务
+const handleCreateTask = async () => {
+  // 校验必须上传背景图
   if (!bgImageFile.value) {
     proxy?.$modal.msgError('请上传背景图片');
     return;
   }
 
-  // logo为非必要
-  // if (!logoImageFile.value) {
-  //   proxy?.$modal.msgError('请上传Logo图片');
-  //   return;
-  // }
-
   bibDialog.loading = true;
   try {
     let qRCodeX = bibForm.qRCodeX;
     let qRCodeY = bibForm.qRCodeY;
 
+    console.log('二维码坐标检查 - bibForm.qRCodeX:', bibForm.qRCodeX, 'bibForm.qRCodeY:', bibForm.qRCodeY);
+    console.log('二维码坐标检查 - qRCodeX:', qRCodeX, 'qRCodeY:', qRCodeY);
+
     // 如果值为null或undefined,强制使用默认值
     if (qRCodeX === null || qRCodeX === undefined || isNaN(qRCodeX)) {
       qRCodeX = 70;
@@ -610,91 +650,67 @@ const handleGenerateBibFile = async () => {
     // 等待一帧确保所有尺寸都已计算完成
     await nextTick();
 
-    // Logo坐标(左上角)
-    const logoCoords = convertCoordinatesWithScale(bibForm.logoX || 25, bibForm.logoY || 25);
+    // 统一使用百分比坐标系统
+    const logoCoords = {
+      x: bibForm.logoX || 4.17,    // 百分比
+      y: bibForm.logoY || 6.25     // 百分比
+    };
 
-    // 二维码坐标(左上角)
-    const qrCoords = convertCoordinatesWithScale(qRCodeX, qRCodeY);
+    // 二维码坐标(百分比)
+    const validQRCodeX = (qRCodeX !== null && qRCodeX !== undefined && !isNaN(qRCodeX)) ? qRCodeX : 11.67;
+    const validQRCodeY = (qRCodeY !== null && qRCodeY !== undefined && !isNaN(qRCodeY)) ? qRCodeY : 32.5;
+    
+    const qrCoords = {
+      x: validQRCodeX, // 百分比
+      y: validQRCodeY  // 百分比
+    };
+    
+    console.log('二维码坐标转换 - 原始坐标:', { qRCodeX, qRCodeY }, '有效坐标:', { validQRCodeX, validQRCodeY }, '转换后坐标:', qrCoords);
 
     const bibParams = {
+      // Logo坐标(百分比)
       logoX: logoCoords.x,
       logoY: logoCoords.y,
+      // 二维码坐标(百分比)
       qRCodeX: qrCoords.x,
       qRCodeY: qrCoords.y,
       fontName: bibForm.fontName || 'simhei',
       fontSize: Math.round((bibForm.fontSize || 36) * 0.75),
       fontColor: parseInt((bibForm.fontColor || '#000000').replace('#', ''), 16),
       eventName: bibForm.eventName || '',
+      // 位置参数(百分比)
+      numberX: bibForm.numberX,
+      numberY: bibForm.numberY,
+      eventX: bibForm.eventX,
+      eventY: bibForm.eventY,
       // 缩放参数
       logoScale: bibForm.logoScale,
       barcodeScale: bibForm.barcodeScale,
       numberScale: bibForm.numberScale,
       eventScale: bibForm.eventScale,
     };
-
-    // 显示进度提示
-    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('参赛证生成成功');
+    
+    console.log('发送给后端的参数:', bibParams);
+
+    const taskName = `参赛证生成_${parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')}`;
+    
+    // 调用创建任务API
+    const response = await createBibTask(
+      bgImageFile.value, 
+      logoImageFile.value, 
+      taskName, 
+      bibParams
+    );
+    
+    proxy?.$modal.msgSuccess('任务创建成功,请到任务列表查看进度');
     handleCloseBibDialog();
+    
+    // 跳转到任务列表页面
+    router.push('/system/gameEvent/taskList');
+    
   } 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);
+    console.error('创建任务失败:', error);
+    proxy?.$modal.msgError('创建任务失败:' + error.message);
   } finally {
     bibDialog.loading = false;
   }
@@ -713,9 +729,11 @@ defineExpose({
   resetBibForm,
   handleBgImageChange,
   handleLogoImageChange,
+  removeBgImage,
+  removeLogoImage,
   handleFontColorChange,
   startDrag,
-  handleGenerateBibFile
+  handleCreateTask
 });
 </script>
 
@@ -766,7 +784,7 @@ defineExpose({
   width: 100%;
   height: 100%;
   position: relative;
-  background-size: cover;
+  background-size: auto;
   background-position: center;
   background-repeat: no-repeat;
   background-color: #f5f5f5;
@@ -805,6 +823,44 @@ defineExpose({
   background-color: rgba(103, 194, 58, 0.1);
 }
 
+/* 上传容器样式 */
+.upload-container {
+  width: 100%;
+}
+
+.file-info {
+  margin-top: 8px;
+  padding: 8px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  max-width: 100%;
+}
+
+.file-name {
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: 12px;
+  color: #606266;
+  margin-right: 8px;
+}
+
+.remove-btn {
+  color: #f56c6c;
+  font-size: 12px;
+  padding: 0;
+  min-height: auto;
+}
+
+.remove-btn:hover {
+  color: #f56c6c;
+  background-color: transparent;
+}
+
 .number-element {
   font-weight: bold;
   text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);

+ 10 - 0
src/views/system/gameEvent/index.vue

@@ -93,6 +93,11 @@
               >生成参赛证
             </el-button>
           </el-col>
+          <!-- <el-col :span="1.5">
+            <el-button type="info" plain icon="List" @click="handleTaskList" v-hasPermi="['system:gameEvent:numberBib']"
+              >任务列表
+            </el-button>
+          </el-col> -->
           <right-toolbar v-model:showSearch="showSearch" :columns="columns" @queryTable="getList"></right-toolbar>
         </el-row>
       </template>
@@ -1055,6 +1060,11 @@ const handleGenerateBib = () => {
   }
 };
 
+// 任务列表按钮处理
+const handleTaskList = () => {
+  router.push('/system/gameEvent/taskList');
+};
+
 onMounted(() => {
   // 获取默认赛事信息
   gameEventStore.fetchDefaultEvent();