Преглед на файлове

feat(system): add Excel export for operlog and dict type lists

Wire ListExportDropdown with filtered/selected backend xlsx export.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu преди 2 месеца
родител
ревизия
b3b6ec187a

+ 50 - 0
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,

+ 100 - 0
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,

+ 6 - 1
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())

+ 9 - 1
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)

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

@@ -223,6 +223,14 @@ export function fetchDictTypeList(params?: { page?: number; pageSize?: number; k
   })
 }
 
+export function fetchExportDictTypes(data: Record<string, any>) {
+  return request.post({
+    url: '/web/admin/dict-types/export',
+    data,
+    responseType: 'blob'
+  })
+}
+
 export function fetchCreateDictType(data: {
   dictName: string
   dictType: string

+ 9 - 0
web-admin/src/api/monitor/operlog/index.ts

@@ -5,6 +5,15 @@ export function list(data?: Record<string, any>) {
   return request.post({ url: '/web/monitor/operlog/list', data })
 }
 
+/** 操作日志导出 */
+export function exportOperLog(data?: Record<string, any>) {
+  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 })

+ 25 - 0
web-admin/src/views/system/dict/index.vue

@@ -72,6 +72,15 @@
             批量删除
           </el-button>
 
+          <ListExportDropdown
+            :export-fn="fetchExportDictTypes"
+            :get-filter-params="getDictTypeExportFilterParams"
+            :has-selection="true"
+            :selected-ids="selectedRows.map((r) => r.id)"
+            :visible-columns="dictTypeExportColumns"
+            filename="字典类型.xlsx"
+          />
+
           <ListToolbarEndActions
             :loading="loading"
             :column-defs="listColumnDefs"
@@ -514,6 +523,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, 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<Api.Dict.DictTypeItem[]>([])
   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)

+ 21 - 1
web-admin/src/views/system/operlog/index.vue

@@ -60,6 +60,15 @@
             查看详情
           </el-button>
 
+          <ListExportDropdown
+            :export-fn="exportOperLog"
+            :get-filter-params="getOperLogExportFilterParams"
+            :has-selection="true"
+            :selected-ids="selectedRows.map((r) => r.id)"
+            :visible-columns="visibleColumns"
+            filename="操作日志.xlsx"
+          />
+
           <ListToolbarEndActions
             :loading="loading"
             :column-defs="listColumnDefs"
@@ -236,10 +245,11 @@
 
 <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 { 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 {