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>
This commit is contained in:
zlwuandCursor committed 2026-08-21 17:28:57 +08:00
1 parent 1053e24896
commit b3b6ec187a
8 files changed
+228 -3

No files matched your search

+50
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
@@ -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
View File
@@ -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
View File
@@ -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 {