diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 121c1e7c1..e96f25613 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -635,6 +635,56 @@ def list_dict_types( return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) + +DICT_TYPE_EXPORT_COLUMNS = [ + ExportColumn(title="字典名称", field="dictName"), + ExportColumn(title="字典类型编码", field="dictType"), + ExportColumn(title="备注", field="remark"), + ExportColumn(title="创建时间", field="createTime"), +] + + +class DictTypeExportBody(BaseModel): + keyword: Optional[str] = None + ids: Optional[List[str]] = None + columns: Optional[List[str]] = None + + +@router.post("/dict-types/export") +def export_dict_types( + body: DictTypeExportBody, + user: dict = Depends(require_menu_code("system:dict:list")), + db: Session = Depends(get_db), +): + """导出字典类型列表(当前筛选全量,或 ids 已选)。""" + try: + tenant_no = _get_tenant_no(db, user) + items, _ = dict_type_svc.list_dict_types( + 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(DICT_TYPE_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("/dict-types") def create_dict_type( body: DictTypeCreateBody, diff --git a/backend/app/api/monitor.py b/backend/app/api/monitor.py index b653eac31..514c3f9f2 100644 --- a/backend/app/api/monitor.py +++ b/backend/app/api/monitor.py @@ -1,7 +1,11 @@ """监控模块接口(操作日志等)""" from __future__ import annotations +from datetime import datetime +from typing import List, Optional + from fastapi import APIRouter, Depends +from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.orm import Session from app.core.database import get_db @@ -9,6 +13,13 @@ from app.core.menu_guard import require_admin_or_super_admin from app.core.response import error_response, success_response from app.crud.monitor.oper_log import get_oper_log, list_oper_logs from app.schemas.monitor.operlog import OperLogDetailBody, OperLogListBody +from app.utils.excel_export import ( + EXPORT_MAX_ROWS, + ExportColumn, + ExportValidationError, + export_rows_to_xlsx_response, + filter_export_columns, +) router = APIRouter(prefix="/web/monitor", tags=["系统监控"]) @@ -63,6 +74,95 @@ def operlog_list( return error_response(500, str(e)) + +_OPER_STATUS_LABEL = {0: "成功", 1: "失败"} +_BUSINESS_TYPE_LABEL = { + 0: "其它", + 1: "新增", + 2: "修改", + 3: "删除", + 4: "授权", + 5: "导出", + 6: "导入", + 7: "强退", + 8: "生成代码", + 9: "清空数据", +} + +OPERLOG_EXPORT_COLUMNS = [ + ExportColumn(title="操作模块", field="title"), + ExportColumn(title="操作类型", field="businessTypeLabel"), + ExportColumn(title="操作人员", field="operName"), + ExportColumn(title="操作时间", field="operTime"), + ExportColumn(title="状态", field="statusLabel"), + ExportColumn(title="耗时(ms)", field="costTime"), +] + + +class OperLogExportBody(BaseModel): + model_config = ConfigDict(populate_by_name=True) + title: Optional[str] = None + business_type: Optional[int] = Field(default=None, alias="businessType") + oper_name: Optional[str] = Field(default=None, alias="operName") + status: Optional[int] = None + ids: Optional[List[str]] = None + columns: Optional[List[str]] = None + + +@router.post("/operlog/export") +def operlog_export( + body: OperLogExportBody = None, + user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + """导出操作日志(当前筛选全量,或 ids 已选)。""" + try: + body = body or OperLogExportBody() + info = _user_info(user) + rows, _ = list_oper_logs( + db, + page=1, + page_size=EXPORT_MAX_ROWS, + title=body.title, + business_type=body.business_type, + oper_name=body.oper_name, + status=body.status, + ids=body.ids, + max_page_size=EXPORT_MAX_ROWS, + user_info=info, + tenant_no=info.get("tenant_no"), + ) + for row in rows: + bt = row.get("businessType") + row["businessTypeLabel"] = _BUSINESS_TYPE_LABEL.get(int(bt) if bt is not None else -1, str(bt if bt is not None else "")) + st = row.get("status") + row["statusLabel"] = _OPER_STATUS_LABEL.get(int(st) if st is not None else -1, str(st if st is not None else "")) + requested = None + if body.columns: + requested = [] + for key in body.columns: + if key == "businessType": + requested.append("businessTypeLabel") + elif key == "status": + requested.append("statusLabel") + else: + requested.append(key) + cols = filter_export_columns(OPERLOG_EXPORT_COLUMNS, requested) + date_str = datetime.now().strftime("%Y%m%d") + return export_rows_to_xlsx_response( + db, + tenant_no=info.get("tenant_no") or None, + 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("/operlog/detail") def operlog_detail( body: OperLogDetailBody, diff --git a/backend/app/crud/dict_type.py b/backend/app/crud/dict_type.py index 3d1c9e63a..bfeaa7083 100644 --- a/backend/app/crud/dict_type.py +++ b/backend/app/crud/dict_type.py @@ -34,6 +34,8 @@ def list_dict_types( 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(): @@ -44,12 +46,15 @@ def list_dict_types( if kw: like = f"%{kw}%" q0 = q0.filter(or_(SysDictType.dict_name.like(like), SysDictType.dict_type.like(like))) + if ids: + q0 = q0.filter(SysDictType.id.in_(ids)) # 字典类型为租户主数据,不做 create_by 数据范围过滤 return q0 total = _filtered().order_by(None).with_entities(func.count(SysDictType.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(SysDictType.create_time.desc()) diff --git a/backend/app/crud/monitor/oper_log.py b/backend/app/crud/monitor/oper_log.py index be0e68cb3..f4c3d454b 100644 --- a/backend/app/crud/monitor/oper_log.py +++ b/backend/app/crud/monitor/oper_log.py @@ -120,6 +120,8 @@ def list_oper_logs( status: Optional[int] = None, business_id: Optional[str] = None, business_title: Optional[str] = None, + ids: Optional[List[str]] = None, + max_page_size: Optional[int] = None, user_info: Optional[dict] = None, tenant_no: Optional[str] = None, ) -> Tuple[List[Dict[str, Any]], int]: @@ -166,8 +168,14 @@ def list_oper_logs( ) ) + if ids: + q = q.filter(SysApiRequestLog.id.in_(ids)) + total = int(q.with_entities(func.count(SysApiRequestLog.id)).order_by(None).scalar() or 0) - start = (max(page, 1) - 1) * page_size + page = max(1, page) + cap = int(max_page_size) if max_page_size else 500 + page_size = min(max(1, page_size), cap) + start = (page - 1) * page_size rows = ( q.order_by(desc(SysApiRequestLog.create_at)) .offset(start) diff --git a/web-admin/src/api/admin.ts b/web-admin/src/api/admin.ts index b34f94bd8..ccb62117c 100644 --- a/web-admin/src/api/admin.ts +++ b/web-admin/src/api/admin.ts @@ -223,6 +223,14 @@ export function fetchDictTypeList(params?: { page?: number; pageSize?: number; k }) } +export function fetchExportDictTypes(data: Record) { + return request.post({ + url: '/web/admin/dict-types/export', + data, + responseType: 'blob' + }) +} + export function fetchCreateDictType(data: { dictName: string dictType: string diff --git a/web-admin/src/api/monitor/operlog/index.ts b/web-admin/src/api/monitor/operlog/index.ts index 24f4dd86f..67c6903d1 100644 --- a/web-admin/src/api/monitor/operlog/index.ts +++ b/web-admin/src/api/monitor/operlog/index.ts @@ -5,6 +5,15 @@ export function list(data?: Record) { return request.post({ url: '/web/monitor/operlog/list', data }) } +/** 操作日志导出 */ +export function exportOperLog(data?: Record) { + return request.post({ + url: '/web/monitor/operlog/export', + data, + responseType: 'blob' + }) +} + /** 操作日志详情 */ export function getDetail(data: { id: string }) { return request.post({ url: '/web/monitor/operlog/detail', data }) diff --git a/web-admin/src/views/system/dict/index.vue b/web-admin/src/views/system/dict/index.vue index 2ec1083f9..dfe4b5758 100644 --- a/web-admin/src/views/system/dict/index.vue +++ b/web-admin/src/views/system/dict/index.vue @@ -72,6 +72,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, onMounted, computed } from 'vue' @@ -535,6 +545,7 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns' import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading' import { fetchDictTypeList, + fetchExportDictTypes, fetchCreateDictType, fetchUpdateDictType, fetchDeleteDictType, @@ -580,6 +591,20 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns' const selectedRows = ref([]) const loading = ref(false) const pagination = reactive({ page: 1, pageSize: 20, total: 0 }) + + function getDictTypeExportFilterParams() { + const quickKeyword = quickSearchKeyword.value?.trim() + return { + keyword: quickKeyword || searchForm.keyword || undefined + } + } + + const dictTypeExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('dictName')) cols.unshift('dictName') + return cols + }) + const dialogVisible = ref(false) const dialogTitle = ref('新增字典类型') const isEdit = ref(false) diff --git a/web-admin/src/views/system/operlog/index.vue b/web-admin/src/views/system/operlog/index.vue index c9160ace7..e802707bc 100644 --- a/web-admin/src/views/system/operlog/index.vue +++ b/web-admin/src/views/system/operlog/index.vue @@ -60,6 +60,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 { RefreshLeft, Search, View } from '@element-plus/icons-vue' -import { list as listOperLog, detail as getOperLogDetail } from '@/api/monitor/operlog' +import { list as listOperLog, exportOperLog, detail as getOperLogDetail } from '@/api/monitor/operlog' import type { OperLogVO, OperLogQuery } from '@/api/monitor/operlog/types' import { OPER_LOG_BUSINESS_TYPE_OPTIONS, @@ -303,6 +313,16 @@ function buildQuery(): OperLogQuery { } } +function getOperLogExportFilterParams() { + const q = buildQuery() + return { + title: q.title, + businessType: q.businessType, + operName: q.operName, + status: q.status + } +} + async function fetchList() { loading.value = true try {