Переглянути джерело

```
feat(gameEvent): 更新赛事编辑器和参赛证预览功能

- 移除竞赛流程标签页,修改默认激活标签页为竞赛项目
- 增加ElMessage导入和生命周期钩子onUpdated
- 优化路由参数获取逻辑,支持从路由或store获取eventId
- 重构参赛证预览组件的背景图片显示方式,使用img标签替代
- 修复各元素缩放计算,统一使用canvasScale进行缩放处理
- 调整字体大小输入范围,从38-198调整为8-198
- 注释掉画布比例选择功能
- 优化坐标转换逻辑,将百分比坐标转换为像素坐标后传递给后端
- 修复参赛证生成时的尺寸和位置计算问题
- 调整预览画布样式,设置固定宽高并居中显示
```

zhou 3 місяців тому
батько
коміт
531060b674

+ 26 - 10
src/views/system/gameEvent/components/ArticleEditor.vue

@@ -23,7 +23,7 @@
             class="mb-2"
           />
           <el-tabs v-model="activeTab" @tab-click="handleTabClick">
-            <el-tab-pane label="竞赛流程" name="competition-process">
+            <!-- <el-tab-pane label="竞赛流程" name="competition-process">
               <div class="article-form">
                 <el-form-item label="标题">
                   <el-input v-model="articleData.competitionProcess.title" placeholder="请输入标题" />
@@ -35,7 +35,7 @@
                   <el-input v-model="articleData.competitionProcess.remark" placeholder="请输入备注" type="textarea" :rows="3" />
                 </el-form-item>
               </div>
-            </el-tab-pane>
+            </el-tab-pane> -->
   
             <el-tab-pane label="竞赛项目" name="competition-items">
               <div class="article-form">
@@ -184,7 +184,8 @@
   
   <script setup lang="ts" name="GameEventArticleEditor">
   import { useRouter, useRoute } from 'vue-router';
-  import { ref, reactive, onMounted } from 'vue';
+  import { ref, reactive, computed, onMounted, onUpdated } from 'vue';
+  import { ElMessage } from 'element-plus';
   import Editor from '@/components/Editor/index.vue';
   import { getEventMdByEventAndType, editEventMd } from '@/api/system/eventMd';
   import type { EventMdForm } from '@/api/system/eventMd/types';
@@ -201,11 +202,14 @@
   const pageTitle = ref<string>('');
   const saving = ref<boolean>(false);
   
+  // 获取路由参数中的eventId
+  const routeEventId = computed(() => route.params.eventId as string | undefined);
+  
   const articleDialog = reactive({
-    currentEventId: defaultEventInfo.value.eventId,
+    currentEventId: defaultEventInfo.value?.eventId || routeEventId.value,
   });
   
-  const activeTab = ref('competition-process');
+  const activeTab = ref('competition-items');
   
   const tabTypeMapping: Record<string, number> = {
     'competition-process': 1,
@@ -329,14 +333,26 @@
   const handleBack = () => {
     router.back();
   };
+
+  onUpdated(async () => {
+    // 从路由参数或store获取eventId
+    const eventId = route.params.eventId as string | undefined || defaultEventInfo.value?.eventId;
+    articleDialog.currentEventId = eventId;
+    if (eventId) {
+      await loadTabData(activeTab.value);
+    }
+  });
   
   onMounted(async () => {
-    const eventId = defaultEventInfo.value.eventId;
-    // const eventIdParam = route.params.eventId as string | undefined;
+    // 先获取默认赛事信息
+    await gameEventStore.fetchDefaultEvent();
+    // 从路由参数或store获取eventId
+    const eventId = route.params.eventId as string | undefined || defaultEventInfo.value?.eventId;
     articleDialog.currentEventId = eventId;
-    // pageTitle.value = eventIdParam ? `赛事ID: ${eventIdParam}` : '未指定赛事';
-    // 初次进入加载默认标签页
-    await loadTabData(activeTab.value);
+    if (eventId) {
+      // 初次进入加载默认标签页
+      await loadTabData(activeTab.value);
+    }
   });
   </script>
   

+ 94 - 70
src/views/system/gameEvent/components/bibViewerDialog.vue

@@ -4,13 +4,9 @@
     <div class="bib-generator">
       <!-- 预览面板 - 占满宽度 -->
       <div class="preview-container" ref="previewContainer">
-        <div class="preview-canvas" :style="{ 
-          backgroundImage: bgImageUrl ? `url(${bgImageUrl})` : 'none',
-          backgroundSize: '100% 100%',
-          backgroundRepeat: 'no-repeat',
-          transform: `scale(${canvasScale})`,
-          transformOrigin: 'center center'
-        }">
+        <div class="preview-canvas">
+          <!-- 背景图片 -->
+          <img v-if="bgImageUrl" :src="bgImageUrl" class="background-image" />
           <!-- Logo元素 -->
           <div
             v-if="logoImageUrl"
@@ -18,7 +14,7 @@
             :style="{
               left: bibForm.logoX + '%',
               top: bibForm.logoY + '%',
-              transform: `translateX(-50%) scale(${bibForm.logoScale})`,
+              transform: `translateX(-50%) scale(${bibForm.logoScale * canvasScale})`,
               transformOrigin: 'top left'
             }"
             @mousedown="startDrag($event, 'logo')"
@@ -33,7 +29,7 @@
             :style="{
               left: (bibForm.qRCodeX || 11.67) + '%',
               top: (bibForm.qRCodeY || 32.5) + '%',
-              transform: `translateX(-50%) scale(${bibForm.barcodeScale})`,
+              transform: `translateX(-50%) translateY(-50%) scale(${bibForm.barcodeScale * canvasScale})`,
               transformOrigin: 'top left',
             }"
             @mousedown="startDrag($event, 'barcode')"
@@ -74,12 +70,12 @@
             v-if="bibForm.eventName"
             class="event-name-preview draggable-element"
             :style="{
-              fontSize: Math.min(28, Math.max(18, bibForm.fontSize * 0.7)) + 'px',
-              color: 'black',
-              fontFamily: '黑体',
+              fontSize: Math.min(bibForm.fontSize, 56) + 'px',
+              color: bibForm.fontColorHex,
+              fontFamily: bibForm.fontName,
               left: (bibForm.eventX || 50) + '%',
               top: (bibForm.eventY || 5) + '%',
-              transform: `translateX(-50%) scale(${bibForm.eventScale})`,
+              transform: `translateX(-50%) scale(${bibForm.eventScale * canvasScale})`,
               transformOrigin: 'top center'
             }"
             @mousedown="startDrag($event, 'event')"
@@ -95,7 +91,7 @@
             :style="{
               left: (bibForm.numberX || 50) + '%',
               top: (bibForm.numberY || 50) + '%',
-              transform: `translate(-50%, -50%) scale(${bibForm.numberScale})`,
+              transform: `translate(-50%, -50%) scale(${bibForm.numberScale * canvasScale})`,
               fontSize: Math.min(bibForm.fontSize, 56) + 'px',
               color: bibForm.fontColorHex,
               fontFamily: bibForm.fontName,
@@ -173,7 +169,7 @@
                   <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-input-number v-model="bibForm.fontSize" :min="8" :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>
@@ -185,7 +181,7 @@
           </el-col>
         </el-row>
         <el-row :gutter="20">
-          <el-col :span="12">
+          <!-- <el-col :span="12">
             <el-form-item label="画布比例">
               <el-select v-model="canvasScale" placeholder="选择画布比例" style="width: 200px">
                 <el-option label="1/4 (四分之一)" :value="0.25"></el-option>
@@ -195,7 +191,7 @@
                 <el-option label="1/1 (原始大小)" :value="1"></el-option>
               </el-select>
             </el-form-item>
-          </el-col>
+          </el-col> -->
           <el-col :span="12">
           </el-col>
         </el-row>
@@ -669,31 +665,20 @@ const handleCreateTask = async () => {
     // 等待一帧确保所有尺寸都已计算完成
     await nextTick();
 
-    // 统一使用百分比坐标系统
-    const logoCoords = {
-      x: bibForm.logoX || 4.17,    // 百分比
-      y: bibForm.logoY || 6.25     // 百分比
-    };
+    // 计算画布的实际尺寸(不考虑预览缩放)
+    const originalWidth = bgImageDimensions.value?.width || 600;
+    const originalHeight = bgImageDimensions.value?.height || 400;
 
-    // 二维码坐标(百分比)
-    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  // 百分比
+    // 将百分比坐标转换为像素坐标
+    const convertPercentToPixel = (percent: number, dimension: number): number => {
+      return Math.round((percent / 100) * dimension);
     };
-    
-    console.log('二维码坐标转换 - 原始坐标:', { qRCodeX, qRCodeY }, '有效坐标:', { validQRCodeX, validQRCodeY }, '转换后坐标:', qrCoords);
 
-    // 计算画布比例处理后的宽高
-    const originalWidth = bgImageDimensions.value?.width || 600;
-    const originalHeight = bgImageDimensions.value?.height || 400;
-    const scaledWidth = Math.round(originalWidth * canvasScale.value);
-    const scaledHeight = Math.round(originalHeight * canvasScale.value);
+    // Logo像素坐标和尺寸
+    const logoPixelX = convertPercentToPixel(bibForm.logoX || 4.17, originalWidth);
+    const logoPixelY = convertPercentToPixel(bibForm.logoY || 6.25, originalHeight);
 
     // 计算Logo尺寸(前端统一处理,避免后端重复计算)
-    // 预览时限制为最大80px,应用用户缩放,计算最终尺寸传递给后端
     const logoOriginalWidth = logoImageDimensions.value?.width || 80;
     const logoOriginalHeight = logoImageDimensions.value?.height || 80;
     const logoPreviewSize = 80; // 前端预览时的最大尺寸
@@ -701,33 +686,63 @@ const handleCreateTask = async () => {
     const logoFinalWidth = Math.round(logoOriginalWidth * logoScale * bibForm.logoScale);
     const logoFinalHeight = Math.round(logoOriginalHeight * logoScale * bibForm.logoScale);
 
+    // 二维码像素坐标
+    const validQRCodeX = (qRCodeX !== null && qRCodeX !== undefined && !isNaN(qRCodeX)) ? qRCodeX : 11.67;
+    const validQRCodeY = (qRCodeY !== null && qRCodeY !== undefined && !isNaN(qRCodeY)) ? qRCodeY : 32.5;
+    const qrPixelX = convertPercentToPixel(validQRCodeX, originalWidth);
+    const qrPixelY = convertPercentToPixel(validQRCodeY, originalHeight);
+
+    // 计算二维码尺寸(与前端预览一致:32px * 缩放比例)
+    const qrInitialSize = 32;
+    const qrFinalWidth = Math.round(qrInitialSize * bibForm.barcodeScale);
+    const qrFinalHeight = Math.round(qrInitialSize * bibForm.barcodeScale);
+
+    // 号码像素坐标
+    const numberPixelX = convertPercentToPixel(bibForm.numberX || 50, originalWidth);
+    const numberPixelY = convertPercentToPixel(bibForm.numberY || 50, originalHeight);
+
+    // 计算号码字体大小(应用缩放)
+    const numberPixelFontSize = Math.round((bibForm.fontSize || 36) * bibForm.numberScale);
+
+    // 赛事名称像素坐标
+    const eventPixelX = convertPercentToPixel(bibForm.eventX || 50, originalWidth);
+    const eventPixelY = convertPercentToPixel(bibForm.eventY || 5, originalHeight);
+
+    // 计算赛事名称字体大小(与前端预览一致:Math.min(28, Math.max(18, fontSize * 0.7)))
+    const eventPixelFontSize = Math.round((bibForm.fontSize || 36) * bibForm.eventScale);
+
     const bibParams = {
-      // Logo坐标(百分比)
-      logoX: logoCoords.x,
-      logoY: logoCoords.y,
-      // 二维码坐标(百分比)
-      qRCodeX: qrCoords.x,
-      qRCodeY: qrCoords.y,
+      // 画布实际尺寸
+      width: originalWidth,
+      height: originalHeight,
+      
+      // Logo参数(像素)
+      logoX: logoPixelX,
+      logoY: logoPixelY,
+      logoWidth: logoFinalWidth,
+      logoHeight: logoFinalHeight,
+      
+      // 二维码参数(像素)
+      qRCodeX: qrPixelX,
+      qRCodeY: qrPixelY,
+      qRCodeWidth: qrFinalWidth,
+      qRCodeHeight: qrFinalHeight,
+      
+      // 号码参数(像素)
+      numberX: numberPixelX,
+      numberY: numberPixelY,
+      numberFontSize: numberPixelFontSize,
+      
+      // 赛事名称参数(像素)
+      eventX: eventPixelX,
+      eventY: eventPixelY,
+      eventFontSize: eventPixelFontSize,
+      
+      // 其他通用参数
       fontName: bibForm.fontName || 'simhei',
-      fontSize: bibForm.fontSize || 36, // 直接传递原始字体大小,后端会应用numberScale
+      fontSize: bibForm.fontSize || 36,
       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,
-      // 画布比例处理后的宽高
-      width: scaledWidth,
-      height: scaledHeight,
-      // Logo尺寸
-      logoWidth: logoFinalWidth,
-      logoHeight: logoFinalHeight,
     };
     
     console.log('发送给后端的参数:', bibParams);
@@ -783,22 +798,23 @@ defineExpose({
 /* 生成参赛证样式 */
 .bib-generator {
   padding: 10px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;    // 让内容居中
 }
 
 /* 预览容器 - 占满宽度 */
 .preview-container {
   border: 2px dashed #ddd;
   border-radius: 8px;
-  min-height: 300px;
-  max-height: 400px;
+  width: 600px !important;
+  height: 400px !important;
   position: relative;
   overflow: auto;
-  aspect-ratio: 3/2;
   display: flex;
   align-items: center;
   justify-content: center;
-  width: 100%;
-  margin-bottom: 20px;
+  margin: 0 auto 20px;        // 居中显示并保留底部间距
   background-color: #f8f9fa;
 }
 
@@ -832,17 +848,25 @@ defineExpose({
 }
 
 .preview-canvas {
-  width: 100%;
-  height: 100%;
+  width: 600px;
+  height: 400px;
   position: relative;
-  background-size: 100% 100%;
-  background-position: center;
-  background-repeat: no-repeat;
   background-color: #f5f5f5;
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
+  overflow: hidden;
+}
+
+.background-image {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  z-index: 0;
 }
 
 .draggable-element {