Explorar o código

feat: 新增还原赛事
perf:优化用户建议、预热活动关联默认赛事、导航菜单

wenkai hai 1 semana
pai
achega
763af6feb5

+ 316 - 8
src/views/index.vue

@@ -204,13 +204,87 @@
         </el-card>
       </el-col>
     </el-row>
+
+    <!-- 还原赛事弹窗 -->
+    <el-dialog v-model="restoreDialogVisible" title="还原赛事" width="500px" :before-close="handleCloseRestoreDialog">
+      <div class="restore-dialog-content">
+        <div class="restore-description">
+          <el-icon class="description-icon">
+            <InfoFilled />
+          </el-icon>
+          <p>请选择您需要的操作:</p>
+        </div>
+
+        <div class="restore-actions">
+          <div class="action-card backup-card" @click="handleBackup">
+            <div class="action-icon">
+              <el-icon :size="32">
+                <FolderAdd />
+              </el-icon>
+            </div>
+            <div class="action-content">
+              <h3>备份数据库</h3>
+              <p>将当前数据库备份为SQL文件下载到本地</p>
+            </div>
+            <el-button v-if="backupLoading" type="primary" loading size="small"> 备份中...</el-button>
+          </div>
+
+          <div class="action-card restore-card">
+            <div class="action-icon">
+              <el-icon :size="32">
+                <RefreshLeft />
+              </el-icon>
+            </div>
+            <div class="action-content">
+              <h3>恢复数据库</h3>
+              <p>从SQL备份文件中恢复数据库数据</p>
+            </div>
+            <div class="restore-upload">
+              <el-upload ref="uploadRef" :show-file-list="false" :before-upload="beforeUpload" :http-request="handleFileUpload" accept=".sql" drag>
+                <el-icon class="el-icon--upload">
+                  <UploadFilled />
+                </el-icon>
+                <div class="el-upload__text">将SQL文件拖到此处,或<em>点击上传</em></div>
+                <template #tip>
+                  <div class="el-upload__tip">只能上传.sql文件,且不超过100MB</div>
+                </template>
+              </el-upload>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="handleCloseRestoreDialog">取消</el-button>
+        </span>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="Index" lang="ts">
 import { ref, nextTick, onMounted } from 'vue';
 import { ElMessage } from 'element-plus';
-import { TrendCharts, PieChart, Monitor, Operation, Calendar, Bell, Trophy, Clock, VideoPlay, CircleCheck, DataBoard } from '@element-plus/icons-vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+import {
+  TrendCharts,
+  PieChart,
+  Monitor,
+  Operation,
+  Calendar,
+  Bell,
+  Trophy,
+  Clock,
+  VideoPlay,
+  CircleCheck,
+  DataBoard,
+  InfoFilled,
+  FolderAdd,
+  RefreshLeft,
+  UploadFilled
+} from '@element-plus/icons-vue';
 import * as echarts from 'echarts';
 import { getArticleCount } from '@/api/system/article';
 import { getEventCount, listGameEvent } from '@/api/system/gameEvent';
@@ -219,6 +293,8 @@ import { getRefereeCount } from '@/api/system/gameReferee';
 import { getTeamCount } from '@/api/system/gameTeam';
 import { listNotice } from '@/api/system/notice';
 import { countOnlineUser } from '@/api/monitor/online';
+import { backupDatabase, restoreDatabase } from '@/api/system/backup';
+import { useRouter } from 'vue-router';
 
 // 响应式数据
 const statsData = ref([
@@ -258,15 +334,19 @@ const statsData = ref([
     loading: true
   }
 ]);
+
 const router = useRouter();
 const onlineUsers = ref(1);
+const restoreDialogVisible = ref(false);
+const backupLoading = ref(false);
+const uploadRef = ref();
 const lastUpdateTime = ref('');
 
 const quickActions = ref([
   { name: '新增赛事', type: 'primary' as const, icon: 'Plus', action: 'addEvent' },
   { name: '参赛队伍管理', type: 'success' as const, icon: 'User', action: 'manageTeam' },
   { name: '裁判管理', type: 'warning' as const, icon: 'Flag', action: 'manageReferees' },
-  { name: '数据导出', type: 'info' as const, icon: 'Download', action: 'exportData' },
+  { name: '还原赛事', type: 'info' as const, icon: 'RefreshLeft', action: 'restoreEvent' },
   { name: '刷新数据', type: 'default' as const, icon: 'Refresh', action: 'refresh' }
 ]);
 
@@ -496,12 +576,9 @@ const handleQuickAction = (action: string) => {
       // 跳转到裁判管理页面
       router.push(`/game/gameReferee`);
       break;
-    case 'exportData':
-      // 导出数据
-      ElMessage({
-        message: '请到具体的管理页面进行导出',
-        type: 'info'
-      });
+    case 'restoreEvent':
+      // 显示还原赛事弹窗
+      restoreDialogVisible.value = true;
       break;
     case 'refresh':
       // 刷新统计数据和最新赛事
@@ -532,6 +609,99 @@ const getAllData = async () => {
   getOnlineUser();
 };
 
+// 弹窗相关方法
+const handleCloseRestoreDialog = () => {
+  restoreDialogVisible.value = false;
+  backupLoading.value = false;
+};
+
+// 备份数据库
+const handleBackup = async () => {
+  backupLoading.value = true;
+  try {
+    await proxy?.download('system/backup/backup', {}, `db_backup_${new Date().getTime()}.sql`);
+    ElMessage({
+      message: '数据库备份下载成功',
+      type: 'success'
+    });
+
+    restoreDialogVisible.value = false;
+  } catch (error) {
+    console.error('备份失败:', error);
+    ElMessage({
+      message: '数据库备份失败,请稍后重试',
+      type: 'error'
+    });
+  } finally {
+    backupLoading.value = false;
+  }
+};
+
+// 文件上传前的验证
+const beforeUpload = (file: File) => {
+  const isSQL = file.name.toLowerCase().endsWith('.sql');
+  const isLt100M = file.size / 1024 / 1024 < 100;
+
+  if (!isSQL) {
+    ElMessage({
+      message: '只能上传.sql格式的文件',
+      type: 'error'
+    });
+    return false;
+  }
+  if (!isLt100M) {
+    ElMessage({
+      message: '上传文件大小不能超过100MB',
+      type: 'error'
+    });
+    return false;
+  }
+  return true;
+};
+
+// 自定义文件上传处理
+const handleFileUpload = async (options: any) => {
+  const { file } = options;
+
+  try {
+    ElMessage({
+      message: '正在恢复数据库,请稍等...',
+      type: 'info',
+      duration: 0
+    });
+
+    const response = await restoreDatabase(file);
+
+    // 关闭加载消息
+    ElMessage.closeAll();
+
+    if (response.code === 200) {
+      ElMessage({
+        message: '数据库恢复成功',
+        type: 'success'
+      });
+      restoreDialogVisible.value = false;
+
+      // 刷新页面数据
+      setTimeout(() => {
+        getAllData();
+      }, 1000);
+    } else {
+      ElMessage({
+        message: response.msg || '数据库恢复失败',
+        type: 'error'
+      });
+    }
+  } catch (error) {
+    ElMessage.closeAll();
+    console.error('恢复失败:', error);
+    ElMessage({
+      message: '数据库恢复失败,请检查文件格式或稍后重试',
+      type: 'error'
+    });
+  }
+};
+
 onMounted(() => {
   getAllData();
   updateTime();
@@ -822,4 +992,142 @@ onMounted(() => {
     color: #909399;
   }
 }
+
+// 还原赛事弹窗样式
+.restore-dialog-content {
+  .restore-description {
+    display: flex;
+    align-items: center;
+    margin-bottom: 24px;
+    padding: 16px;
+    background: #f8f9fa;
+    border-radius: 8px;
+
+    .description-icon {
+      color: #409eff;
+      margin-right: 12px;
+      font-size: 20px;
+    }
+
+    p {
+      margin: 0;
+      color: #606266;
+      font-size: 14px;
+    }
+  }
+
+  .restore-actions {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+  }
+
+  .action-card {
+    border: 2px solid #e4e7ed;
+    border-radius: 12px;
+    padding: 20px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    position: relative;
+
+    &:hover {
+      border-color: #409eff;
+      box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
+    }
+
+    &.backup-card {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      .action-icon {
+        color: #67c23a;
+        margin-right: 16px;
+      }
+
+      .action-content {
+        flex: 1;
+
+        h3 {
+          margin: 0 0 8px 0;
+          color: #303133;
+          font-size: 16px;
+          font-weight: 600;
+        }
+
+        p {
+          margin: 0;
+          color: #909399;
+          font-size: 14px;
+        }
+      }
+    }
+
+    &.restore-card {
+      .action-icon {
+        color: #e6a23c;
+        margin-bottom: 16px;
+      }
+
+      .action-content {
+        margin-bottom: 20px;
+
+        h3 {
+          margin: 0 0 8px 0;
+          color: #303133;
+          font-size: 16px;
+          font-weight: 600;
+        }
+
+        p {
+          margin: 0;
+          color: #909399;
+          font-size: 14px;
+        }
+      }
+
+      .restore-upload {
+        :deep(.el-upload) {
+          width: 100%;
+        }
+
+        :deep(.el-upload-dragger) {
+          width: 100%;
+          height: 120px;
+          border: 2px dashed #d9d9d9;
+          border-radius: 8px;
+          background: #fafafa;
+          transition: all 0.3s ease;
+
+          &:hover {
+            border-color: #409eff;
+            background: #f0f8ff;
+          }
+        }
+
+        :deep(.el-icon--upload) {
+          font-size: 32px;
+          color: #c0c4cc;
+          margin-bottom: 8px;
+        }
+
+        :deep(.el-upload__text) {
+          color: #606266;
+          font-size: 14px;
+
+          em {
+            color: #409eff;
+            font-style: normal;
+          }
+        }
+
+        :deep(.el-upload__tip) {
+          margin-top: 8px;
+          color: #909399;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+}
 </style>

+ 0 - 8
src/views/system/activityIntroduction/index.vue

@@ -4,9 +4,6 @@
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <!--            <el-form-item label="赛事id" prop="eventId">-->
-            <!--              <el-input v-model="queryParams.eventId" placeholder="请输入赛事id" clearable @keyup.enter="handleQuery" />-->
-            <!--            </el-form-item>-->
             <el-form-item label="标题" prop="title">
               <el-input v-model="queryParams.title" placeholder="请输入标题" clearable @keyup.enter="handleQuery" />
             </el-form-item>
@@ -54,8 +51,6 @@
 
       <el-table v-loading="loading" border :data="articleList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="" align="center" prop="id" v-if="true" />
-        <el-table-column label="赛事id" align="center" prop="eventId" />
         <el-table-column label="标题" align="center" prop="title" />
         <!--        <el-table-column label="内容" align="center" prop="content" />-->
         <el-table-column label="文章类型" align="center" prop="type">
@@ -90,9 +85,6 @@
     <!-- 添加或修改文章对话框 -->
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="1200px" append-to-body>
       <el-form ref="articleFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="赛事id" prop="eventId">
-          <el-input v-model="form.eventId" placeholder="请输入赛事id" />
-        </el-form-item>
         <el-form-item label="标题" prop="title">
           <el-input v-model="form.title" placeholder="请输入标题" />
         </el-form-item>

+ 71 - 85
src/views/system/common/nav/components/GameNavigator.vue

@@ -24,16 +24,6 @@
             <el-option label="停用" :value="1" />
           </el-select>
         </el-form-item>
-        <el-form-item label="创建时间" prop="createTime">
-          <el-date-picker
-            v-model="dateRange"
-            type="daterange"
-            range-separator="-"
-            start-placeholder="开始时间"
-            end-placeholder="结束时间"
-            style="width: 240px"
-          />
-        </el-form-item>
         <el-form-item>
           <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
           <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -87,7 +77,14 @@
             <img :src="scope.row.pic" style="width: 50px; height: 50px" />
           </template>
         </el-table-column>
-        <el-table-column label="颜色" align="center" prop="color" width="150" v-if="columns[4].visible" />
+        <el-table-column label="颜色" align="center" prop="color" width="150" v-if="columns[4].visible">
+          <template #default="scope">
+            <div
+              v-if="scope.row.color"
+              :style="{ backgroundColor: scope.row.color, width: '30px', height: '20px', borderRadius: '4px', margin: '0 auto' }"
+            ></div>
+          </template>
+        </el-table-column>
         <el-table-column label="链接类别" align="center" prop="jumpType" width="150" v-if="columns[5].visible">
           <template #default="scope">
             {{ getJumpTypeText(scope.row.jumpType) }}
@@ -186,19 +183,8 @@
           <el-col :span="12">
             <el-form-item label="颜色" required>
               <div class="color-input-group">
-                <el-color-picker 
-                  v-model="form.color" 
-                  size="large"
-                  class="color-picker"
-                  @change="handleColorChange"
-                />
-                <el-input 
-                  v-model="colorInput" 
-                  placeholder="#RRGGBB"
-                  class="color-input"
-                  @input="handleColorInput"
-                  @blur="validateColorInput"
-                />
+                <el-color-picker v-model="form.color" size="large" class="color-picker" @change="handleColorChange" />
+                <el-input v-model="colorInput" placeholder="#RRGGBB" class="color-input" @input="handleColorInput" @blur="validateColorInput" />
               </div>
             </el-form-item>
           </el-col>
@@ -398,7 +384,7 @@ interface NavItem extends GameNavigatorVo {
 // 表单数据
 const navItems = ref<NavItem[]>([]);
 const formRef = ref();
-const queryRef = ref()
+const queryRef = ref();
 const form = reactive<
   GameNavigatorBo & {
     createTime?: string;
@@ -487,15 +473,15 @@ function formatDateTime(date: string | Date | null | undefined): string {
     hour: '2-digit',
     minute: '2-digit',
     second: '2-digit'
-  })
+  });
 }
 
 // 格式化日期为查询格式 (YYYY-MM-DD)
 function formatDateForQuery(date: Date): string {
-  const year = date.getFullYear()
-  const month = String(date.getMonth() + 1).padStart(2, '0')
-  const day = String(date.getDate()).padStart(2, '0')
-  return `${year}-${month}-${day}`
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, '0');
+  const day = String(date.getDate()).padStart(2, '0');
+  return `${year}-${month}-${day}`;
 }
 
 // 获取跳转类型文本
@@ -532,26 +518,26 @@ async function loadNavData() {
       pageNum: 1,
       pageSize: 100, // 设置一个较大的值以获取所有数据
       ...queryParams
-    }
-    
+    };
+
     // 处理日期范围
     if (dateRange.value && dateRange.value.length === 2) {
-      query.beginTime = formatDateForQuery(dateRange.value[0])
-      query.endTime = formatDateForQuery(dateRange.value[1])
+      query.beginTime = formatDateForQuery(dateRange.value[0]);
+      query.endTime = formatDateForQuery(dateRange.value[1]);
     }
-    
-    const response = await listNavigator(query)
+
+    const response = await listNavigator(query);
     if (response.code === 200 && (response as any).rows) {
-      navItems.value = (response as any).rows || []
-      total.value = (response as any).total || 0
+      navItems.value = (response as any).rows || [];
+      total.value = (response as any).total || 0;
     } else {
-      navItems.value = []
-      total.value = 0
+      navItems.value = [];
+      total.value = 0;
     }
   } catch (error) {
-    console.error('加载数据失败:', error)
-    navItems.value = []
-    total.value = 0
+    console.error('加载数据失败:', error);
+    navItems.value = [];
+    total.value = 0;
   } finally {
     loading.value = false;
     // 数据加载完成后,更新排序值
@@ -642,8 +628,8 @@ function handleView(row: NavItem) {
 
 // 处理编辑
 function handleEdit(item: NavItem) {
-  reset()
-  const normalizedColor = normalizeColor(item.color || '#409EFF')
+  reset();
+  const normalizedColor = normalizeColor(item.color || '#409EFF');
   const formData = {
     navId: item.navId,
     name: item.name,
@@ -658,23 +644,23 @@ function handleEdit(item: NavItem) {
     remark: item.remark || '',
     createTime: item.createTime,
     updateTime: item.updateTime
-  }
-  Object.assign(form, formData)
-  colorInput.value = normalizedColor
-  dialogVisible.value = true
-  dialogTitle.value = '修改菜单'
+  };
+  Object.assign(form, formData);
+  colorInput.value = normalizedColor;
+  dialogVisible.value = true;
+  dialogTitle.value = '修改菜单';
 }
 
 // 处理添加
 function handleAdd() {
   reset();
   // 设置排序值为当前最大值+1
-  form.sortNum = nextSortNum.value
+  form.sortNum = nextSortNum.value;
   // 确保颜色值为16进制格式
-  form.color = normalizeColor(form.color)
-  colorInput.value = form.color
-  dialogVisible.value = true
-  dialogTitle.value = '添加菜单'
+  form.color = normalizeColor(form.color);
+  colorInput.value = form.color;
+  dialogVisible.value = true;
+  dialogTitle.value = '添加菜单';
 }
 
 // 处理删除
@@ -747,7 +733,7 @@ async function handleSubmit() {
 
 // 重置表单
 function reset() {
-  const defaultColor = '#409EFF'
+  const defaultColor = '#409EFF';
   Object.assign(form, {
     navId: undefined,
     name: '',
@@ -762,9 +748,9 @@ function reset() {
     remark: '',
     createTime: '',
     updateTime: ''
-  })
-  colorInput.value = defaultColor
-  formRef.value?.clearValidate()
+  });
+  colorInput.value = defaultColor;
+  formRef.value?.clearValidate();
 }
 
 // 跳转类型变更处理
@@ -822,30 +808,30 @@ const rgbToHex = (rgb: string): string => {
   }
 
   // 如果无法解析,返回默认值
-  return '#409EFF'
-}
+  return '#409EFF';
+};
 
 // 将任意颜色格式统一转换为16进制
 const normalizeColor = (color: string): string => {
-  if (!color) return '#409EFF'
-  
+  if (!color) return '#409EFF';
+
   // 如果已经是16进制格式,直接返回
   if (color.startsWith('#')) {
-    return color
+    return color;
   }
-  
+
   // 如果是RGB格式,转换为16进制
-  const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/)
+  const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
   if (rgbMatch) {
-    const r = parseInt(rgbMatch[1])
-    const g = parseInt(rgbMatch[2])
-    const b = parseInt(rgbMatch[3])
-    return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
+    const r = parseInt(rgbMatch[1]);
+    const g = parseInt(rgbMatch[2]);
+    const b = parseInt(rgbMatch[3]);
+    return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
   }
-  
+
   // 尝试解析其他格式或返回默认值
-  return '#409EFF'
-}
+  return '#409EFF';
+};
 
 // 处理颜色输入框变化
 const handleColorInput = (value: string) => {
@@ -859,9 +845,9 @@ const handleColorInput = (value: string) => {
 const handleColorChange = (value: string) => {
   // 当颜色选择器改变时,将颜色统一转换为16进制格式
   if (value) {
-    const hexValue = normalizeColor(value)
-    form.color = hexValue
-    colorInput.value = hexValue
+    const hexValue = normalizeColor(value);
+    form.color = hexValue;
+    colorInput.value = hexValue;
   }
 };
 
@@ -869,24 +855,24 @@ const handleColorChange = (value: string) => {
 const validateColorInput = () => {
   const hexRegex = /^#[0-9A-Fa-f]{6}$/;
   if (!hexRegex.test(colorInput.value)) {
-    ElMessage.warning('请输入正确的16进制颜色格式,如 #FF0000')
-    colorInput.value = form.color
+    ElMessage.warning('请输入正确的16进制颜色格式,如 #FF0000');
+    colorInput.value = form.color;
   } else {
     // 验证通过,确保表单中的颜色值也是16进制格式
-    form.color = colorInput.value
+    form.color = colorInput.value;
   }
 };
 
 // 初始化数据
 onMounted(() => {
   // 初始化排序值
-  form.sortNum = nextSortNum.value
+  form.sortNum = nextSortNum.value;
   // 确保初始颜色值为16进制格式
-  form.color = normalizeColor(form.color)
-  colorInput.value = form.color
+  form.color = normalizeColor(form.color);
+  colorInput.value = form.color;
   // 加载初始数据
-  loadNavData()
-})
+  loadNavData();
+});
 </script>
 
 <style scoped>
@@ -1099,4 +1085,4 @@ onMounted(() => {
   flex: 1;
   min-width: 120px;
 }
-</style> 
+</style>

+ 17 - 21
src/views/system/gameEvent/edit.vue

@@ -81,13 +81,6 @@
             </el-row>
 
             <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="是否默认赛事" prop="isDefault">
-                  <el-radio-group v-model="basicForm.isDefault">
-                    <el-radio v-for="dict in sys_yes_no" :key="dict.value" :value="dict.value">{{ dict.label }} </el-radio>
-                  </el-radio-group>
-                </el-form-item>
-              </el-col>
               <el-col :span="12">
                 <el-form-item label="状态" prop="status">
                   <el-radio-group v-model="basicForm.status">
@@ -250,7 +243,7 @@
                   <image-or-url-input v-model="scope.row.configValue" />
                 </template>
               </el-table-column>
-              
+
               <!-- <el-table-column label="是否启用" prop="isEnabled">
                 <template #default="scope">
                   <el-switch v-model="scope.row.isEnabled" />
@@ -385,8 +378,6 @@ const loadEventData = async (eventId: string | number) => {
 const handleStatusChange = async (row: GameEventVO) => {
   const text = row.isDefault === '0' ? '启用' : '停用';
   try {
-    await proxy?.$modal.confirm('确认要"' + text + '""' + row.eventName + '"为默认赛事吗?');
-    await changeEventDefault(row.eventId, row.isDefault);
     await loadEventData(row.eventId);
     proxy?.$modal.msgSuccess(text + '成功');
   } catch {
@@ -406,10 +397,20 @@ const menuSearchKeyword = ref('');
 
 // 过滤可用菜单的计算属性
 const filteredAvailableMenus = computed(() => {
-  if (!menuSearchKeyword.value) {
-    return availableMenus.value;
+  // 首先过滤掉已添加的菜单
+  let filteredMenus = availableMenus.value.filter((menu) => {
+    // 检查是否已存在相同的菜单(根据navId比较)
+    return !menuItems.value.some((item) => item.navId === Number(menu.navId));
+  });
+
+  // 然后根据搜索关键词进一步过滤
+  if (menuSearchKeyword.value) {
+    filteredMenus = filteredMenus.filter((menu) =>
+      menu.name?.toLowerCase().includes(menuSearchKeyword.value.toLowerCase())
+    );
   }
-  return availableMenus.value.filter((menu) => menu.name?.toLowerCase().includes(menuSearchKeyword.value.toLowerCase()));
+
+  return filteredMenus;
 });
 
 // 加载菜单数据
@@ -455,14 +456,9 @@ const confirmAddMenuItems = () => {
     return;
   }
 
-  // 将选中的菜单添加到菜单列表中
-  selectedMenus.value.forEach((menu) => {
-    // 检查是否已存在相同的菜单(根据menu的navId和当前menuItems的icon字段比较)
-    const exists = menuItems.value.some((item) => item.navId === Number(menu.navId));
-    if (!exists) {
-      menuItems.value.push(menu);
-    }
-  });
+  // 将选中的菜单添加到菜单列表中(无需重复检查,因为列表已经过滤掉了重复项)
+  menuItems.value.push(...selectedMenus.value);
+
   console.log('menuItems:', menuItems.value);
   menuSelectDialogVisible.value = false;
   proxy?.$modal.msgSuccess(`成功添加 ${selectedMenus.value.length} 个菜单项`);

+ 148 - 111
src/views/system/gameEvent/index.vue

@@ -61,91 +61,95 @@
       </template>
 
       <el-table v-loading="loading" border :data="gameEventList" @selection-change="handleSelectionChange">
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="280">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="left" width="220">
           <template #default="scope">
             <div class="operation-buttons">
-              <el-tooltip content="修改" placement="top">
-                <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:gameEvent:edit']">
-                  <span class="button-text">修改</span>
-                </el-button>
-              </el-tooltip>
-              <!-- 删除操作 -->
-              <el-tooltip :content="getDeleteTooltip(scope.row)" placement="top">
-                <el-button
-                  link
-                  type="danger"
-                  icon="Delete"
-                  :disabled="!canDelete(scope.row)"
-                  @click="handleDelete(scope.row)"
-                  v-hasPermi="['system:event:remove']"
-                >
-                  <span class="button-text">删除</span>
-                </el-button>
-              </el-tooltip>
-              <!-- 下载模板 -->
-              <el-tooltip content="下载报名信息模板" placement="top">
-                <el-button link type="warning" icon="Download" @click="handleDownloadTemplate(scope.row)">
-                  <span class="button-text">下载</span>
-                </el-button>
-              </el-tooltip>
-              <!-- 导入报名信息 -->
-              <el-tooltip content="导入报名信息" placement="top">
-                <el-button
-                  link
-                  type="info"
-                  icon="FolderOpened"
-                  @click="handleImportRegistration(scope.row)"
-                  v-hasPermi="['system:gameEvent:import']"
-                >
-                  <span class="button-text">导入</span>
-                </el-button>
-              </el-tooltip>
-              <!-- 添加参赛者 -->
-              <el-tooltip content="添加参赛者" placement="top">
-                <el-button
-                  link
-                  type="success"
-                  icon="User"
-                  @click="handleAddParticipant(scope.row)"
-                  v-hasPermi="['system:gameEvent:addParticipant']"
-                >
-                  <span class="button-text">参赛者</span>
-                </el-button>
-              </el-tooltip>
-              <!-- 添加裁判 -->
-              <el-tooltip content="添加裁判" placement="top">
-                <el-button
-                  link
-                  type="primary"
-                  icon="Avatar"
-                  @click="handleAddReferee(scope.row)"
-                  v-hasPermi="['system:gameEvent:addReferee']"
-                >
-                  <span class="button-text">裁判</span>
-                </el-button>
-              </el-tooltip>
-              <!-- 预览按钮 -->
-              <el-tooltip content="预览" placement="top">
-                <el-button link type="info" icon="View" @click="handlePreview(scope.row)" v-hasPermi="['system:gameEvent:view']">
-                  <span class="button-text">预览</span>
-                </el-button>
-              </el-tooltip>
-              <!-- 比赛数据按钮 -->
-              <el-tooltip content="比赛排行榜" placement="top">
-                <el-button
-                  link
-                  type="warning"
-                  icon="DataAnalysis"
-                  @click="handleGameData(scope.row)"
-                  v-hasPermi="['system:gameEvent:gameData']"
-                >
-                  <span class="button-text">排行榜</span>
-                </el-button>
-              </el-tooltip>
+              <!-- 第一行:基本操作 -->
+              <div class="button-row">
+                <el-tooltip content="修改" placement="top">
+                  <el-button link type="primary" icon="Edit" size="small" @click="handleUpdate(scope.row)" v-hasPermi="['system:gameEvent:edit']"></el-button>
+                </el-tooltip>
+                <el-tooltip :content="getDeleteTooltip(scope.row)" placement="top">
+                  <el-button
+                    link
+                    type="danger"
+                    icon="Delete"
+                    size="small"
+                    :disabled="!canDelete(scope.row)"
+                    @click="handleDelete(scope.row)"
+                    v-hasPermi="['system:event:remove']"
+                  ></el-button>
+                </el-tooltip>
+                <el-tooltip content="预览" placement="top">
+                  <el-button link type="primary" icon="View" size="small" @click="handlePreview(scope.row)" v-hasPermi="['system:gameEvent:view']"></el-button>
+                </el-tooltip>
+                <el-tooltip content="比赛数据" placement="top">
+                  <el-button
+                    link
+                    type="success"
+                    icon="DataAnalysis"
+                    size="small"
+                    @click="handleGameData(scope.row)"
+                    v-hasPermi="['system:gameEvent:gameData']"
+                  ></el-button>
+                </el-tooltip>
+              </div>
+              
+              <!-- 第二行:管理操作 -->
+              <div class="button-row">
+                <el-tooltip content="下载模板" placement="top">
+                  <el-button link type="info" icon="Download" size="small" @click="handleDownloadTemplate(scope.row)"></el-button>
+                </el-tooltip>
+                <el-tooltip content="导入报名" placement="top">
+                  <el-button
+                    link
+                    type="warning"
+                    icon="FolderOpened"
+                    size="small"
+                    @click="handleImportRegistration(scope.row)"
+                    v-hasPermi="['system:gameEvent:import']"
+                  ></el-button>
+                </el-tooltip>
+                <el-tooltip content="添加参赛者" placement="top">
+                  <el-button
+                    link
+                    type="primary"
+                    icon="User"
+                    size="small"
+                    @click="handleAddParticipant(scope.row)"
+                    v-hasPermi="['system:gameEvent:addParticipant']"
+                  ></el-button>
+                </el-tooltip>
+                <el-tooltip content="添加裁判" placement="top">
+                  <el-button
+                    link
+                    type="primary"
+                    icon="Avatar"
+                    size="small"
+                    @click="handleAddReferee(scope.row)"
+                    v-hasPermi="['system:gameEvent:addReferee']"
+                  ></el-button>
+                </el-tooltip>
+              </div>
+              
+              <!-- 第三行:文章操作 -->
+              <div class="button-row">
+                <el-tooltip content="编写文章" placement="top">
+                  <el-button
+                    link
+                    type="warning"
+                    icon="EditPen"
+                    size="small"
+                    @click="handleWriteArticle(scope.row)"
+                    v-hasPermi="['system:gameEvent:writeArticle']"
+                  ></el-button>
+                </el-tooltip>
+              </div>
             </div>
           </template>
         </el-table-column>
-        <el-table-column type="selection" width="55" align="center" />
+
         <el-table-column label="主键" align="center" prop="eventId" v-if="columns[0].visible" />
         <el-table-column label="赛事编号" align="center" prop="eventCode" v-if="columns[1].visible" />
         <el-table-column label="赛事名称" align="center" prop="eventName" v-if="columns[2].visible" />
@@ -327,7 +331,7 @@
         </div>
       </template>
     </el-dialog>
-    
+
     <!-- 用户导入对话框 -->
     <el-dialog v-model="upload.open" :title="upload.title" width="400px" append-to-body>
       <el-upload
@@ -722,13 +726,13 @@ const activeTab = ref('competition-process');
 // 标签页与类型值的映射关系
 const tabTypeMapping: Record<string, number> = {
   'competition-process': 1, // 竞赛流程
-  'competition-items': 2,   // 竞赛项目
-  'activity-agenda': 3,     // 活动议程
+  'competition-items': 2, // 竞赛项目
+  'activity-agenda': 3, // 活动议程
   'project-introduction': 4, // 项目介绍
-  'competition-flow': 5,    // 竞赛流程
-  'event-grouping': 6,      // 赛事分组
-  'athlete-handbook': 7,    // 运动员号码簿
-  'project-venue': 8        // 项目场地
+  'competition-flow': 5, // 竞赛流程
+  'event-grouping': 6, // 赛事分组
+  'athlete-handbook': 7, // 运动员号码簿
+  'project-venue': 8 // 项目场地
 };
 
 const articleData = reactive({
@@ -774,7 +778,7 @@ const handleWriteArticle = async (row: GameEventVO) => {
   articleDialog.currentEventId = row.eventId;
   articleDialog.visible = true;
   activeTab.value = 'competition-process';
-  
+
   // 加载默认标签页(竞赛流程)的数据
   await loadTabData('competition-process');
 };
@@ -782,12 +786,12 @@ const handleWriteArticle = async (row: GameEventVO) => {
 /** 加载指定标签页的数据 */
 const loadTabData = async (tabName: string) => {
   const type = tabTypeMapping[tabName];
-  
+
   if (articleDialog.currentEventId && type) {
     try {
       const response = await getEventMdByEventAndType(articleDialog.currentEventId, type);
       const eventMd = response.data;
-      
+
       const dataKey = getDataKeyByTabName(tabName);
       if (dataKey && articleData[dataKey]) {
         if (eventMd) {
@@ -839,14 +843,14 @@ const getDataKeyByTabName = (tabName: string): keyof typeof articleData | null =
 /** 关闭文章编写对话框 */
 const handleCloseArticleDialog = () => {
   // 清空所有文章数据
-  Object.keys(articleData).forEach(key => {
+  Object.keys(articleData).forEach((key) => {
     const dataKey = key as keyof typeof articleData;
     articleData[dataKey].id = undefined;
     articleData[dataKey].title = '';
     articleData[dataKey].content = '';
     articleData[dataKey].remark = '';
   });
-  
+
   // 重置对话框状态
   articleDialog.visible = false;
   articleDialog.currentEventId = undefined;
@@ -863,14 +867,14 @@ const handleSaveArticle = async () => {
   const currentTabName = activeTab.value;
   const type = tabTypeMapping[currentTabName];
   const dataKey = getDataKeyByTabName(currentTabName);
-  
+
   if (!dataKey || !articleData[dataKey]) {
     proxy?.$modal.msgError('获取当前标签页数据失败');
     return;
   }
 
   const currentData = articleData[dataKey];
-  
+
   if (!currentData.title?.trim()) {
     proxy?.$modal.msgError('标题不能为空');
     return;
@@ -888,7 +892,7 @@ const handleSaveArticle = async () => {
 
     await editEventMd(formData);
     proxy?.$modal.msgSuccess('文章保存成功');
-    
+
     // 重新加载当前标签页数据以获取最新的ID
     await loadTabData(currentTabName);
   } catch (error) {
@@ -919,30 +923,44 @@ onActivated(() => {
 <style scoped>
 .operation-buttons {
   display: flex;
-  flex-wrap: wrap;
-  gap: 8px;
+  flex-direction: column;
+  gap: 4px;
+  align-items: center;
+  min-width: 200px;
+}
+
+.button-row {
+  display: flex;
+  gap: 6px;
   justify-content: center;
   align-items: center;
+  flex-wrap: wrap;
 }
 
 .operation-buttons .el-button {
+  min-width: 28px;
+  min-height: 28px;
+  padding: 4px 6px;
+  border-radius: 4px;
+  transition: all 0.2s ease;
   display: flex;
   align-items: center;
-  gap: 4px;
-  padding: 6px 12px;
-  border-radius: 6px;
-  transition: all 0.3s ease;
+  justify-content: center;
 }
 
 .operation-buttons .el-button:hover {
   transform: translateY(-1px);
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
 }
 
-.button-text {
-  font-size: 12px;
-  font-weight: 500;
-  white-space: nowrap;
+.operation-buttons .el-button[disabled] {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.operation-buttons .el-button[disabled]:hover {
+  transform: none;
+  box-shadow: none;
 }
 
 /* 为不同类型的按钮设置不同的颜色主题 */
@@ -967,17 +985,36 @@ onActivated(() => {
 }
 
 /* 响应式设计 */
-@media (max-width: 1200px) {
+@media (max-width: 1400px) {
   .operation-buttons {
-    gap: 4px;
+    min-width: 180px;
   }
   
+  .button-row {
+    gap: 4px;
+  }
+
   .operation-buttons .el-button {
-    padding: 4px 8px;
+    min-width: 24px;
+    min-height: 24px;
+    padding: 3px 5px;
+  }
+}
+
+@media (max-width: 1200px) {
+  .operation-buttons {
+    min-width: 160px;
+    gap: 2px;
   }
   
-  .button-text {
-    font-size: 11px;
+  .button-row {
+    gap: 2px;
+  }
+
+  .operation-buttons .el-button {
+    min-width: 22px;
+    min-height: 22px;
+    padding: 2px 4px;
   }
 }
 </style>