diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index e96f25613..7c52bbcca 100644 --- a/backend/app/api/admin.py +++ b/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, diff --git a/web-admin/src/api/admin.ts b/web-admin/src/api/admin.ts index ccb62117c..fb9c29627 100644 --- a/web-admin/src/api/admin.ts +++ b/web-admin/src/api/admin.ts @@ -346,6 +346,14 @@ export function fetchDeptTree(params?: { keyword?: string }) { }) } +export function fetchExportDepts(data: Record) { + 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) { + return request.post({ + url: '/web/admin/menus/export', + data, + responseType: 'blob' + }) +} + export function fetchCreateMenu(data: { menuName: string parentId?: string diff --git a/web-admin/src/views/system/dept/index.vue b/web-admin/src/views/system/dept/index.vue index 4c7c572b5..374582dc0 100644 --- a/web-admin/src/views/system/dept/index.vue +++ b/web-admin/src/views/system/dept/index.vue @@ -66,6 +66,15 @@ {{ isExpandAll ? '全部折叠' : '全部展开' }} + + 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([]) /** 弹窗上级组织:全量树(不受列表关键词筛选影响) */ const deptTreeForSelect = ref([]) diff --git a/web-admin/src/views/system/menu/index.vue b/web-admin/src/views/system/menu/index.vue index 2d405840d..6ca557566 100644 --- a/web-admin/src/views/system/menu/index.vue +++ b/web-admin/src/views/system/menu/index.vue @@ -65,6 +65,15 @@ {{ isExpandAll ? '全部折叠' : '全部展开' }} + + 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)