ソースを参照

feat(system): add Excel export for dept and menu tree lists

Export flattened filtered or selected rows via backend xlsx and ListExportDropdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 ヶ月 前
親
コミット
ee56dad875

+ 106 - 0
backend/app/api/admin.py

@@ -987,6 +987,52 @@ def list_depts(
     return success_response(tree)
 
 
+
+DEPT_EXPORT_COLUMNS = [
+    ExportColumn(title="组织名称", field="deptName"),
+    ExportColumn(title="组织类别", field="deptCategoryCode"),
+    ExportColumn(title="状态", field="status", dict_type="sys_status"),
+    ExportColumn(title="负责人", field="leader"),
+    ExportColumn(title="联系电话", field="leaderPhone"),
+    ExportColumn(title="创建时间", field="createTime"),
+]
+
+
+class DeptExportBody(BaseModel):
+    keyword: Optional[str] = None
+    ids: Optional[List[str]] = None
+    columns: Optional[List[str]] = None
+
+
+@router.post("/depts/export")
+def export_depts(
+    body: DeptExportBody,
+    user: dict = Depends(require_menu_code("system:dept:list")),
+    db: Session = Depends(get_db),
+):
+    """导出组织列表(扁平行;当前筛选或已选)。"""
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        rows = dept_svc.list_depts(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
+        if body.ids:
+            idset = set(body.ids)
+            rows = [r for r in rows if r.get("id") in idset]
+        cols = filter_export_columns(DEPT_EXPORT_COLUMNS, body.columns)
+        date_str = datetime.now().strftime("%Y%m%d")
+        return export_rows_to_xlsx_response(
+            db,
+            tenant_no=tenant_no,
+            sheet_title="组织列表",
+            columns=cols,
+            rows=rows,
+            filename_stem=f"组织列表_{date_str}",
+        )
+    except ExportValidationError as e:
+        return error_response(400, str(e))
+    except Exception as e:
+        return error_response(500, str(e))
+
+
 @router.post("/depts")
 def create_dept(
     body: DeptCreateBody,
@@ -1462,6 +1508,66 @@ def list_menus(
     return success_response(tree)
 
 
+
+MENU_EXPORT_COLUMNS = [
+    ExportColumn(title="菜单名称", field="menuName"),
+    ExportColumn(title="菜单类型", field="menuType"),
+    ExportColumn(title="路由", field="path"),
+    ExportColumn(title="组件", field="component"),
+    ExportColumn(title="权限标识", field="perms"),
+    ExportColumn(title="状态", field="status", dict_type="sys_status"),
+    ExportColumn(title="可见", field="visible"),
+    ExportColumn(title="创建时间", field="createTime"),
+]
+
+
+class MenuExportBody(BaseModel):
+    keyword: Optional[str] = None
+    ids: Optional[List[str]] = None
+    columns: Optional[List[str]] = None
+
+
+@router.post("/menus/export")
+def export_menus(
+    body: MenuExportBody,
+    user: dict = Depends(require_admin_or_super_admin),
+    db: Session = Depends(get_db),
+):
+    """导出菜单列表(扁平行;当前筛选或已选)。"""
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        # list flat via tree then flatten, or use underlying list if exists
+        from app.crud import menu as menu_mod
+        if hasattr(menu_mod, 'list_menus'):
+            rows = menu_mod.list_menus(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
+        else:
+            tree = menu_svc.list_menus_tree(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
+            rows = []
+            def walk(nodes):
+                for n in nodes or []:
+                    item = {k: v for k, v in n.items() if k != 'children'}
+                    rows.append(item)
+                    walk(n.get('children') or [])
+            walk(tree)
+        if body.ids:
+            idset = set(body.ids)
+            rows = [r for r in rows if r.get("id") in idset]
+        cols = filter_export_columns(MENU_EXPORT_COLUMNS, body.columns)
+        date_str = datetime.now().strftime("%Y%m%d")
+        return export_rows_to_xlsx_response(
+            db,
+            tenant_no=tenant_no,
+            sheet_title="菜单列表",
+            columns=cols,
+            rows=rows,
+            filename_stem=f"菜单列表_{date_str}",
+        )
+    except ExportValidationError as e:
+        return error_response(400, str(e))
+    except Exception as e:
+        return error_response(500, str(e))
+
+
 @router.post("/menus")
 def create_menu(
     body: MenuCreateBody,

+ 16 - 0
web-admin/src/api/admin.ts

@@ -346,6 +346,14 @@ export function fetchDeptTree(params?: { keyword?: string }) {
   })
 }
 
+export function fetchExportDepts(data: Record<string, any>) {
+  return request.post({
+    url: '/web/admin/depts/export',
+    data,
+    responseType: 'blob'
+  })
+}
+
 export function fetchCreateDept(data: {
   parentId?: string
   deptName: string
@@ -505,6 +513,14 @@ export function fetchMenuTree(params?: { keyword?: string }) {
   })
 }
 
+export function fetchExportMenus(data: Record<string, any>) {
+  return request.post({
+    url: '/web/admin/menus/export',
+    data,
+    responseType: 'blob'
+  })
+}
+
 export function fetchCreateMenu(data: {
   menuName: string
   parentId?: string

+ 24 - 0
web-admin/src/views/system/dept/index.vue

@@ -66,6 +66,15 @@
             {{ isExpandAll ? '全部折叠' : '全部展开' }}
           </el-button>
 
+          <ListExportDropdown
+            :export-fn="fetchExportDepts"
+            :get-filter-params="getDeptExportFilterParams"
+            :has-selection="true"
+            :selected-ids="selectedRows.map((r) => r.id)"
+            :visible-columns="deptExportColumns"
+            filename="组织列表.xlsx"
+          />
+
           <ListToolbarEndActions
             :loading="loading"
             :column-defs="listColumnDefs"
@@ -304,6 +313,7 @@
 
 <script setup lang="ts">
 import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
+import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
 import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
 import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
   import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
@@ -323,6 +333,7 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
   import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
   import {
     fetchDeptTree,
+    fetchExportDepts,
     fetchCreateDept,
     fetchUpdateDept,
     fetchDeleteDept,
@@ -356,6 +367,19 @@ const dictStore = useDictStore()
 
   const searchForm = reactive<{ keyword: string }>({ keyword: '' })
   const quickSearchKeyword = ref('')
+
+  function getDeptExportFilterParams() {
+    const quickKeyword = quickSearchKeyword.value?.trim()
+    return {
+      keyword: quickKeyword || searchForm.keyword || undefined
+    }
+  }
+
+  const deptExportColumns = computed(() => {
+    const cols = [...visibleColumns.value]
+    if (!cols.includes('deptName')) cols.unshift('deptName')
+    return cols
+  })
   const tableData = ref<Api.Dept.DeptItem[]>([])
   /** 弹窗上级组织:全量树(不受列表关键词筛选影响) */
   const deptTreeForSelect = ref<Api.Dept.DeptItem[]>([])

+ 24 - 1
web-admin/src/views/system/menu/index.vue

@@ -65,6 +65,15 @@
             {{ isExpandAll ? '全部折叠' : '全部展开' }}
           </el-button>
 
+          <ListExportDropdown
+            :export-fn="fetchExportMenus"
+            :get-filter-params="getMenuExportFilterParams"
+            :has-selection="true"
+            :selected-ids="selectedRows.map((r) => r.id)"
+            :visible-columns="menuExportColumns"
+            filename="菜单列表.xlsx"
+          />
+
           <ListToolbarEndActions
             :loading="loading"
             :column-defs="listColumnDefs"
@@ -314,6 +323,7 @@
 
 <script setup lang="ts">
 import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
+import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
 import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
 import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
   import { ref, reactive, computed, onMounted, nextTick } from 'vue'
@@ -331,7 +341,7 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
     Search
   } from '@element-plus/icons-vue'
   import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
-  import { fetchMenuTree, fetchCreateMenu, fetchUpdateMenu, fetchDeleteMenu } from '@/api/admin'
+  import { fetchMenuTree, fetchExportMenus, fetchCreateMenu, fetchUpdateMenu, fetchDeleteMenu } from '@/api/admin'
   import { useDictStore } from '@/store/modules/dict'
 
   defineOptions({ name: 'SystemMenu' })
@@ -363,6 +373,19 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
   const isExpandAll = ref(true)
   const tableRef = ref()
 
+  function getMenuExportFilterParams() {
+    const quickKeyword = quickSearchKeyword.value?.trim()
+    return {
+      keyword: quickKeyword || searchForm.keyword || undefined
+    }
+  }
+
+  const menuExportColumns = computed(() => {
+    const cols = [...visibleColumns.value]
+    if (!cols.includes('menuName')) cols.unshift('menuName')
+    return cols
+  })
+
   const dialogVisible = ref(false)
   const dialogTitle = ref('新增菜单')
   const isEdit = ref(false)