5 커밋 79f20caf5a ... 0659dd99c7

작성자 SHA1 메시지 날짜
  zhou 0659dd99c7 feat(gameEvent): 添加自定义字体上传和云端字体库功能 2 주 전
  zhou ef37635582 refactor(gameEvent): 移除未使用的画布比例选择器和排行榜文章编辑功能 2 주 전
  zhou f3a50f475d feat(gameScore): 添加成绩导入功能 2 주 전
  zhou 73187206c2 feat(system): 参赛队伍管理增加导入更新功能 2 주 전
  zhou 398ae88460 feat(gameAthlete): 优化参赛队员表格显示 2 주 전

+ 30 - 0
src/api/system/gameScore/index.ts

@@ -219,3 +219,33 @@ export const exportProjectScore = (eventId: string | number, projectId: string |
     responseType: 'blob'
   });
 };
+
+/**
+ * 导入成绩
+ * @param data 导入数据
+ */
+export const importScore = (data: any) => {
+  return request({
+    url: '/system/gameScore/import',
+    method: 'post',
+    data: data,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
+
+/**
+ * 下载导入模板
+ * @param eventId 赛事ID
+ * @param projectId 项目ID
+ * @param classification 项目分类
+ */
+export const importTemplate = (eventId: string | number, projectId: string | number, classification: string) => {
+  return request({
+    url: '/system/gameScore/importTemplate',
+    method: 'post',
+    params: { eventId, projectId, classification },
+    responseType: 'blob'
+  });
+};

+ 4 - 52
src/views/system/gameAthlete/index.vue

@@ -61,7 +61,7 @@
         <el-table-column label="序号" align="center" type="index" />
         <el-table-column label="运动员id" align="center" prop="athleteId" v-if="columns[0].visible" />
         <el-table-column label="赛事名称" align="center" prop="eventName" width="120px" v-if="columns[1].visible" />
-        <el-table-column label="号码" align="center" prop="athleteCode" width="100px" v-if="columns[2].visible" />
+        <el-table-column label="号码" align="center" prop="athleteCode" sortable width="100px" v-if="columns[2].visible" />
         <el-table-column label="姓名" align="center" prop="name" v-if="columns[3].visible" />
         <el-table-column label="性别" align="center" prop="gender" v-if="columns[4].visible">
           <template #default="scope">
@@ -70,10 +70,10 @@
         </el-table-column>
         <el-table-column label="年龄" align="center" prop="age" v-if="columns[5].visible" />
         <el-table-column label="手机号" align="center" prop="phone" v-if="columns[11].visible" />
-        <el-table-column label="参与项目" align="center" prop="projectList" width="200px" :show-overflow-tooltip="true" v-if="columns[6].visible">
-          <template #default="scope">
+        <el-table-column label="参与项目" align="center" prop="projectNames" width="200px" :show-overflow-tooltip="true" v-if="columns[6].visible" >
+          <!-- <template #default="scope">
             {{ formatProjectList(scope.row.projectList) }}
-          </template>
+          </template> -->
         </el-table-column>
         <!-- <el-table-column label="证件号" align="center" prop="idCard" v-if="columns[7].visible" /> -->
         <el-table-column label="队伍" align="center" prop="teamId" v-if="columns[8].visible">
@@ -480,30 +480,6 @@ const getTeamNameById = (teamId: string | number | null | undefined) => {
   return team ? team.teamName : '';
 };
 
-// 获取赛事项目列表
-const getProjectList = async () => {
-  try {
-    const res = await listGameEventProject();
-    gameEventProjectList.value = res.rows.map((item) => ({
-      key: String(item.projectId),
-      label: item.projectName,
-      disabled: false
-    }));
-    
-    // 更新项目状态
-    await updateProjectStatus();
-  } catch (error) {
-    // console.error('获取项目列表失败:', error);
-  }
-};
-
-// 格式化项目列表显示
-const formatProjectList = (projectList: number[]) => {
-  if (!projectList) return '';
-  const projectNames = gameEventProjectList.value.filter((p) => projectList.includes(Number(p.key))).map((p) => p.label);
-  return projectNames.join(',');
-};
-
 /** 查询参赛队员列表 */
 const getList = async () => {
   loading.value = true;
@@ -562,12 +538,6 @@ const handleAdd = () => {
   reset();
   dialog.visible = true;
   dialog.title = '添加参赛队员';
-  // 获取项目列表
-  // nextTick(() => {
-  //   if (form.value.eventId) {
-  //     getProjectList(String(form.value.eventId));
-  //   }
-  // });
 };
 
 /** 修改按钮操作 */
@@ -588,12 +558,6 @@ const handleUpdate = async (row?: GameAthleteVO) => {
 
   dialog.visible = true;
   dialog.title = '修改参赛队员';
-  // 获取项目列表
-  // nextTick(() => {
-  //   if (form.value.eventId) {
-  //     getProjectList(String(form.value.eventId));
-  //   }
-  // });
 };
 
 // 添加一个计算属性用于处理projectList2的类型转换
@@ -698,17 +662,6 @@ const submitForm = () => {
   });
 };
 
-// 在组件加载时更新项目状态
-const updateProjectStatus = async () => {
-  if (gameEventProjectList.value.length > 0) {
-    for (const project of gameEventProjectList.value) {
-      const projectId = Number(project.key);
-      const isFull = await isProjectFull(projectId);
-      project.disabled = isFull;
-    }
-  }
-};
-
 /** 删除按钮操作 */
 const handleDelete = async (row?: GameAthleteVO) => {
   const _athleteIds = row?.athleteId || ids.value;
@@ -805,7 +758,6 @@ const importTemplate = () => {
 onMounted(() => {
   getList();
   getTeamList();
-  getProjectList();
 });
 </script>
 

+ 140 - 38
src/views/system/gameEvent/components/bibViewerDialog.vue

@@ -69,6 +69,7 @@
           <div
             v-if="bibForm.eventName"
             class="event-name-preview draggable-element"
+            v-loading="fontPreviewLoading"
             :style="{
               fontSize: Math.min(bibForm.fontSize, 56) + 'px',
               color: bibForm.fontColorHex,
@@ -88,6 +89,9 @@
           <!-- 示例数字 12345 -->
           <div
             class="draggable-element number-element"
+            v-loading="fontPreviewLoading"
+            element-loading-text="加载字体..."
+            element-loading-background="rgba(255, 255, 255, 0.7)"
             :style="{
               left: (bibForm.numberX || 50) + '%',
               top: (bibForm.numberY || 50) + '%',
@@ -163,14 +167,27 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="字体设置">
-              <div style="display: flex; gap: 15px; align-items: center">
-                <el-select v-model="bibForm.fontName" placeholder="字体" style="width: 100px">
+              <div style="display: flex; gap: 15px; align-items: center; flex-wrap: wrap">
+                <el-select v-model="bibForm.fontName" filterable clearable 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-option v-if="bibForm.isCustomFont" :label="bibForm.customFontName" :value="bibForm.customFontNameValue"></el-option>
                 </el-select>
                 <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>
+                <el-upload
+                  class="font-upload"
+                  :auto-upload="true"
+                  :show-file-list="false"
+                  :action="uploadUrl"
+                  :headers="uploadHeaders"
+                  :on-success="handleFontUploadSuccess"
+                  accept=".ttf,.otf"
+                >
+                  <el-button type="primary" link icon="Upload">上传字体</el-button>
+                </el-upload>
+                <el-button type="primary" link icon="FolderOpened" @click="openCloudFontDialog">云端字体库</el-button>
               </div>
             </el-form-item>
           </el-col>
@@ -181,17 +198,6 @@
           </el-col>
         </el-row>
         <el-row :gutter="20">
-          <!-- <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>
-                <el-option label="1/3 (三分之一)" :value="0.33"></el-option>
-                <el-option label="1/2 (二分之一)" :value="0.5"></el-option>
-                <el-option label="3/4 (四分之三)" :value="0.75"></el-option>
-                <el-option label="1/1 (原始大小)" :value="1"></el-option>
-              </el-select>
-            </el-form-item>
-          </el-col> -->
           <el-col :span="12">
           </el-col>
         </el-row>
@@ -242,11 +248,31 @@
       </div>
     </template>
   </el-dialog>
+
+  <!-- 云端字体选择对话框 -->
+  <el-dialog v-model="cloudFontVisible" title="选择云端字体" width="600px" append-to-body>
+    <el-table v-loading="fontLoading" :data="fontList" @row-click="handleCloudFontSelect" highlight-current-row>
+      <el-table-column label="字体名称" align="center" prop="originalName" />
+      <el-table-column label="上传时间" align="center" prop="createTime" width="180" />
+      <el-table-column label="操作" align="center" width="100">
+        <template #default="scope">
+          <el-button link type="primary" @click="handleCloudFontSelect(scope.row)">选择</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="cloudFontVisible = false">取 消</el-button>
+      </div>
+    </template>
+  </el-dialog>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, nextTick, getCurrentInstance } from 'vue';
 import { createBibTask } from '@/api/system/gameEvent/task';
+import { listOss } from '@/api/system/oss';
+import { getToken } from '@/utils/auth';
 import type { UploadInstance } from 'element-plus';
 import { parseTime } from '@/utils/ruoyi';
 
@@ -305,7 +331,23 @@ const bibForm = reactive({
   barcodeScale: 1,
   numberScale: 1,
   eventScale: 1,
+  // 字体相关
+  fontOssId: null as number | null,
+  isCustomFont: false,
+  customFontName: '',
+  customFontNameValue: '' // 存储自定义字体的内部标识符,如 CustomFont_123
+});
+
+const uploadHeaders = ref({
+  Authorization: 'Bearer ' + getToken(),
+  clientid: import.meta.env.VITE_APP_CLIENT_ID
 });
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
+
+const cloudFontVisible = ref(false);
+const fontLoading = ref(false);
+const fontPreviewLoading = ref(false); // 字体预览加载状态
+const fontList = ref<any[]>([]);
 
 const bgImageFile = ref<File | null>(null);
 const logoImageFile = ref<File | null>(null);
@@ -416,6 +458,12 @@ const resetBibForm = () => {
   selectedElement.value = '';
   // 重置画布比例
   canvasScale.value = 1;
+  
+  // 重置自定义字体
+  bibForm.fontOssId = null;
+  bibForm.isCustomFont = false;
+  bibForm.customFontName = '';
+  bibForm.customFontNameValue = '';
 
   // 清除上传的文件
   if (bgUploadRef.value) {
@@ -502,6 +550,84 @@ const handleFontColorChange = (color: string) => {
   bibForm.fontColorHex = color;
 };
 
+// 上传字体成功回调
+const handleFontUploadSuccess = (response: any) => {
+  if (response.code === 200) {
+    const ossData = response.data;
+    loadCustomFont(ossData.url, ossData.originalName, ossData.ossId);
+    proxy?.$modal.msgSuccess('字体上传成功');
+  } else {
+    proxy?.$modal.msgError(response.msg || '上传失败');
+  }
+};
+
+// 打开云端字体对话框
+const openCloudFontDialog = async () => {
+  cloudFontVisible.value = true;
+  fontLoading.value = true;
+  try {
+    const res = await listOss({ fileSuffix: '.ttf' } as any);
+    const resOtf = await listOss({ fileSuffix: '.otf' } as any);
+    fontList.value = [...(res.rows || []), ...(resOtf.rows || [])];
+  } finally {
+    fontLoading.value = false;
+  }
+};
+
+// 选择云端字体
+const handleCloudFontSelect = (row: any) => {
+  loadCustomFont(row.url, row.originalName, row.ossId);
+  cloudFontVisible.value = false;
+};
+
+// 加载并注册自定义字体以供预览
+const loadCustomFont = async (url: string, name: string, ossId: number) => {
+  // 1. 处理名称:去除后缀
+  const cleanName = name.replace(/\.[^/.]+$/, "");
+  const fontFamily = `CustomFont_${ossId}`;
+  
+  // 2. 检查缓存:如果该字体已经加载过,直接应用
+  const existingFont = Array.from(document.fonts).find(f => f.family === fontFamily);
+  if (existingFont && existingFont.status === 'loaded') {
+    bibForm.fontName = fontFamily;
+    bibForm.fontOssId = ossId;
+    bibForm.isCustomFont = true;
+    bibForm.customFontName = cleanName;
+    bibForm.customFontNameValue = fontFamily;
+    return;
+  }
+
+  fontPreviewLoading.value = true;
+  try {
+    const fontFace = new FontFace(fontFamily, `url(${url})`);
+    // 显式添加到集合中
+    document.fonts.add(fontFace);
+    
+    // 3. 等待加载完成
+    await fontFace.load();
+    
+    // 4. 更新状态
+    bibForm.fontName = fontFamily;
+    bibForm.fontOssId = ossId;
+    bibForm.isCustomFont = true;
+    bibForm.customFontName = cleanName;
+    bibForm.customFontNameValue = fontFamily;
+    
+    proxy?.$modal.msgSuccess(`成功加载字体: ${cleanName}`);
+  } catch (error) {
+    console.error('加载字体文件失败:', error);
+    proxy?.$modal.msgError('加载预览字体失败');
+    
+    // 即使加载失败也记录信息
+    bibForm.fontName = fontFamily;
+    bibForm.fontOssId = ossId;
+    bibForm.isCustomFont = true;
+    bibForm.customFontName = cleanName;
+  } finally {
+    fontPreviewLoading.value = false;
+  }
+};
+
 // 开始拖拽
 const startDrag = (event: MouseEvent, target: string) => {
   event.preventDefault();
@@ -598,31 +724,6 @@ const stopDrag = () => {
   document.removeEventListener('mouseup', stopDrag);
 };
 
-// 坐标转换函数:根据背景图片实际尺寸调整坐标
-const convertCoordinatesWithScale = (x: number, y: number): { x: number; y: number } => {
-  // 获取预览容器尺寸
-  const container = previewContainer.value;
-  const previewWidth = container?.clientWidth || container?.offsetWidth || PREVIEW_WIDTH;
-  const previewHeight = container?.clientHeight || container?.offsetHeight || PREVIEW_HEIGHT;
-
-  // 使用固定的实际输出尺寸(2948×2079px)
-  const actualWidth = ACTUAL_WIDTH;
-  const actualHeight = ACTUAL_HEIGHT;
-
-  // 计算实际比例
-  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) => {
@@ -745,6 +846,7 @@ const handleCreateTask = async () => {
       
       // 其他通用参数
       fontName: bibForm.fontName || 'simhei',
+      fontOssId: bibForm.fontOssId,
       fontSize: bibForm.fontSize || 36,
       fontColor: parseInt((bibForm.fontColor || '#000000').replace('#', ''), 16),
       eventName: bibForm.eventName || '',

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

@@ -181,164 +181,6 @@
     <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" />
-    </el-dialog> -->
-    <!-- 文章编写对话框 -->
-    <!-- <el-dialog v-model="articleDialog.visible" :title="articleDialog.title" width="1200px" append-to-body>
-      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
-        <el-tab-pane label="竞赛流程" name="competition-process">
-          <div class="article-form">
-            <el-form-item label="标题">
-              <el-input v-model="articleData.competitionProcess.title" placeholder="请输入标题" />
-            </el-form-item>
-            <el-form-item label="内容">
-              <Editor v-model="articleData.competitionProcess.content" :min-height="300" />
-            </el-form-item>
-            <el-form-item label="备注">
-              <el-input v-model="articleData.competitionProcess.remark" placeholder="请输入备注" type="textarea" :rows="3" />
-            </el-form-item>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="竞赛项目" name="competition-items">
-          <div class="article-form">
-            <el-form-item label="标题">
-              <el-input v-model="articleData.competitionItems.title" placeholder="请输入标题" />
-            </el-form-item>
-            <el-form-item label="内容">
-              <Editor v-model="articleData.competitionItems.content" :min-height="300" />
-            </el-form-item>
-            <el-form-item label="备注">
-              <el-input v-model="articleData.competitionItems.remark" placeholder="请输入备注" type="textarea" :rows="3" />
-            </el-form-item>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="活动议程" name="activity-agenda">
-          <div class="article-form">
-            <el-form-item label="标题">
-              <el-input v-model="articleData.activityAgenda.title" placeholder="请输入标题" />
-            </el-form-item>
-            <el-form-item label="内容">
-              <Editor v-model="articleData.activityAgenda.content" :min-height="300" />
-            </el-form-item>
-            <el-form-item label="备注">
-              <el-input v-model="articleData.activityAgenda.remark" placeholder="请输入备注" type="textarea" :rows="3" />
-            </el-form-item>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="项目介绍" name="project-introduction">
-          <div class="article-form">
-            <el-form-item label="标题">
-              <el-input v-model="articleData.projectIntroduction.title" placeholder="请输入标题" />
-            </el-form-item>
-            <el-form-item label="内容">
-              <Editor v-model="articleData.projectIntroduction.content" :min-height="300" />
-            </el-form-item>
-            <el-form-item label="备注">
-              <el-input v-model="articleData.projectIntroduction.remark" placeholder="请输入备注" type="textarea" :rows="3" />
-            </el-form-item>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="竞赛流程" name="competition-flow">
-          <div class="article-form">
-            <el-form-item label="标题">
-              <el-input v-model="articleData.competitionFlow.title" placeholder="请输入标题" />
-            </el-form-item>
-            <el-form-item label="内容">
-              <Editor v-model="articleData.competitionFlow.content" :min-height="300" />
-            </el-form-item>
-            <el-form-item label="备注">
-              <el-input v-model="articleData.competitionFlow.remark" placeholder="请输入备注" type="textarea" :rows="3" />
-            </el-form-item>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="赛事分组" name="event-grouping">
-          <div class="article-form">
-            <el-form-item label="标题">
-              <el-input v-model="articleData.eventGrouping.title" placeholder="请输入标题" />
-            </el-form-item>
-            <el-form-item label="内容">
-              <Editor v-model="articleData.eventGrouping.content" :min-height="300" />
-            </el-form-item>
-            <el-form-item label="备注">
-              <el-input v-model="articleData.eventGrouping.remark" placeholder="请输入备注" type="textarea" :rows="3" />
-            </el-form-item>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="运动员号码簿" name="athlete-handbook">
-          <div class="article-form">
-            <el-form-item label="标题">
-              <el-input v-model="articleData.athleteHandbook.title" placeholder="请输入标题" />
-            </el-form-item>
-            <el-form-item label="内容">
-              <Editor v-model="articleData.athleteHandbook.content" :min-height="300" />
-            </el-form-item>
-            <el-form-item label="备注">
-              <el-input v-model="articleData.athleteHandbook.remark" placeholder="请输入备注" type="textarea" :rows="3" />
-            </el-form-item>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="项目场地" name="project-venue">
-          <div class="article-form">
-            <el-form-item label="标题">
-              <el-input v-model="articleData.projectVenue.title" placeholder="请输入标题" />
-            </el-form-item>
-            <el-form-item label="内容">
-              <Editor v-model="articleData.projectVenue.content" :min-height="300" />
-            </el-form-item>
-            <el-form-item label="备注">
-              <el-input v-model="articleData.projectVenue.remark" placeholder="请输入备注" type="textarea" :rows="3" />
-            </el-form-item>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="交通指示" name="traffic-guide">
-          <div class="article-form">
-            <el-form-item label="标题">
-              <el-input v-model="articleData.trafficGuide.title" placeholder="请输入标题" />
-            </el-form-item>
-            <el-form-item label="内容">
-              <Editor v-model="articleData.trafficGuide.content" :min-height="300" />
-            </el-form-item>
-            <el-form-item label="备注">
-              <el-input v-model="articleData.trafficGuide.remark" placeholder="请输入备注" type="textarea" :rows="3" />
-            </el-form-item>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="快捷报名" name="quick-registration">
-          <div class="article-form">
-            <el-form-item label="标题">
-              <el-input v-model="articleData.quickRegistration.title" placeholder="请输入标题" />
-            </el-form-item>
-            <el-form-item label="内容">
-              <Editor v-model="articleData.quickRegistration.content" :min-height="300" />
-            </el-form-item>
-            <el-form-item label="备注">
-              <el-input v-model="articleData.quickRegistration.remark" placeholder="请输入备注" type="textarea" :rows="3" />
-            </el-form-item>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="报名咨询" name="registration-consultation">
-          <div class="article-form">
-            <el-form-item label="标题">
-              <el-input v-model="articleData.registrationConsultation.title" placeholder="请输入标题" />
-            </el-form-item>
-            <el-form-item label="内容">
-              <Editor v-model="articleData.registrationConsultation.content" :min-height="300" />
-            </el-form-item>
-            <el-form-item label="备注">
-              <el-input v-model="articleData.registrationConsultation.remark" placeholder="请输入备注" type="textarea" :rows="3" />
-            </el-form-item>
-          </div>
-        </el-tab-pane>
-      </el-tabs>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="handleCloseArticleDialog">取 消</el-button>
-          <el-button type="primary" @click="handleSaveArticle">保 存</el-button>
-        </div>
-      </template>
-    </el-dialog> -->
 
     <!-- 用户导入对话框 -->
     <el-dialog v-model="upload.open" :title="upload.title" width="400px" append-to-body>

+ 114 - 1
src/views/system/gameScore/gameScoreEdit.vue

@@ -24,6 +24,9 @@
       <el-button type="warning" @click="handleExport">
         <el-icon><Download /></el-icon> 导出
       </el-button>
+      <el-button type="info" plain @click="handleImport">
+        <el-icon><Upload /></el-icon> 导入
+      </el-button>
       <el-input 
         v-model="searchValue" 
         :placeholder="projectClassification === '0' ? '输入运动员姓名搜索' : '输入队伍名称搜索'" 
@@ -229,13 +232,49 @@
         </div>
       </template>
     </el-dialog>
+
+    <!-- 成绩导入对话框 -->
+    <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
+      <el-upload
+        ref="uploadRef"
+        :limit="1"
+        accept=".xlsx, .xls"
+        :headers="upload.headers"
+        :action="upload.url"
+        :data="upload.data"
+        :disabled="upload.isUploading"
+        :on-progress="handleFileUploadProgress"
+        :on-success="handleFileSuccess"
+        :auto-upload="false"
+        drag
+      >
+        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+        <template #tip>
+          <div class="text-center el-upload__tip">
+            <span>仅允许导入xls、xlsx格式文件。</span>
+            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplateFile">下载模板 </el-link>
+          </div>
+        </template>
+      </el-upload>
+      <div style="margin-top: 10px; text-align: center;">
+        <el-checkbox v-model="upload.updateSupport" :true-label="1" :false-label="0"> 存在是否覆盖成绩</el-checkbox>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitFileForm">确 定</el-button>
+          <el-button @click="upload.open = false">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="GameScoreEdit" lang="ts">
 import { onMounted, onUnmounted, ref, reactive, getCurrentInstance, toRefs } from 'vue';
 import { useRoute } from 'vue-router';
-import { getProjectScoreData, updateScoreAndRecalculate, exportProjectScore } from '@/api/system/gameScore/index';
+import { getProjectScoreData, updateScoreAndRecalculate, exportProjectScore, importTemplate } from '@/api/system/gameScore/index';
+import { globalHeaders } from '@/utils/request';
 import { getGameEventProject } from '@/api/system/gameEventProject';
 import { listScoreDetail } from '@/api/system/scoreDetail';
 import { GameScoreForm } from '@/api/system/gameScore/types';
@@ -264,6 +303,26 @@ const exportDialog = reactive({
   topN: undefined as number | undefined
 });
 
+/*** 成绩导入参数 */
+const upload = reactive<ImportOption>({
+  // 是否显示弹出层(成绩导入)
+  open: false,
+  // 弹出层标题(成绩导入)
+  title: '',
+  // 是否禁用上传
+  isUploading: false,
+  // 设置上传的请求头部
+  headers: globalHeaders(),
+  // 上传的地址
+  url: import.meta.env.VITE_APP_BASE_API + '/system/gameScore/import',
+  eventId: '',
+  projectId: '',
+  classification: '',
+  updateSupport: 1,
+  data: {}
+});
+const uploadRef = ref<ElUploadInstance>();
+
 // 统计数据
 const stats = reactive({
   registrationCount: 0,
@@ -572,6 +631,60 @@ const doExport = async () => {
   }
 };
 
+/** 导入按钮操作 */
+const handleImport = () => {
+  upload.title = '成绩导入';
+  upload.eventId = eventId;
+  upload.projectId = projectId;
+  upload.classification = projectClassification;
+  upload.updateSupport = 1;
+  upload.data = {
+    eventId: eventId,
+    projectId: projectId,
+    classification: projectClassification,
+    updateSupport: upload.updateSupport
+  };
+  upload.open = true;
+};
+
+/** 下载模板操作 */
+const importTemplateFile = async () => {
+  try {
+    const response = await importTemplate(eventId, projectId, projectClassification);
+    const fileName = `成绩导入模板_${projectName}.xlsx`;
+    const blob = new Blob([response as any]);
+    const link = document.createElement('a');
+    link.href = window.URL.createObjectURL(blob);
+    link.download = fileName;
+    link.click();
+    window.URL.revokeObjectURL(link.href);
+  } catch (error) {
+    proxy?.$modal.msgError("下载模板失败");
+  }
+};
+
+/**文件上传中处理 */
+const handleFileUploadProgress = () => {
+  upload.isUploading = true;
+};
+
+/** 文件上传成功处理 */
+const handleFileSuccess = (response: any, file: any) => {
+  upload.open = false;
+  upload.isUploading = false;
+  uploadRef.value?.handleRemove(file);
+  ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", {
+    dangerouslyUseHTMLString: true
+  });
+  loadData(false);
+};
+
+/** 提交上传文件 */
+function submitFileForm() {
+  upload.data.updateSupport = upload.updateSupport;
+  uploadRef.value?.submit();
+}
+
 // 加载数据方法,支持搜索
 const loadData = async (autoCalculateRanking = false) => {
   loading.value = true;

+ 5 - 1
src/views/system/gameTeam/index.vue

@@ -167,6 +167,9 @@
         <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
         <template #tip>
           <div class="text-center el-upload__tip">
+            <div class="el-upload__tip">
+              <el-checkbox v-model="upload.updateSupport" :true-label="1" :false-label="0" /> 是否更新已经存在的参赛队伍数据
+            </div>
             <span>仅允许导入xls、xlsx格式文件。</span>
             <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板 </el-link>
           </div>
@@ -238,7 +241,7 @@ const upload = reactive<ImportOption>({
   open: false,
   title: '',
   isUploading: false,
-  updateSupport: 0,
+  updateSupport: 1,
   headers: globalHeaders(),
   url: import.meta.env.VITE_APP_BASE_API + '/system/gameTeam/import'
 });
@@ -451,6 +454,7 @@ const handleExport = () => {
 /** 导入按钮操作 */
 const handleImport = () => {
   upload.title = '参赛队伍导入';
+  upload.updateSupport = 1;
   upload.open = true;
 };