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:
1 parent
1053e24896
commit
b3b6ec187a
8 files changed
+228
-3
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user