|
@@ -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>
|