From 82d8cb7f64844658f3247bb55493c78c75d6a6b0 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Fri, 21 Aug 2026 17:21:50 +0800 Subject: [PATCH] feat(system): add Excel export for tenant, role, post and site lists Reuse list filters with ids/columns support and ListExportDropdown UX. Co-authored-by: Cursor --- backend/app/api/admin.py | 204 ++++++++++++++++++++ backend/app/crud/post.py | 7 +- backend/app/crud/role.py | 7 +- backend/app/crud/site.py | 7 +- backend/app/crud/tenant.py | 7 +- web-admin/src/api/admin.ts | 36 ++++ web-admin/src/views/system/post/index.vue | 25 +++ web-admin/src/views/system/role/index.vue | 27 ++- web-admin/src/views/system/site/index.vue | 17 ++ web-admin/src/views/system/tenant/index.vue | 25 +++ 10 files changed, 356 insertions(+), 6 deletions(-) diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index cf7a53692..121c1e7c1 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -37,6 +37,7 @@ from app.schemas.menu_favorite import ( ) from app.services.web_auth_roles import _map_role_name_to_web from app.utils.excel_export import ( + EXPORT_MAX_ROWS, ExportColumn, ExportValidationError, export_rows_to_xlsx_response, @@ -174,6 +175,55 @@ def list_tenants( ) + +TENANT_EXPORT_COLUMNS = [ + ExportColumn(title="租户编号", field="tenantNo"), + ExportColumn(title="租户名称", field="tenantName"), + ExportColumn(title="联系人", field="contactName"), + ExportColumn(title="联系电话", field="contactPhone"), + ExportColumn(title="创建时间", field="createTime"), +] + + +class TenantExportBody(BaseModel): + keyword: Optional[str] = None + ids: Optional[List[str]] = None + columns: Optional[List[str]] = None + + +@router.post("/tenants/export") +def export_tenants( + body: TenantExportBody, + _user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + """导出租户列表(当前筛选全量,或 ids 已选)。""" + try: + items, _ = tenant_svc.list_tenants( + db, + page=1, + page_size=EXPORT_MAX_ROWS, + keyword=body.keyword, + ids=body.ids, + max_page_size=EXPORT_MAX_ROWS, + user_info=_user, + ) + cols = filter_export_columns(TENANT_EXPORT_COLUMNS, body.columns) + date_str = datetime.now().strftime("%Y%m%d") + return export_rows_to_xlsx_response( + db, + tenant_no=None, + sheet_title="租户列表", + columns=cols, + rows=items, + 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("/tenants") def create_tenant( body: TenantCreateBody, @@ -1001,6 +1051,60 @@ def list_sites( return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) + +SITE_EXPORT_COLUMNS = [ + ExportColumn(title="站点名称", field="siteName"), + ExportColumn(title="站点编码", field="siteCode"), + ExportColumn(title="站点类型", field="siteType"), + ExportColumn(title="状态", field="status", dict_type="sys_status"), + ExportColumn(title="地址", field="address"), + ExportColumn(title="联系人", field="contactName"), + ExportColumn(title="联系电话", field="contactPhone"), + ExportColumn(title="创建时间", field="createTime"), +] + + +class SiteExportBody(BaseModel): + keyword: Optional[str] = None + ids: Optional[List[str]] = None + columns: Optional[List[str]] = None + + +@router.post("/sites/export") +def export_sites( + body: SiteExportBody, + user: dict = Depends(require_menu_code("system:site:list")), + db: Session = Depends(get_db), +): + """导出站点列表(当前筛选全量,或 ids 已选)。""" + try: + tenant_no = _get_tenant_no(db, user) + items, _ = site_svc.list_sites( + db, + page=1, + page_size=EXPORT_MAX_ROWS, + keyword=body.keyword, + tenant_no=tenant_no, + ids=body.ids, + max_page_size=EXPORT_MAX_ROWS, + user_info=user, + ) + cols = filter_export_columns(SITE_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=items, + 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("/sites") def create_site( body: SiteCreateBody, @@ -1110,6 +1214,56 @@ def list_posts( return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) + +POST_EXPORT_COLUMNS = [ + ExportColumn(title="职位名称", field="postName"), + ExportColumn(title="职位编码", field="postCode"), + ExportColumn(title="状态", field="status", dict_type="sys_status"), + ExportColumn(title="创建时间", field="createTime"), +] + + +class PostExportBody(BaseModel): + keyword: Optional[str] = None + ids: Optional[List[str]] = None + columns: Optional[List[str]] = None + + +@router.post("/posts/export") +def export_posts( + body: PostExportBody, + user: dict = Depends(require_menu_code("system:post:list")), + db: Session = Depends(get_db), +): + """导出职位列表(当前筛选全量,或 ids 已选)。""" + try: + tenant_no = _get_tenant_no(db, user) + items, _ = post_svc.list_posts( + db, + page=1, + page_size=EXPORT_MAX_ROWS, + keyword=body.keyword, + tenant_no=tenant_no, + ids=body.ids, + max_page_size=EXPORT_MAX_ROWS, + user_info=user, + ) + cols = filter_export_columns(POST_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=items, + 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("/posts") def create_post( body: PostCreateBody, @@ -1380,6 +1534,56 @@ def list_role_management( return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) + +ROLE_EXPORT_COLUMNS = [ + ExportColumn(title="角色名称", field="roleName"), + ExportColumn(title="数据权限", field="dataScope", dict_type="sys_data_scope"), + ExportColumn(title="状态", field="status", dict_type="sys_status"), + ExportColumn(title="创建时间", field="createTime"), +] + + +class RoleExportBody(BaseModel): + keyword: Optional[str] = None + ids: Optional[List[str]] = None + columns: Optional[List[str]] = None + + +@router.post("/role-management/export") +def export_role_management( + body: RoleExportBody, + user: dict = Depends(require_menu_code("system:role:list")), + db: Session = Depends(get_db), +): + """导出角色列表(当前筛选全量,或 ids 已选)。""" + try: + tenant_no = _get_tenant_no(db, user) + items, _ = role_svc.list_roles( + db, + page=1, + page_size=EXPORT_MAX_ROWS, + keyword=body.keyword, + tenant_no=tenant_no, + ids=body.ids, + max_page_size=EXPORT_MAX_ROWS, + user_info=user, + ) + cols = filter_export_columns(ROLE_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=items, + 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("/role-management") def create_role_management( body: RoleCreateBody, diff --git a/backend/app/crud/post.py b/backend/app/crud/post.py index 54690ed99..68efa97a1 100644 --- a/backend/app/crud/post.py +++ b/backend/app/crud/post.py @@ -35,6 +35,8 @@ def list_posts( page_size: int = 10, keyword: Optional[str] = None, tenant_no: Optional[str] = None, + ids: Optional[List[str]] = None, + max_page_size: Optional[int] = None, user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: def _filtered(): @@ -45,12 +47,15 @@ def list_posts( if kw: like = f"%{kw}%" q0 = q0.filter(or_(SysPost.post_name.like(like), SysPost.post_code.like(like))) + if ids: + q0 = q0.filter(SysPost.id.in_(ids)) # 职位为租户主数据,不做 create_by 数据范围过滤 return q0 total = _filtered().order_by(None).with_entities(func.count(SysPost.id)).scalar() or 0 page = max(1, page) - page_size = min(max(1, page_size), 500) + cap = int(max_page_size) if max_page_size else 500 + page_size = min(max(1, page_size), cap) rows = ( _filtered() .order_by(SysPost.create_time.desc()) diff --git a/backend/app/crud/role.py b/backend/app/crud/role.py index ac18641b8..07856a4f9 100644 --- a/backend/app/crud/role.py +++ b/backend/app/crud/role.py @@ -51,6 +51,8 @@ def list_roles( page_size: int = 10, keyword: Optional[str] = None, tenant_no: Optional[str] = None, + ids: Optional[List[str]] = None, + max_page_size: Optional[int] = None, user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: def _filtered(): @@ -61,6 +63,8 @@ def list_roles( if kw: like = f"%{kw}%" q0 = q0.filter(SysRole.role_name.like(like)) + if ids: + q0 = q0.filter(SysRole.id.in_(ids)) if user_info: from app.core.data_scope import apply_data_scope q0 = apply_data_scope(q0, SysRole, user_info, db, tenant_no) @@ -68,7 +72,8 @@ def list_roles( total = _filtered().order_by(None).with_entities(func.count(SysRole.id)).scalar() or 0 page = max(1, page) - page_size = min(max(1, page_size), 500) + cap = int(max_page_size) if max_page_size else 500 + page_size = min(max(1, page_size), cap) rows = ( _filtered() .order_by(SysRole.create_time.desc()) diff --git a/backend/app/crud/site.py b/backend/app/crud/site.py index 3a907ac98..61645b732 100644 --- a/backend/app/crud/site.py +++ b/backend/app/crud/site.py @@ -40,6 +40,8 @@ def list_sites( page_size: int = 10, keyword: Optional[str] = None, tenant_no: Optional[str] = None, + ids: Optional[List[str]] = None, + max_page_size: Optional[int] = None, user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: def _filtered(): @@ -50,12 +52,15 @@ def list_sites( if kw: like = f"%{kw}%" q0 = q0.filter(or_(SysSite.site_name.like(like), SysSite.site_code.like(like))) + if ids: + q0 = q0.filter(SysSite.id.in_(ids)) # 站点为租户主数据,不做 create_by 数据范围过滤 return q0 total = _filtered().order_by(None).with_entities(func.count(SysSite.id)).scalar() or 0 page = max(1, page) - page_size = min(max(1, page_size), 500) + cap = int(max_page_size) if max_page_size else 500 + page_size = min(max(1, page_size), cap) rows = ( _filtered() .order_by(SysSite.create_time.desc()) diff --git a/backend/app/crud/tenant.py b/backend/app/crud/tenant.py index 4da4d411a..52375d717 100644 --- a/backend/app/crud/tenant.py +++ b/backend/app/crud/tenant.py @@ -35,6 +35,8 @@ def list_tenants( page: int = 1, page_size: int = 10, keyword: Optional[str] = None, + ids: Optional[List[str]] = None, + max_page_size: Optional[int] = None, user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: def _filtered(): @@ -43,6 +45,8 @@ def list_tenants( if kw: like = f"%{kw}%" q0 = q0.filter(or_(SysTenant.tenant_no.like(like), SysTenant.tenant_name.like(like))) + if ids: + q0 = q0.filter(SysTenant.id.in_(ids)) if user_info: from app.core.data_scope import apply_data_scope q0 = apply_data_scope(q0, SysTenant, user_info, db) @@ -50,7 +54,8 @@ def list_tenants( total = _filtered().order_by(None).with_entities(func.count(SysTenant.id)).scalar() or 0 page = max(1, page) - page_size = min(max(1, page_size), 500) + cap = int(max_page_size) if max_page_size else 500 + page_size = min(max(1, page_size), cap) rows = ( _filtered() .order_by(SysTenant.create_time.desc()) diff --git a/web-admin/src/api/admin.ts b/web-admin/src/api/admin.ts index 585389aca..b34f94bd8 100644 --- a/web-admin/src/api/admin.ts +++ b/web-admin/src/api/admin.ts @@ -52,6 +52,15 @@ export function fetchTenantList(params?: { page?: number; pageSize?: number; key }) } + +export function fetchExportTenants(data: Record) { + return request.post({ + url: '/web/admin/tenants/export', + data, + responseType: 'blob' + }) +} + export function fetchCreateTenant(data: { tenantNo: string tenantName: string @@ -374,6 +383,15 @@ export function fetchSiteList(params?: { page?: number; pageSize?: number; keywo totalPages: number }>({ url: '/web/admin/sites', params }) } + + +export function fetchExportSites(data: Record) { + return request.post({ + url: '/web/admin/sites/export', + data, + responseType: 'blob' + }) +} export function fetchCreateSite(data: { siteName: string siteCode: string @@ -422,6 +440,15 @@ export function fetchPostList(params?: { page?: number; pageSize?: number; keywo totalPages: number }>({ url: '/web/admin/posts', params }) } + + +export function fetchExportPosts(data: Record) { + return request.post({ + url: '/web/admin/posts/export', + data, + responseType: 'blob' + }) +} export function fetchCreatePost(data: { postName: string postCode: string @@ -522,6 +549,15 @@ export function fetchRoleList(params?: { page?: number; pageSize?: number; keywo totalPages: number }>({ url: '/web/admin/role-management', params }) } + + +export function fetchExportRoles(data: Record) { + return request.post({ + url: '/web/admin/role-management/export', + data, + responseType: 'blob' + }) +} export function fetchRoleDetail(id: string) { return request.get({ url: `/web/admin/role-management/${id}` }) } diff --git a/web-admin/src/views/system/post/index.vue b/web-admin/src/views/system/post/index.vue index 777c44b33..b9cae50a4 100644 --- a/web-admin/src/views/system/post/index.vue +++ b/web-admin/src/views/system/post/index.vue @@ -73,6 +73,15 @@ 批量删除 + + 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' @@ -377,6 +387,7 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns' import { useDictStore } from '@/store/modules/dict' import { fetchPostList, + fetchExportPosts, fetchCreatePost, fetchUpdatePost, fetchDeletePost, @@ -411,6 +422,20 @@ import { useDictStore } from '@/store/modules/dict' const selectedRows = ref([]) const loading = ref(false) const pagination = reactive({ page: 1, pageSize: 20, total: 0 }) + + function getPostExportFilterParams() { + const quickKeyword = quickSearchKeyword.value?.trim() + return { + keyword: quickKeyword || searchForm.keyword || undefined + } + } + + const postExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('postName')) cols.unshift('postName') + return cols + }) + const dialogVisible = ref(false) const dialogTitle = ref('新增职位') const isEdit = ref(false) diff --git a/web-admin/src/views/system/role/index.vue b/web-admin/src/views/system/role/index.vue index 1280dd612..2e6850d4b 100644 --- a/web-admin/src/views/system/role/index.vue +++ b/web-admin/src/views/system/role/index.vue @@ -85,6 +85,15 @@ 批量删除 + + 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 RoleManageDialogs from '@/components/business/role-manage-dialogs/index.vue' import { usePersistedListColumns } from '@/composables/usePersistedListColumns' - import { ref, reactive, onMounted } from 'vue' + import { ref, reactive, onMounted, computed } from 'vue' import { canDeleteByCreator, filterDeletableByCreator, @@ -211,7 +221,7 @@ } from '@element-plus/icons-vue' import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading' import { useDictStore } from '@/store/modules/dict' - import { fetchRoleList, fetchDeleteRole } from '@/api/admin' + import { fetchRoleList, fetchExportRoles, fetchDeleteRole } from '@/api/admin' defineOptions({ name: 'SystemRole' }) @@ -235,6 +245,19 @@ const pagination = reactive({ page: 1, pageSize: 20, total: 0 }) const roleDialogsRef = ref | null>(null) + function getRoleExportFilterParams() { + const quickKeyword = quickSearchKeyword.value?.trim() + return { + keyword: quickKeyword || searchForm.keyword || undefined + } + } + + const roleExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('roleName')) cols.unshift('roleName') + return cols + }) + const handleSelectionChange = (rows: Api.Role.RoleItem[]) => { selectedRows.value = rows } diff --git a/web-admin/src/views/system/site/index.vue b/web-admin/src/views/system/site/index.vue index 1620315ea..3c4676126 100644 --- a/web-admin/src/views/system/site/index.vue +++ b/web-admin/src/views/system/site/index.vue @@ -41,6 +41,14 @@ 批量删除 + + @@ -302,6 +310,7 @@