优化
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { getPlatformAuditLog, listPlatformAuditLogs } from './platformAudit'
|
||||
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
http: { get: vi.fn() },
|
||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
||||
}))
|
||||
|
||||
describe('platform audit api', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('queries the selected customer and preserves long ids as strings', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ data: { records: [{ id: '2082813911786024961', accountId: '8', operatorTenantId: '0', userId: '9', resourceId: '8', eventId: 'evt', eventType: 'PLATFORM_TENANT_UPDATE', resourceType: 'TENANT', description: '编辑客户', username: 'admin', appCode: 'ADMIN', clientType: 'WEB', requestId: null, traceId: null, serviceName: 'iam-service', success: true, errorCode: null, errorMessage: null, occurredAt: '2026-08-13T01:00:00Z', createdAt: '2026-08-13T09:00:00' }], page: 1, size: 20, total: 1, pages: 1 } })
|
||||
|
||||
const result = await listPlatformAuditLogs({ accountId: '8', page: 1, size: 20 })
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/audit/logs', { params: { accountId: '8', page: 1, size: 20 } })
|
||||
expect(result.records[0].id).toBe('2082813911786024961')
|
||||
})
|
||||
|
||||
it('maps audit filters and omits empty query values', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ data: { records: [], page: 1, size: 20, total: 0, pages: 0 } })
|
||||
|
||||
await listPlatformAuditLogs({
|
||||
accountId: '8', page: 1, size: 20, keyword: 'admin', success: false,
|
||||
occurredFrom: '2026-08-01T00:00:00.000Z', occurredTo: '2026-08-13T23:59:59.999Z',
|
||||
eventType: '', resourceType: undefined,
|
||||
})
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/audit/logs', { params: {
|
||||
accountId: '8', page: 1, size: 20, keyword: 'admin', success: false,
|
||||
occurredFrom: '2026-08-01T00:00:00.000Z', occurredTo: '2026-08-13T23:59:59.999Z',
|
||||
} })
|
||||
})
|
||||
|
||||
it('loads one audit detail', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ data: { id: '1', accountId: '8', operatorTenantId: '0', userId: '9', resourceId: '8', eventId: 'evt', eventType: 'PLATFORM_TENANT_UPDATE', resourceType: 'TENANT', description: '编辑客户', username: 'admin', appCode: 'ADMIN', clientType: 'WEB', requestId: null, traceId: null, serviceName: 'iam-service', success: true, errorCode: null, errorMessage: null, occurredAt: '2026-08-13T01:00:00Z', createdAt: '2026-08-13T09:00:00', httpMethod: 'PUT', requestUri: '/tenants/8', clientIp: '127.0.0.1', durationMs: 12, beforeData: '{}', afterData: '{"name":"new"}' } })
|
||||
|
||||
const result = await getPlatformAuditLog('1')
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/audit/logs/1')
|
||||
expect(result.afterData).toBe('{"name":"new"}')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export interface PlatformAuditLogListItem {
|
||||
id: string
|
||||
eventId: string
|
||||
eventType: string
|
||||
resourceType: string
|
||||
resourceId: string | null
|
||||
description: string | null
|
||||
accountId: string
|
||||
operatorTenantId: string
|
||||
userId: string
|
||||
username: string | null
|
||||
appCode: string | null
|
||||
clientType: string | null
|
||||
requestId: string | null
|
||||
traceId: string | null
|
||||
serviceName: string | null
|
||||
success: boolean
|
||||
errorCode: string | null
|
||||
errorMessage: string | null
|
||||
occurredAt: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface PlatformAuditLogDetail extends PlatformAuditLogListItem {
|
||||
httpMethod: string | null
|
||||
requestUri: string | null
|
||||
clientIp: string | null
|
||||
durationMs: number | null
|
||||
beforeData: string | null
|
||||
afterData: string | null
|
||||
}
|
||||
|
||||
export interface PlatformAuditLogPage {
|
||||
records: PlatformAuditLogListItem[]
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
pages: number
|
||||
}
|
||||
|
||||
export interface PlatformAuditLogQuery {
|
||||
accountId: string
|
||||
page?: number
|
||||
size?: number
|
||||
operatorUserId?: string
|
||||
eventType?: string
|
||||
resourceType?: string
|
||||
resourceId?: string
|
||||
success?: boolean
|
||||
occurredFrom?: string
|
||||
occurredTo?: string
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
type RawId = string | number | null
|
||||
type RawListItem = Omit<PlatformAuditLogListItem, 'id' | 'accountId' | 'operatorTenantId' | 'userId' | 'resourceId'> & {
|
||||
id: RawId
|
||||
accountId: RawId
|
||||
operatorTenantId: RawId
|
||||
userId: RawId
|
||||
resourceId: RawId
|
||||
}
|
||||
type RawDetail = Omit<PlatformAuditLogDetail, keyof PlatformAuditLogListItem> & RawListItem
|
||||
type RawPage = Omit<PlatformAuditLogPage, 'records' | 'page' | 'size' | 'total' | 'pages'> & {
|
||||
records?: RawListItem[]
|
||||
page: number | string
|
||||
size: number | string
|
||||
total: number | string
|
||||
pages?: number | string
|
||||
}
|
||||
|
||||
const basePath = '/platform/v1/audit/logs'
|
||||
const id = (value: RawId) => value == null ? '' : String(value)
|
||||
|
||||
function normalizeListItem(item: RawListItem): PlatformAuditLogListItem {
|
||||
return {
|
||||
...item,
|
||||
id: id(item.id),
|
||||
accountId: id(item.accountId),
|
||||
operatorTenantId: id(item.operatorTenantId),
|
||||
userId: id(item.userId),
|
||||
resourceId: item.resourceId == null ? null : String(item.resourceId),
|
||||
}
|
||||
}
|
||||
|
||||
export async function listPlatformAuditLogs(query: PlatformAuditLogQuery) {
|
||||
const params = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== '' && value !== null && value !== undefined))
|
||||
const response = await http.get<ApiEnvelope<RawPage>, ApiEnvelope<RawPage>>(basePath, { params })
|
||||
const data = unwrapApiResponse(response)
|
||||
const size = Number(data.size || query.size || 20)
|
||||
const total = Number(data.total || 0)
|
||||
return {
|
||||
records: (data.records || []).map(normalizeListItem),
|
||||
page: Number(data.page || query.page || 1),
|
||||
size,
|
||||
total,
|
||||
pages: Number(data.pages ?? (total > 0 ? Math.ceil(total / size) : 0)),
|
||||
} satisfies PlatformAuditLogPage
|
||||
}
|
||||
|
||||
export async function getPlatformAuditLog(idValue: string) {
|
||||
const response = await http.get<ApiEnvelope<RawDetail>, ApiEnvelope<RawDetail>>(`${basePath}/${encodeURIComponent(idValue)}`)
|
||||
return normalizeListItem(unwrapApiResponse(response)) as PlatformAuditLogDetail
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
|
||||
import { getPlatformAuditLog, listPlatformAuditLogs } from '../api/platformAudit'
|
||||
import CustomerAuditLogsPanel from './CustomerAuditLogsPanel.vue'
|
||||
|
||||
vi.mock('../api/platformAudit', () => ({ getPlatformAuditLog: vi.fn(), listPlatformAuditLogs: vi.fn() }))
|
||||
|
||||
const row = { id: '1', eventId: 'evt', eventType: 'PLATFORM_TENANT_UPDATE', resourceType: 'TENANT', resourceId: '8', description: '编辑客户', accountId: '8', operatorTenantId: '0', userId: '9', username: 'platform_admin', appCode: 'ADMIN', clientType: 'WEB', requestId: 'req', traceId: 'trace', serviceName: 'iam-service', success: true, errorCode: null, errorMessage: null, occurredAt: '2026-08-13T01:00:00Z', createdAt: '2026-08-13T09:00:00' }
|
||||
|
||||
function mountPanel(permissions: string[]) {
|
||||
const pinia = createPinia(); setActivePinia(pinia)
|
||||
const auth = useAuthStore(); auth.token = 'token'; auth.permissions = permissions
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
return mount(CustomerAuditLogsPanel, { props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [pinia, ElementPlus] } })
|
||||
}
|
||||
|
||||
describe('CustomerAuditLogsPanel', () => {
|
||||
beforeEach(() => { vi.clearAllMocks(); vi.mocked(listPlatformAuditLogs).mockResolvedValue({ records: [row], page: 1, size: 20, total: 1, pages: 1 }) })
|
||||
|
||||
it('queries logs with the current customer id', async () => {
|
||||
const wrapper = mountPanel(['platform:audit:list']); await flushPromises()
|
||||
expect(listPlatformAuditLogs).toHaveBeenCalledWith({ accountId: '8', page: 1, size: 20 })
|
||||
expect(wrapper.text()).toContain('编辑客户')
|
||||
})
|
||||
|
||||
it('applies keyword, result and local date range only after search', async () => {
|
||||
const wrapper = mountPanel(['platform:audit:list']); await flushPromises()
|
||||
vi.mocked(listPlatformAuditLogs).mockClear()
|
||||
const toolbar = wrapper.findComponent(QueryToolbar)
|
||||
toolbar.vm.$emit('update:modelValue', { keyword: ' admin ', success: false, occurredRange: ['2026-08-01', '2026-08-13'] })
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(listPlatformAuditLogs).not.toHaveBeenCalled()
|
||||
|
||||
toolbar.vm.$emit('search', { keyword: ' admin ', success: false, occurredRange: ['2026-08-01', '2026-08-13'] })
|
||||
await flushPromises()
|
||||
|
||||
expect(listPlatformAuditLogs).toHaveBeenCalledTimes(1)
|
||||
expect(listPlatformAuditLogs).toHaveBeenCalledWith({
|
||||
accountId: '8', page: 1, size: 20, keyword: 'admin', success: false,
|
||||
occurredFrom: new Date(2026, 7, 1, 0, 0, 0, 0).toISOString(),
|
||||
occurredTo: new Date(2026, 7, 13, 23, 59, 59, 999).toISOString(),
|
||||
})
|
||||
})
|
||||
|
||||
it('resets to all logs and requests once on the first page', async () => {
|
||||
const wrapper = mountPanel(['platform:audit:list']); await flushPromises()
|
||||
const toolbar = wrapper.findComponent(QueryToolbar)
|
||||
toolbar.vm.$emit('update:modelValue', { keyword: 'admin', success: true, occurredRange: ['2026-08-01', '2026-08-13'] })
|
||||
toolbar.vm.$emit('search', { keyword: 'admin', success: true, occurredRange: ['2026-08-01', '2026-08-13'] })
|
||||
await flushPromises()
|
||||
vi.mocked(listPlatformAuditLogs).mockClear()
|
||||
|
||||
;(toolbar.vm as unknown as { reset: () => void }).reset()
|
||||
await flushPromises()
|
||||
|
||||
expect(listPlatformAuditLogs).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.findComponent(QueryToolbar).props('modelValue')).toEqual({ keyword: '', success: '', occurredRange: [] })
|
||||
expect(listPlatformAuditLogs).toHaveBeenCalledWith({ accountId: '8', page: 1, size: 20, keyword: undefined, success: undefined, occurredFrom: undefined, occurredTo: undefined })
|
||||
})
|
||||
|
||||
it('does not query without audit permission', async () => {
|
||||
const wrapper = mountPanel([]); await flushPromises()
|
||||
expect(listPlatformAuditLogs).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('暂无操作日志查看权限')
|
||||
})
|
||||
|
||||
it('loads detail and formats json snapshots', async () => {
|
||||
vi.mocked(getPlatformAuditLog).mockResolvedValue({ ...row, httpMethod: 'PUT', requestUri: '/tenants/8', clientIp: '127.0.0.1', durationMs: 12, beforeData: '{"name":"旧名称"}', afterData: '{"name":"新名称"}' })
|
||||
const wrapper = mountPanel(['platform:audit:list']); await flushPromises()
|
||||
await wrapper.get('button[aria-label="查看操作日志1详情"]').trigger('click'); await flushPromises()
|
||||
expect(getPlatformAuditLog).toHaveBeenCalledWith('1')
|
||||
expect(wrapper.text()).toContain('旧名称')
|
||||
expect(wrapper.text()).toContain('新名称')
|
||||
})
|
||||
|
||||
it('loads the selected audit page', async () => {
|
||||
const wrapper = mountPanel(['platform:audit:list']); await flushPromises()
|
||||
wrapper.findComponent(QueryToolbar).vm.$emit('search', { keyword: 'tenant', success: true, occurredRange: [] })
|
||||
await flushPromises()
|
||||
vi.mocked(listPlatformAuditLogs).mockClear()
|
||||
|
||||
;(wrapper.vm as unknown as { page: number }).page = 2
|
||||
await flushPromises()
|
||||
|
||||
expect(listPlatformAuditLogs).toHaveBeenCalledWith({ accountId: '8', page: 2, size: 20, keyword: 'tenant', success: true, occurredFrom: undefined, occurredTo: undefined })
|
||||
})
|
||||
|
||||
it('clears draft and applied filters when switching customers', async () => {
|
||||
const wrapper = mountPanel(['platform:audit:list']); await flushPromises()
|
||||
const toolbar = wrapper.findComponent(QueryToolbar)
|
||||
toolbar.vm.$emit('update:modelValue', { keyword: 'draft', success: false, occurredRange: [] })
|
||||
toolbar.vm.$emit('search', { keyword: 'applied', success: true, occurredRange: [] })
|
||||
await flushPromises()
|
||||
vi.mocked(listPlatformAuditLogs).mockClear()
|
||||
|
||||
await wrapper.setProps({ customerId: '9' })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.findComponent(QueryToolbar).props('modelValue')).toEqual({ keyword: '', success: '', occurredRange: [] })
|
||||
expect(listPlatformAuditLogs).toHaveBeenCalledTimes(1)
|
||||
expect(listPlatformAuditLogs).toHaveBeenCalledWith({ accountId: '9', page: 1, size: 20, keyword: undefined, success: undefined, occurredFrom: undefined, occurredTo: undefined })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Eye, ScrollText } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDrawer from '@/shared/components/AppDrawer.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { getPlatformAuditLog, listPlatformAuditLogs, type PlatformAuditLogDetail, type PlatformAuditLogListItem, type PlatformAuditLogQuery } from '../api/platformAudit'
|
||||
|
||||
const props = defineProps<{ customerId: string; customerName: string }>()
|
||||
const access = useAccessPreviewStore()
|
||||
const rows = ref<PlatformAuditLogListItem[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const detailOpen = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detail = ref<PlatformAuditLogDetail | null>(null)
|
||||
const detailError = ref<ApiErrorDetails | null>(null)
|
||||
const emptyFilters = (): QueryModel => ({ keyword: '', success: '', occurredRange: [] })
|
||||
const filters = ref<QueryModel>(emptyFilters())
|
||||
const appliedFilters = ref<QueryModel>(emptyFilters())
|
||||
let requestSerial = 0
|
||||
|
||||
const queryFields: QueryField[] = [
|
||||
{ key: 'keyword', type: 'input', label: '关键词', width: 260, placeholder: '操作内容/操作人/资源或请求标识' },
|
||||
{ key: 'success', type: 'select', label: '结果', width: 150, options: [{ label: '成功', value: true }, { label: '失败', value: false }] },
|
||||
{ key: 'occurredRange', type: 'daterange', label: '发生日期', width: 280 },
|
||||
]
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 92, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'description', label: '操作内容', minWidth: 190, slotName: 'description' },
|
||||
{ key: 'eventType', prop: 'eventType', label: '事件类型', minWidth: 220 },
|
||||
{ key: 'username', label: '操作人', width: 140, slotName: 'username' },
|
||||
{ key: 'serviceName', label: '来源服务', width: 150, slotName: 'serviceName' },
|
||||
{ key: 'success', label: '结果', width: 90, slotName: 'success' },
|
||||
{ key: 'occurredAt', label: '发生时间', width: 170, slotName: 'occurredAt' },
|
||||
]
|
||||
|
||||
const display = (value?: string | null) => value?.trim() || '—'
|
||||
const formatDate = (value?: string | null) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—'
|
||||
function formatSnapshot(value?: string | null) {
|
||||
if (!value?.trim()) return '无'
|
||||
try { return JSON.stringify(JSON.parse(value), null, 2) }
|
||||
catch { return value }
|
||||
}
|
||||
|
||||
function localDayIso(value: string, end: boolean) {
|
||||
const [year, month, day] = value.split('-').map(Number)
|
||||
if (!year || !month || !day) return undefined
|
||||
return new Date(year, month - 1, day, end ? 23 : 0, end ? 59 : 0, end ? 59 : 0, end ? 999 : 0).toISOString()
|
||||
}
|
||||
|
||||
function toQuery(customerId: string): PlatformAuditLogQuery {
|
||||
const range = Array.isArray(appliedFilters.value.occurredRange) ? appliedFilters.value.occurredRange.map(String) : []
|
||||
const success = appliedFilters.value.success
|
||||
return {
|
||||
accountId: customerId,
|
||||
page: page.value,
|
||||
size: pageSize.value,
|
||||
keyword: String(appliedFilters.value.keyword || '').trim() || undefined,
|
||||
success: typeof success === 'boolean' ? success : undefined,
|
||||
occurredFrom: range[0] ? localDayIso(range[0], false) : undefined,
|
||||
occurredTo: range[1] ? localDayIso(range[1], true) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const customerId = props.customerId.trim()
|
||||
const serial = ++requestSerial
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
loadError.value = null
|
||||
if (!customerId || !access.can('platform:audit:list')) return
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await listPlatformAuditLogs(toQuery(customerId))
|
||||
if (serial !== requestSerial || customerId !== props.customerId.trim()) return
|
||||
rows.value = result.records
|
||||
total.value = result.total
|
||||
} catch (error) {
|
||||
if (serial === requestSerial) loadError.value = getApiErrorDetails(error, '客户操作日志加载失败,请稍后重试')
|
||||
} finally {
|
||||
if (serial === requestSerial) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters(model: QueryModel) {
|
||||
appliedFilters.value = {
|
||||
keyword: String(model.keyword || '').trim(),
|
||||
success: typeof model.success === 'boolean' ? model.success : '',
|
||||
occurredRange: Array.isArray(model.occurredRange) ? model.occurredRange.map(String) : [],
|
||||
}
|
||||
if (page.value === 1) void load()
|
||||
else page.value = 1
|
||||
}
|
||||
|
||||
async function openDetail(row: PlatformAuditLogListItem) {
|
||||
detailOpen.value = true
|
||||
detailLoading.value = true
|
||||
detail.value = null
|
||||
detailError.value = null
|
||||
try { detail.value = await getPlatformAuditLog(row.id) }
|
||||
catch (error) { detailError.value = getApiErrorDetails(error, '操作日志详情加载失败,请稍后重试') }
|
||||
finally { detailLoading.value = false }
|
||||
}
|
||||
|
||||
watch(() => props.customerId, () => {
|
||||
filters.value = emptyFilters()
|
||||
appliedFilters.value = emptyFilters()
|
||||
if (page.value === 1) void load()
|
||||
else page.value = 1
|
||||
}, { immediate: true })
|
||||
watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) => {
|
||||
if (nextPage !== previousPage || nextSize !== previousSize) void load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="customer-audit-panel">
|
||||
<div class="panel-title"><div><h2>操作日志</h2><p>{{ customerName }}已归属当前客户的档案与业务操作记录</p></div><ScrollText :size="19" /></div>
|
||||
<div v-if="!access.can('platform:audit:list')" class="module-empty"><ScrollText :size="30" /><strong>暂无操作日志查看权限</strong><p>请联系管理员开通平台操作日志查看权限。</p></div>
|
||||
<template v-else>
|
||||
<QueryToolbar v-model="filters" class="audit-query" ariaLabel="客户操作日志筛选" :fields="queryFields" preference-key="customer-audit-logs-query" @search="applyFilters" />
|
||||
<LoadErrorState v-if="loadError" title="客户操作日志加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
<DataTable v-else v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" row-key="id" aria-label="客户操作日志列表" empty-text="当前客户暂无操作日志" preference-key="customer-audit-logs" border stripe>
|
||||
<template #actions="{ row }"><el-button link type="primary" :aria-label="`查看操作日志${(row as PlatformAuditLogListItem).id}详情`" @click="openDetail(row as PlatformAuditLogListItem)"><Eye :size="15" />详情</el-button></template>
|
||||
<template #description="{ row }">{{ display((row as PlatformAuditLogListItem).description) }}</template>
|
||||
<template #username="{ row }">{{ display((row as PlatformAuditLogListItem).username) }}</template>
|
||||
<template #serviceName="{ row }">{{ display((row as PlatformAuditLogListItem).serviceName) }}</template>
|
||||
<template #success="{ row }"><el-tag :type="(row as PlatformAuditLogListItem).success ? 'success' : 'danger'">{{ (row as PlatformAuditLogListItem).success ? '成功' : '失败' }}</el-tag></template>
|
||||
<template #occurredAt="{ row }">{{ formatDate((row as PlatformAuditLogListItem).occurredAt) }}</template>
|
||||
<template #pagination-left>共 {{ total }} 条客户操作记录</template>
|
||||
</DataTable>
|
||||
</template>
|
||||
|
||||
<AppDrawer v-model="detailOpen" title="操作日志详情" size="min(860px, 94vw)" :loading="detailLoading" :show-footer="false">
|
||||
<LoadErrorState v-if="detailError" title="详情加载失败" :message="detailError.message" :loading="detailLoading"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
|
||||
<template v-else-if="detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="操作内容">{{ display(detail.description) }}</el-descriptions-item><el-descriptions-item label="结果">{{ detail.success ? '成功' : '失败' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">{{ display(detail.username) }}</el-descriptions-item><el-descriptions-item label="发生时间">{{ formatDate(detail.occurredAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="事件类型">{{ detail.eventType }}</el-descriptions-item><el-descriptions-item label="资源">{{ detail.resourceType }} / {{ display(detail.resourceId) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求">{{ display(detail.httpMethod) }} {{ display(detail.requestUri) }}</el-descriptions-item><el-descriptions-item label="耗时">{{ detail.durationMs == null ? '—' : `${detail.durationMs} ms` }}</el-descriptions-item>
|
||||
<el-descriptions-item label="来源服务">{{ display(detail.serviceName) }}</el-descriptions-item><el-descriptions-item label="请求标识">{{ display(detail.requestId) }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="!detail.success" label="失败原因" :span="2">{{ display(detail.errorMessage || detail.errorCode) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="snapshot-grid"><section><h3>操作前快照</h3><pre>{{ formatSnapshot(detail.beforeData) }}</pre></section><section><h3>操作结果快照</h3><pre>{{ formatSnapshot(detail.afterData) }}</pre></section></div>
|
||||
</template>
|
||||
</AppDrawer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.customer-audit-panel { min-width:0; }.panel-title { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; margin-bottom:18px; padding-bottom:14px; border-bottom:1px solid var(--cc-border); }.panel-title h2 { margin:0; color:#344054; font-size:16px; }.panel-title p { margin:5px 0 0; color:var(--cc-muted); font-size:11px; }.panel-title > svg { color:var(--cc-primary); }.audit-query { margin-bottom:16px; }.module-empty { display:flex; min-height:260px; align-items:center; justify-content:center; flex-direction:column; gap:7px; color:#98a2b3; text-align:center; }.module-empty strong { color:#475467; font-size:13px; }.module-empty p { margin:0; color:var(--cc-muted); font-size:12px; }.snapshot-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; margin-top:18px; }.snapshot-grid section { min-width:0; }.snapshot-grid h3 { margin:0 0 8px; color:#475467; font-size:14px; }.snapshot-grid pre { min-height:160px; margin:0; padding:14px; overflow:auto; border:1px solid var(--cc-border); border-radius:6px; background:#f8fafc; color:#344054; font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace; white-space:pre-wrap; overflow-wrap:anywhere; }@media(max-width:720px){.snapshot-grid{grid-template-columns:1fr}}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as CustomerAuditLogsPanel } from './components/CustomerAuditLogsPanel.vue'
|
||||
Reference in New Issue
Block a user