feat(admin): 新增后台审计日志视图

This commit is contained in:
yml2213
2026-10-30 13:29:46 +08:00
parent d46636c3d2
commit 1acf433611
10 changed files with 713 additions and 6 deletions
+3 -3
View File
@@ -194,12 +194,12 @@ dist/assets/AdminLayout-*.js 36.87 kB │ gzip 13.02 kB
## 5. 已知限制
1. 移动端订单列表/详情与消息中心已接入真实接口;下单、支付、交接、归还、结账等操作仍待迁移。
2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板、公告管理,其余页面仍为统一占位。
2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板、公告管理、审计日志,其余页面仍为统一占位。
3. 分销后台已完成主题、Logo、登录页与 Shell 轻改,页面结构未重写。
4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`,公告 `announcement:view` / `announcement:manage`;其余页面仍需核对。
4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`,公告 `announcement:view` / `announcement:manage`,审计日志 `audit_log:view`;系统配置与分销站点仍需核对。
5. SVG 中文尚未转曲。
6. 入口包已降至 495 kB(gzip 161 kB);AntD 共享基础设施仍在入口,后续可拆独立入口。
7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理与审核在本批次提交。
7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理、审核、退款、提现、用户、申诉、财务、公告与审计日志已按批次提交。
---
+86
View File
@@ -0,0 +1,86 @@
# Phase 2 验收报告(后台审计日志)
> 日期:2026-10-30
> 项目:解忧商行(hfb_jysh)
> 范围:`/console-jy8k3m/audit-logs`
> 结论:**本切片通过,审计日志列表、筛选、分页与明细抽屉已接真实接口**
---
## 1. 接口与权限
| 能力 | 接口 | 权限码 |
| --- | --- | --- |
| 审计日志列表 | `GET /api/admin/audit-logs` | `audit_log:view` |
查询参数:
```text
actor_id 管理员 ID,正整数
action 动作代码,精确匹配
biz_type 业务类型,精确匹配
page 页码,默认 1
page_size 每页条数,默认 20,最大 100
```
响应字段:`items`、`total`、`page`、`page_size`。单条记录包含 `id`、`actor_type`、`actor_id`、`actor_username`、`actor_nickname`、`action`、`biz_type`、`biz_id`、`ip`、`user_agent`、`detail`、`created_at`。
## 2. 页面
`frontend/src/features/admin/AdminAuditLogsPage.tsx`
- 筛选:管理员 ID、动作、业务类型
- 动作与业务类型支持下拉建议,也允许直接输入后端动作代码
- 列:时间/编号、操作人、动作、业务对象、IP、变更摘要、操作
- 分页 10 / 20 / 50
- 行点击或「详情」打开右侧抽屉
- 无 `audit_log:view` 权限时显示提示并停止请求
## 3. 明细抽屉
- 操作信息:发生时间、日志编号、操作人、操作人类型、动作代码、业务对象
- 请求信息:来源 IP、User-Agent
- 变更明细:`detail` 统一格式化为两空格缩进 JSON,等宽字体、可滚动
- `detail` 兼容 JSON 对象、JSON 字符串与 `null`,无法解析的字符串按原文展示
## 4. 动作与业务类型文案
在 `frontend/src/features/admin/labels.ts` 增加:
- `auditActorTypeMap`:管理员 / 用户 / 系统
- `auditBizTypeMap`:订单、商品、申诉、用户资料、后台账号、系统配置、支付配置、人工打款、运营支出、捡漏、分销、公告
- `auditActionMap`:订单、商品、申诉、用户、配置、打款、支出、捡漏、分销与令牌刷新等已知动作的中文名称
未知动作或业务类型保留原始代码,避免后端新增动作时前端丢失信息。
## 5. 菜单与路由
- 路由:`frontend/src/app/AppRouter.tsx` 新增 `audit-logs`
- 菜单:`frontend/src/layouts/AdminLayout.tsx` 权限码由错误的 `audit:view` 修正为后端实际使用的 `audit_log:view`
- 占位列表同步移除 `audit-logs`
## 6. 验证结果
```text
npx tsc --noEmit 通过
npm run build 通过(入口 497.18 kB,gzip 161.36 kB)
GET /api/admin/audit-logs?page=1&page_size=5 ok,total=1
GET /api/admin/audit-logs?actor_id=1&action=... ok,筛选命中 1 条
GET /console-jy8k3m/audit-logs 200
```
开发库当前只有 1 条改密产生的审计记录;列表读取与筛选已实测,未触发新的资金或配置写操作。
## 7. 已知限制
1. 没有动作字典接口,筛选项来自前端维护的已知动作清单,后端新增动作需同步文案(未知动作仍可展示与按代码筛选)。
2. 不支持时间范围筛选与关键字搜索,后端暂未提供对应参数。
3. 没有导出 CSV 或日志归档能力。
4. `detail` 仅在抽屉中展示,未做字段级 diff 高亮。
## 8. 下一步建议
1. 后台系统配置页。
2. 线下出款与运营支出登记。
3. 审计日志时间范围与关键字筛选(需后端补参数)。
4. 移动端下单、支付与交接/归还/结账。
+1 -1
View File
@@ -395,7 +395,7 @@ hfb_jysh/
### Phase 4:管理后台核心(4–5 周)
> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板与公告管理;线下出款/运营支出、系统配置与审计日志待迁移。
> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板、公告管理与审计日志;线下出款/运营支出与系统配置待迁移。
- 后台登录、工作台、订单列表、订单详情。
- 商品列表、商品详情、审核。
+6 -1
View File
@@ -103,6 +103,11 @@ const AdminAnnouncementsPage = lazy(() =>
default: module.AdminAnnouncementsPage,
}))
)
const AdminAuditLogsPage = lazy(() =>
import('@/features/admin/AdminAuditLogsPage').then(module => ({
default: module.AdminAuditLogsPage,
}))
)
const AdminLayout = lazy(() =>
import('@/layouts/AdminLayout').then(module => ({ default: module.AdminLayout }))
)
@@ -113,7 +118,6 @@ const AdminSectionPlaceholder = lazy(() =>
const adminSections: Array<{ path: string; title: string; description: string }> = [
{ path: 'distribution/sites', title: '分销站点', description: '管理分站、规则、结算与站点钱包。' },
{ path: 'system-configs', title: '系统配置', description: '维护首页、发布、订单与支付配置。' },
{ path: 'audit-logs', title: '审计日志', description: '查看高风险操作与变更记录。' },
]
export function AppRouter() {
@@ -154,6 +158,7 @@ export function AppRouter() {
<Route path="disputes" element={<AdminDisputesPage />} />
<Route path="finance" element={<AdminFinancePage />} />
<Route path="announcements" element={<AdminAnnouncementsPage />} />
<Route path="audit-logs" element={<AdminAuditLogsPage />} />
{adminSections.map(section => (
<Route
key={section.path}
@@ -0,0 +1,457 @@
import { useMemo, useState } from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import {
Alert,
AutoComplete,
Button,
InputNumber,
Space,
Tag,
Typography,
type TableColumnsType,
} from 'antd'
import { EyeOutlined, ReloadOutlined, SafetyCertificateOutlined } from '@ant-design/icons'
import { fetchAdminAuditLogs } from '@/features/admin/api'
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
import { DataTable } from '@/features/admin/components/DataTable'
import { DetailDrawer, type DetailSection } from '@/features/admin/components/DetailDrawer'
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
import {
auditActionMap,
auditActionOptions,
auditActorTypeMap,
auditBizTypeMap,
auditBizTypeOptions,
} from '@/features/admin/labels'
import type { AdminAuditLog, AdminAuditQuery } from '@/features/admin/types'
import { useAdminSessionStore } from '@/stores/adminSession'
import { formatDateTime, formatText } from '@/shared/utils/format'
const emptyFilters: AdminAuditQuery = {
actor_id: undefined,
action: '',
biz_type: '',
}
function parseDetail(detail: unknown): unknown {
if (typeof detail !== 'string') return detail
const text = detail.trim()
if (!text) return null
try {
return JSON.parse(text)
} catch {
return text
}
}
function normalizeDetail(detail: unknown): string {
const parsed = parseDetail(detail)
if (parsed == null) return '—'
if (typeof parsed === 'string') return parsed
try {
return JSON.stringify(parsed, null, 2)
} catch {
return String(parsed)
}
}
function summarizeValue(value: unknown): string {
if (value == null) return 'null'
if (typeof value === 'string') return value
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}
function summarizeDetail(detail: unknown): string {
const parsed = parseDetail(detail)
if (parsed == null) return '—'
if (typeof parsed === 'object' && !Array.isArray(parsed)) {
const entries = Object.entries(parsed as Record<string, unknown>)
if (entries.length === 0) return '{}'
return entries
.slice(0, 4)
.map(([key, value]) => `${key}: ${summarizeValue(value)}`)
.join(' · ')
}
const text = summarizeValue(parsed)
return text.length > 120 ? `${text.slice(0, 120)}…` : text
}
function actorName(row: AdminAuditLog) {
return row.actor_nickname || row.actor_username || `ID ${row.actor_id}`
}
function actorTypeLabel(value?: string | null) {
if (!value) return '未知来源'
return auditActorTypeMap[value] ?? value
}
function bizTypeLabel(value?: string | null) {
if (!value) return '未关联业务'
return auditBizTypeMap[value] ?? value
}
function buildColumns(onOpen: (row: AdminAuditLog) => void): TableColumnsType<AdminAuditLog> {
return [
{
title: '时间 / 编号',
dataIndex: 'created_at',
width: 180,
fixed: 'left',
render: (_, row) => (
<div className="admin-stack">
<strong>{formatDateTime(row.created_at)}</strong>
<span className="admin-mono">#{row.id}</span>
</div>
),
},
{
title: '操作人',
key: 'actor',
width: 210,
render: (_, row) => (
<div className="admin-stack">
<button type="button" className="admin-order-link" onClick={() => onOpen(row)}>
{actorName(row)}
</button>
<span>
{actorTypeLabel(row.actor_type)}
{row.actor_username ? ` · ${row.actor_username}` : ''}
</span>
<span className="admin-mono">ID {row.actor_id}</span>
</div>
),
},
{
title: '动作',
dataIndex: 'action',
width: 230,
render: (_, row) => (
<div className="admin-stack">
<strong>{auditActionMap[row.action] ?? row.action}</strong>
<span className="admin-mono">{row.action}</span>
</div>
),
},
{
title: '业务对象',
key: 'biz',
width: 150,
render: (_, row) => (
<div className="admin-stack">
<span>{bizTypeLabel(row.biz_type)}</span>
<span className="admin-mono">{row.biz_id != null ? `#${row.biz_id}` : '无关联 ID'}</span>
</div>
),
},
{
title: 'IP',
dataIndex: 'ip',
width: 140,
render: value => <span className="admin-mono">{formatText(value)}</span>,
},
{
title: '变更摘要',
key: 'summary',
width: 320,
render: (_, row) => (
<Typography.Text type="secondary" ellipsis={{ tooltip: summarizeDetail(row.detail) }}>
<span className="admin-audit-summary">{summarizeDetail(row.detail)}</span>
</Typography.Text>
),
},
{
title: '操作',
key: 'actions',
width: 92,
fixed: 'right',
render: (_, row) => (
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => onOpen(row)}>
详情
</Button>
),
},
]
}
function buildSections(row: AdminAuditLog): DetailSection[] {
return [
{
key: 'actor',
title: '操作信息',
icon: <SafetyCertificateOutlined />,
fields: [
{
key: 'time',
label: '发生时间',
value: formatDateTime(row.created_at),
},
{
key: 'log_id',
label: '日志编号',
value: <span className="admin-mono">#{row.id}</span>,
},
{
key: 'actor',
label: '操作人',
value: (
<div className="admin-stack">
<strong>{actorName(row)}</strong>
<span>{actorTypeLabel(row.actor_type)}</span>
</div>
),
},
{
key: 'actor_id',
label: '操作人 ID',
value: <span className="admin-mono">{row.actor_id}</span>,
},
{
key: 'action',
label: '动作',
value: (
<div className="admin-stack">
<strong>{auditActionMap[row.action] ?? row.action}</strong>
<span className="admin-mono">{row.action}</span>
</div>
),
},
{
key: 'biz',
label: '业务对象',
value: (
<div className="admin-stack">
<strong>{bizTypeLabel(row.biz_type)}</strong>
<span className="admin-mono">{row.biz_id != null ? `#${row.biz_id}` : '—'}</span>
</div>
),
},
],
},
{
key: 'request',
title: '请求信息',
fields: [
{
key: 'ip',
label: '来源 IP',
value: <span className="admin-mono">{formatText(row.ip)}</span>,
},
{
key: 'user_agent',
label: 'User-Agent',
span: 2,
value: <span className="admin-audit-ua">{formatText(row.user_agent)}</span>,
},
],
},
{
key: 'detail',
title: '变更明细',
fields: [
{
key: 'json',
label: '原始 JSON',
span: 2,
value: <pre className="admin-detail__pre admin-detail__pre--tall">{normalizeDetail(row.detail)}</pre>,
},
],
},
]
}
export function AdminAuditLogsPage() {
const [draft, setDraft] = useState<AdminAuditQuery>(emptyFilters)
const [applied, setApplied] = useState<AdminAuditQuery>(emptyFilters)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [selected, setSelected] = useState<AdminAuditLog | null>(null)
const [detailOpen, setDetailOpen] = useState(false)
const permissions = useAdminSessionStore(state => state.admin?.permissions)
const canView = Boolean(
permissions?.includes('*') || permissions?.includes('audit_log:view')
)
const { data, isPending, isFetching, isError, error, refetch } = useQuery({
queryKey: ['admin-audit-logs', applied, page, pageSize],
queryFn: () => fetchAdminAuditLogs({ ...applied, page, page_size: pageSize }),
placeholderData: keepPreviousData,
enabled: canView,
})
const columns = useMemo(
() =>
buildColumns(row => {
setSelected(row)
setDetailOpen(true)
}),
[]
)
const patchFilter = (patch: Partial<AdminAuditQuery>) => {
setDraft(current => ({ ...current, ...patch }))
}
const applyFilters = () => {
setApplied({ ...draft })
setPage(1)
}
const resetFilters = () => {
setDraft(emptyFilters)
setApplied(emptyFilters)
setPage(1)
}
const hasActiveFilters = Boolean(draft.actor_id || draft.action || draft.biz_type)
const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录后台。'
return (
<div className="admin-page">
<AdminPageHeader
eyebrow="JIEYOU OPS · 系统"
title="审计日志"
description="追踪后台高风险操作、资金变更与关键配置调整,保留操作前后的完整明细。"
meta={
<Space size={8} wrap>
<Tag color="geekblue">共 {data?.total ?? 0} 条</Tag>
{isFetching && !isPending ? <Tag color="processing">正在更新</Tag> : null}
<Tag>只读</Tag>
</Space>
}
extra={
<Button
icon={<ReloadOutlined />}
onClick={() => void refetch()}
loading={isFetching}
disabled={!canView}
>
刷新数据
</Button>
}
/>
{!canView ? (
<Alert
type="warning"
showIcon
className="admin-table-alert"
message="当前账号没有审计日志查看权限"
description="需要 audit_log:view 权限才能读取后台操作记录,请联系超级管理员分配角色。"
/>
) : null}
<FilterBar
onSearch={applyFilters}
onReset={hasActiveFilters ? resetFilters : undefined}
loading={isFetching}
>
<FilterField label="管理员 ID" width={160}>
<InputNumber
min={1}
precision={0}
style={{ width: '100%' }}
placeholder="如 1"
value={draft.actor_id}
onChange={value =>
patchFilter({ actor_id: typeof value === 'number' ? value : undefined })
}
/>
</FilterField>
<FilterField label="动作" width={270}>
<AutoComplete
allowClear
value={draft.action || undefined}
options={auditActionOptions}
placeholder="如 order.admin_close"
filterOption={(input, option) =>
String(option?.value ?? '').includes(input) ||
String(option?.label ?? '').includes(input)
}
onChange={value => patchFilter({ action: value ?? '' })}
/>
</FilterField>
<FilterField label="业务类型" width={200}>
<AutoComplete
allowClear
value={draft.biz_type || undefined}
options={auditBizTypeOptions}
placeholder="如 order"
filterOption={(input, option) =>
String(option?.value ?? '').includes(input) ||
String(option?.label ?? '').includes(input)
}
onChange={value => patchFilter({ biz_type: value ?? '' })}
/>
</FilterField>
</FilterBar>
{isError ? (
<Alert
type="error"
showIcon
className="admin-table-alert"
message="审计日志没有加载出来"
description={errorText}
action={
<Button size="small" onClick={() => void refetch()}>
重试
</Button>
}
/>
) : null}
<DataTable<AdminAuditLog>
title={<span>操作记录</span>}
extra={
<Typography.Text type="secondary">
{data ? `第 ${data.page} 页 · 当前 ${data.items.length} 条` : '加载中'}
</Typography.Text>
}
rowKey="id"
columns={columns}
dataSource={data?.items ?? []}
loading={isFetching}
total={data?.total ?? 0}
page={data?.page ?? page}
pageSize={data?.page_size ?? pageSize}
onPageChange={(nextPage, nextPageSize) => {
setPage(nextPageSize !== pageSize ? 1 : nextPage)
setPageSize(nextPageSize)
}}
onRowClick={row => {
setSelected(row)
setDetailOpen(true)
}}
emptyDescription={canView ? '没有匹配的审计记录' : '当前账号没有查看权限'}
/>
<DetailDrawer
open={detailOpen}
onClose={() => setDetailOpen(false)}
title={selected ? auditActionMap[selected.action] ?? selected.action : '审计详情'}
subtitle={
selected
? `#${selected.id} · ${formatDateTime(selected.created_at)} · ${actorName(selected)}`
: undefined
}
tags={
selected ? (
<Space size={6} wrap>
<Tag color="geekblue">{bizTypeLabel(selected.biz_type)}</Tag>
<Tag>{actorTypeLabel(selected.actor_type)}</Tag>
</Space>
) : undefined
}
sections={selected ? buildSections(selected) : []}
width={820}
emptyDescription="没有可展示的审计详情"
/>
</div>
)
}
+17
View File
@@ -5,6 +5,9 @@ import type {
AdminAnnouncementPage,
AdminAnnouncementPayload,
AdminAnnouncementQuery,
AdminAuditLog,
AdminAuditPage,
AdminAuditQuery,
AdminCaptcha,
AdminDispute,
AdminDisputePage,
@@ -405,3 +408,17 @@ export async function unarchiveAdminAnnouncement(id: number | string) {
export async function deleteAdminAnnouncement(id: number | string) {
await adminClient.delete(`/admin/announcements/${id}`)
}
export async function fetchAdminAuditLogs(query: AdminAuditQuery) {
const { data } = await adminClient.get<ApiResponse<Partial<AdminAuditPage>>>('/admin/audit-logs', {
params: compactParams(query),
})
return {
items: data.data.items ?? [],
total: data.data.total ?? 0,
page: data.data.page ?? query.page ?? 1,
page_size: data.data.page_size ?? query.page_size ?? 20,
}
}
export type { AdminAuditLog }
+92
View File
@@ -218,3 +218,95 @@ export const announcementStatusOptions = Object.entries(announcementStatusMap).m
export const announcementCategoryOptions = Object.entries(announcementCategoryMap).map(
([value, label]) => ({ value, label })
)
export const auditActorTypeMap: Record<string, string> = {
admin: '管理员',
user: '用户',
system: '系统',
}
export const auditBizTypeMap: Record<string, string> = {
order: '订单',
listing: '商品',
dispute: '申诉',
admin_user: '用户资料',
admin: '后台账号',
system_config: '系统配置',
payment_config: '支付配置',
manual_disbursement: '人工打款',
operating_expense: '运营支出',
pickup: '捡漏订单',
pickup_shop: '捡漏店铺',
distribution: '分销',
announcement: '公告',
}
export const auditActionMap: Record<string, string> = {
'order.admin_close': '后台关闭订单',
'order.admin_seal': '订单封存',
'order.deposit_hold': '冻结押金',
'order.deposit_release': '释放押金',
'order.force_handoff': '强制交接',
'order.mark_abnormal': '标记异常订单',
'order.offline_settlement': '线下结算',
'order.platform_handoff': '平台代交接',
'order.platform_checkout_confirm': '平台代确认结账',
'order.platform_checkout_counter': '平台修正结账',
'order.platform_checkout_dispute': '平台发起结账争议',
'order.reset_checkout_confirm': '重置结账确认',
'order.reset_handoff': '重置交接状态',
'order.reset_renter_confirm': '重置租客确认',
'order.reset_return_overdue': '重置归还逾期',
'listing.adjust_external': '调整外部同步',
'listing.adjust_review_price': '调整审核价',
'listing.admin_offline': '后台下架商品',
'listing.mark_abnormal': '标记异常商品',
'listing.transfer_owner': '转移商品归属',
'listing.update_screenshots': '更新商品截图',
'dispute.admin_create': '代建申诉',
'dispute.arbitrate': '提交仲裁',
'admin_user.freeze': '冻结用户',
'admin_user.unfreeze': '解冻用户',
'admin_user.set_deposit_free_quota': '设置免押额度',
'admin_user.adjust_growth_points': '调整成长值',
'admin_user.manual_realname': '人工实名',
'admin_user.revoke_realname': '撤销实名',
'admin_user.wallet_adjust': '调整钱包余额',
'system_config.create': '新增系统配置',
'system_config.update': '更新系统配置',
'payment_config.create': '新增支付配置',
'payment_config.update': '更新支付配置',
'payment_config.delete': '删除支付配置',
'payment_config.export': '导出支付配置',
'payment_config.import': '导入支付配置',
'payment_config.view_secret': '查看支付密钥',
'manual_disbursement_create': '新建人工打款',
'manual_disbursement_void': '作废人工打款',
'operating_expense_create': '登记运营支出',
'operating_expense_void': '作废运营支出',
'pickup_create': '创建捡漏订单',
'pickup_cancel': '取消捡漏订单',
'pickup_complete': '完成捡漏订单',
'pickup_profit_update': '更新捡漏利润',
'pickup_offline_settlement': '捡漏线下结算',
'pickup_financial_adjustment_create': '新建捡漏财务调整',
'pickup_financial_adjustment_settle': '结算捡漏财务调整',
'pickup_shop_create': '新增捡漏店铺',
'pickup_shop_update': '更新捡漏店铺',
'pickup_shop_delete': '删除捡漏店铺',
'pickup_shop_map_historical': '关联历史捡漏店铺',
'pickup_shop_unmap_historical': '解除历史捡漏店铺',
'pickup_third_party_order_no_update': '更新第三方单号',
'distribution_financial_snapshot_create': '生成分销财务快照',
'auth.token_version.bump': '刷新登录令牌版本',
}
export const auditActionOptions = Object.entries(auditActionMap).map(([value, label]) => ({
value,
label,
}))
export const auditBizTypeOptions = Object.entries(auditBizTypeMap).map(([value, label]) => ({
value,
label,
}))
+30
View File
@@ -577,3 +577,33 @@ export interface AdminAnnouncementPayload {
is_important: boolean
is_popup: boolean
}
export interface AdminAuditQuery {
page?: number
page_size?: number
actor_id?: number
action?: string
biz_type?: string
}
export interface AdminAuditLog {
id: number
actor_type: string
actor_id: number
actor_username: string
actor_nickname: string
action: string
biz_type: string
biz_id?: number | null
ip: string
user_agent: string
detail?: unknown
created_at: string
}
export interface AdminAuditPage {
items: AdminAuditLog[]
total: number
page: number
page_size: number
}
+1 -1
View File
@@ -107,7 +107,7 @@ const allNavGroups: NavGroup[] = [
icon: <SettingOutlined />,
children: [
{ key: '/system-configs', label: '系统配置', permission: 'systemconfig:view' },
{ key: '/audit-logs', label: '审计日志', permission: 'audit:view' },
{ key: '/audit-logs', label: '审计日志', permission: 'audit_log:view' },
],
},
]
+20
View File
@@ -1469,6 +1469,26 @@ a {
word-break: break-word;
}
.admin-detail__pre--tall {
max-height: 440px;
}
.admin-audit-summary {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
.admin-audit-ua {
color: var(--ink-muted);
font-family: 'Inter', monospace;
font-size: 11px;
word-break: break-all;
}
.admin-detail-drawer .ant-drawer-header {
align-items: flex-start;
}