Pārlūkot izejas kodu

feat(system): 排名分组支持树形结构和层级管理

- 为RankGroupVO和RankGroupForm接口新增parentId、ancestors、sortNum字段
- 添加children字段支持树形结构展示
- 在排名分组页面实现树形表格展示和层级选择功能
- 支持新增下级分组和展开/折叠操作
- 优化分组筛选逻辑,支持按父分组及子分组进行筛选
- 更新对话框表单,添加上级分组选择和显示顺序配置
zhou 3 nedēļas atpakaļ
vecāks
revīzija
83b2a6894d

+ 45 - 5
src/api/system/rankGroup/types.ts

@@ -9,11 +9,26 @@ export interface RankGroupVO {
    */
   eventId: string | number;
 
+  /**
+   * 父分组ID
+   */
+  parentId: string | number;
+
+  /**
+   * 祖级列表
+   */
+  ancestors: string;
+
   /**
    * 分组名
    */
   rgName: string;
 
+  /**
+   * 显示顺序
+   */
+  sortNum: number;
+
   /**
    * 状态(0正常 1停用)
    */
@@ -24,6 +39,11 @@ export interface RankGroupVO {
    */
   remark: string;
 
+  /**
+   * 子级
+   */
+  children: RankGroupVO[];
+
 }
 
 export interface RankGroupForm extends BaseEntity {
@@ -37,11 +57,26 @@ export interface RankGroupForm extends BaseEntity {
    */
   eventId?: string | number;
 
+  /**
+   * 父分组ID
+   */
+  parentId?: string | number;
+
+  /**
+   * 祖级列表
+   */
+  ancestors?: string;
+
   /**
    * 分组名
    */
   rgName?: string;
 
+  /**
+   * 显示顺序
+   */
+  sortNum?: number;
+
   /**
    * 状态(0正常 1停用)
    */
@@ -54,13 +89,18 @@ export interface RankGroupForm extends BaseEntity {
 
 }
 
-export interface RankGroupQuery extends PageQuery {
+export interface RankGroupQuery {
 
   /**
    * 赛事ID
    */
   eventId?: string | number;
 
+  /**
+   * 父分组ID
+   */
+  parentId?: string | number;
+
   /**
    * 分组名
    */
@@ -71,10 +111,10 @@ export interface RankGroupQuery extends PageQuery {
    */
   status?: string;
 
-    /**
-     * 日期范围参数
-     */
-    params?: any;
+  /**
+   * 日期范围参数
+   */
+  params?: any;
 }
 
 

+ 44 - 13
src/views/system/gameEvent/RankingBoardPage.vue

@@ -145,15 +145,16 @@
                     v-model="selectedRankGroupId" 
                     placeholder="选择分组" 
                     clearable 
+                    filterable
                     size="small" 
                     @change="handleRankGroupChange"
                     class="group-select"
                   >
                     <el-option label="全部队伍" :value="ALL_GROUPS_VALUE"></el-option>
                     <el-option
-                      v-for="group in rankGroupOptions"
+                      v-for="group in flatRankGroupOptions"
                       :key="group.rgId"
-                      :label="group.rgName"
+                      :label="group.displayName"
                       :value="group.rgId"
                     />
                   </el-select>
@@ -389,19 +390,50 @@ const loadTeamProjectScores = async () => {
   handleRankGroupChange(selectedRankGroupId.value);
 };
 
+// 扁平化的分组选项
+const flatRankGroupOptions = ref<any[]>([]);
+
+// 将树形结构打平
+const flattenTree = (tree: any[]) => {
+  const arr: any[] = [];
+  tree.forEach(node => {
+    arr.push({
+      ...node,
+      displayName: node.rgName
+    });
+    if (node.children && node.children.length > 0) {
+      arr.push(...flattenTree(node.children));
+    }
+  });
+  return arr;
+};
+
+// 获取所有子节点ID(包含自身)
+const getAllChildIds = (rgId: string | number, options: any[]): (string | number)[] => {
+  const ids: (string | number)[] = [rgId];
+  const findChildren = (parentId: string | number) => {
+    options.forEach(opt => {
+      if (opt.parentId === parentId) {
+        ids.push(opt.rgId);
+        findChildren(opt.rgId);
+      }
+    });
+  };
+  findChildren(rgId);
+  return ids;
+};
+
 // 处理分组筛选变化
 const handleRankGroupChange = (rgId: string | number | null | undefined) => {
   if (rgId === null || rgId === undefined || rgId === '' || rgId === ALL_GROUPS_VALUE) {
     // 显示所有队伍
     filteredTeamScores.value = teamScores.value;
   } else {
-    // 筛选指定分组的队伍
-    filteredTeamScores.value = teamScores.value.filter(team => team.rgId === rgId);
+    // 获取当前选中分组及其所有子分组的ID集合
+    const targetIds = getAllChildIds(rgId, flatRankGroupOptions.value);
+    // 筛选所属分组在集合内的队伍
+    filteredTeamScores.value = teamScores.value.filter(team => targetIds.includes(team.rgId));
   }
-  
-  // 重新计算排名(基于已有的 rank 字段进行筛选显示,不重新计算,因为数据已经是有序的了)
-  // 只是同步到 filteredTeamScores 供前端展示
-  // 注意:如果是从 teamScores 中过滤出来的,它们已经带有了全局排名信息
 };
 
 // 获取分组选项
@@ -409,13 +441,12 @@ const loadRankGroupOptions = async () => {
   try {
     const res = await listRankGroup({
       eventId: defaultEventInfo.value.eventId,
-      pageNum: 1,
-      pageSize: 1000,
-      orderByColumn: undefined,
-      isAsc: undefined,
       status: '0'
     });
-    rankGroupOptions.value = res.rows;
+    // 保存原始树形数据
+    rankGroupOptions.value = res.data;
+    // 打平用于下拉显示
+    flatRankGroupOptions.value = flattenTree(res.data);
   } catch (error) {
     console.error('获取分组列表失败:', error);
   }

+ 107 - 70
src/views/system/rankGroup/index.vue

@@ -20,13 +20,10 @@
       <template #header>
         <el-row :gutter="10" class="mb8">
           <el-col :span="1.5">
-            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:rankGroup:add']">新增</el-button>
+            <el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['system:rankGroup:add']">新增</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:rankGroup:edit']">修改</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:rankGroup:remove']">删除</el-button>
+            <el-button type="info" plain icon="Sort" @click="toggleExpandAll">展开/折叠</el-button>
           </el-col>
           <el-col :span="1.5">
             <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:rankGroup:export']">导出</el-button>
@@ -35,9 +32,15 @@
         </el-row>
       </template>
 
-      <el-table v-loading="loading" border :data="rankGroupList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="序号" align="center" type="index" />
+      <el-table
+        v-if="refreshTable"
+        v-loading="loading"
+        :data="rankGroupList"
+        row-key="rgId"
+        :default-expand-all="isExpandAll"
+        :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+      >
+      <el-table-column label="序号" align="center" type="index" />
         <el-table-column label="分组id" align="center" prop="rgId" v-if="false" />
         <el-table-column label="赛事ID" align="center" prop="eventId" v-if="false" />
         <el-table-column label="分组名" align="center" prop="rgName" />
@@ -49,24 +52,49 @@
             <el-tooltip content="修改" placement="top">
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:rankGroup:edit']"></el-button>
             </el-tooltip>
+            <el-tooltip content="新增下级" placement="top">
+              <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:rankGroup:add']"></el-button>
+            </el-tooltip>
             <el-tooltip content="删除" placement="top">
               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:rankGroup:remove']"></el-button>
             </el-tooltip>
           </template>
         </el-table-column>
       </el-table>
-
-      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
+
     <!-- 添加或修改排名分组对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
       <el-form ref="rankGroupFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="分组名" prop="rgName">
-          <el-input v-model="form.rgName" placeholder="请输入分组名" />
-        </el-form-item>
-        <el-form-item label="备注" prop="remark">
-            <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
-        </el-form-item>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="上级分组" prop="parentId">
+              <el-tree-select
+                v-model="form.parentId"
+                :data="rankGroupOptions"
+                :props="{ value: 'rgId', label: 'rgName', children: 'children' }"
+                value-key="rgId"
+                placeholder="请选择上级分组"
+                check-strictly
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="分组名" prop="rgName">
+              <el-input v-model="form.rgName" placeholder="请输入分组名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="显示顺序" prop="sortNum">
+              <el-input-number v-model="form.sortNum" :min="0" controls-position="right" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注" prop="remark">
+              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+            </el-form-item>
+          </el-col>
+        </el-row>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -86,19 +114,17 @@ import { useGameEventStore } from '@/store/modules/gameEvent';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const rankGroupList = ref<RankGroupVO[]>([]);
+const rankGroupOptions = ref<RankGroupVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
 const showSearch = ref(true);
-const ids = ref<Array<string | number>>([]);
-const single = ref(true);
-const multiple = ref(true);
-const total = ref(0);
+const isExpandAll = ref(false);
+const refreshTable = ref(true);
 
 const queryFormRef = ref<ElFormInstance>();
 const rankGroupFormRef = ref<ElFormInstance>();
 
-let gameEventStore = null;
-let defaultEventInfo = null;
+let gameEventStore = useGameEventStore();
 
 const dialog = reactive<DialogOption>({
   visible: false,
@@ -107,28 +133,25 @@ const dialog = reactive<DialogOption>({
 
 const getInitFormData = () => ({
   rgId: undefined,
-  eventId: defaultEventInfo?.eventId,  // 使用可选链操作符防止报错
+  parentId: 0,
+  eventId: gameEventStore.defaultEventInfo?.eventId,
   rgName: undefined,
-  updateTime: undefined,
+  sortNum: 0,
   status: '0',
   remark: undefined
 });
+
 const data = reactive<PageData<RankGroupForm, RankGroupQuery>>({
   form: getInitFormData(),
   queryParams: {
-    pageNum: 1,
-    pageSize: 10,
-    eventId: defaultEventInfo?.eventId,  // 同样使用可选链操作符
+    eventId: gameEventStore.defaultEventInfo?.eventId,
     rgName: undefined,
     status: '0',
-    orderByColumn: undefined,
-    isAsc: undefined,
     params: {}
   },
   rules: {
-    rgName: [
-      { required: true, message: "分组名不能为空", trigger: "blur" }
-    ]
+    parentId: [{ required: true, message: "上级分组不能为空", trigger: "blur" }],
+    rgName: [{ required: true, message: "分组名不能为空", trigger: "blur" }]
   }
 });
 
@@ -138,58 +161,73 @@ const { queryParams, form, rules } = toRefs(data);
 const getList = async () => {
   loading.value = true;
   const res = await listRankGroup(queryParams.value);
-  rankGroupList.value = res.rows;
-  total.value = res.total;
+  rankGroupList.value = res.data;
   loading.value = false;
-}
+};
+
+/** 查询菜单下拉树结构 */
+const getTreeSelect = async () => {
+  rankGroupOptions.value = [];
+  const res = await listRankGroup({ eventId: queryParams.value.eventId });
+  const group: any = { rgId: 0, rgName: '顶级分组', children: [] };
+  group.children = res.data;
+  rankGroupOptions.value.push(group);
+};
 
 /** 取消按钮 */
 const cancel = () => {
   reset();
   dialog.visible = false;
-}
+};
 
 /** 表单重置 */
 const reset = () => {
   form.value = getInitFormData();
   rankGroupFormRef.value?.resetFields();
-}
+};
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
-  queryParams.value.pageNum = 1;
   getList();
-}
+};
 
 /** 重置按钮操作 */
 const resetQuery = () => {
   queryFormRef.value?.resetFields();
   handleQuery();
-}
+};
 
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: RankGroupVO[]) => {
-  ids.value = selection.map(item => item.rgId);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-}
+/** 展开/折叠操作 */
+const toggleExpandAll = () => {
+  refreshTable.value = false;
+  isExpandAll.value = !isExpandAll.value;
+  nextTick(() => {
+    refreshTable.value = true;
+  });
+};
 
 /** 新增按钮操作 */
-const handleAdd = () => {
+const handleAdd = (row?: RankGroupVO) => {
   reset();
+  getTreeSelect();
+  if (row != null && row.rgId) {
+    form.value.parentId = row.rgId;
+  } else {
+    form.value.parentId = 0;
+  }
   dialog.visible = true;
   dialog.title = "添加排名分组";
-}
+};
 
 /** 修改按钮操作 */
-const handleUpdate = async (row?: RankGroupVO) => {
+const handleUpdate = async (row: RankGroupVO) => {
   reset();
-  const _rgId = row?.rgId || ids.value[0]
-  const res = await getRankGroup(_rgId);
+  getTreeSelect();
+  const res = await getRankGroup(row.rgId);
   Object.assign(form.value, res.data);
   dialog.visible = true;
   dialog.title = "修改排名分组";
-}
+};
 
 /** 提交按钮 */
 const submitForm = () => {
@@ -197,42 +235,41 @@ const submitForm = () => {
     if (valid) {
       buttonLoading.value = true;
       if (form.value.rgId) {
-        await updateRankGroup(form.value).finally(() =>  buttonLoading.value = false);
+        await updateRankGroup(form.value).finally(() => buttonLoading.value = false);
       } else {
-        await addRankGroup(form.value).finally(() =>  buttonLoading.value = false);
+        await addRankGroup(form.value).finally(() => buttonLoading.value = false);
       }
       proxy?.$modal.msgSuccess("操作成功");
       dialog.visible = false;
       await getList();
     }
   });
-}
+};
 
 /** 删除按钮操作 */
-const handleDelete = async (row?: RankGroupVO) => {
-  const _rgIds = row?.rgId || ids.value;
-  await proxy?.$modal.confirm('是否确认删除排名分组编号为"' + _rgIds + '"的数据项?').finally(() => loading.value = false);
-  await delRankGroup(_rgIds);
+const handleDelete = async (row: RankGroupVO) => {
+  await proxy?.$modal.confirm('是否确认删除名称为"' + row.rgName + '"的数据项?');
+  loading.value = true;
+  await delRankGroup(row.rgId).finally(() => loading.value = false);
   proxy?.$modal.msgSuccess("删除成功");
   await getList();
-}
+};
 
 /** 导出按钮操作 */
 const handleExport = () => {
   proxy?.download('system/rankGroup/export', {
     ...queryParams.value
-  }, `rankGroup_${new Date().getTime()}.xlsx`)
-}
+  }, `组别_${new Date().getTime()}.xlsx`)
+};
 
-// 在 onMounted 中初始化 store 后更新表单数据
 onMounted(() => {
-  gameEventStore = useGameEventStore();
-  defaultEventInfo = gameEventStore.defaultEventInfo;
-  
-  form.value.eventId = defaultEventInfo?.eventId;
-  queryParams.value.eventId = defaultEventInfo?.eventId;
-  
   getList();
 });
 
 </script>
+
+<style scoped>
+.dialog-footer {
+  text-align: right;
+}
+</style>