update log page

This commit is contained in:
2026-04-03 11:18:23 +08:00
parent 2f5af0538e
commit 16df59ca9d
6 changed files with 178 additions and 27 deletions
@@ -83,6 +83,26 @@ const mockUsers = {
per_page: 200,
}
const mockRoles = {
items: [
{ id: 3, name: 'developer' },
{ id: 4, name: 'administrator' },
],
total: 2,
page: 1,
per_page: 200,
}
const mockRouteGroups = {
items: [
{ id: 10, name: 'default' },
{ id: 11, name: 'api-management' },
],
total: 2,
page: 1,
per_page: 200,
}
// ─── Store Tests ───────────────────────────────────────
describe('useOperationLogStore', () => {
@@ -192,27 +212,59 @@ describe('useOperationLogStore', () => {
})
})
describe('userMap', () => {
it('S6: builds userMap from loaded users', async () => {
describe('lookup maps', () => {
it('S6: builds all maps from loaded lookups', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.get).mockResolvedValueOnce(mockUsers)
vi.mocked(api.get)
.mockResolvedValueOnce(mockUsers)
.mockResolvedValueOnce(mockRoles)
.mockResolvedValueOnce(mockRouteGroups)
const store = useOperationLogStore()
await store.loadLookups()
expect(store.userMap.get(1)).toBe('admin')
expect(store.userMap.get(2)).toBe('editor')
expect(store.roleMap.get(3)).toBe('developer')
expect(store.roleMap.get(4)).toBe('administrator')
expect(store.routeGroupMap.get(10)).toBe('default')
expect(store.routeGroupMap.get(11)).toBe('api-management')
})
it('S7: handles loadLookups failure gracefully', async () => {
it('S6a: userMap handles string id from API via Number() defense', () => {
const store = useOperationLogStore()
store.users = [{ id: '1' as any, username: 'admin' }] as any
expect(store.userMap.get(1)).toBe('admin')
})
it('S7: handles all lookups failure gracefully', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.get).mockRejectedValueOnce(new Error('加载失败'))
vi.mocked(api.get)
.mockRejectedValueOnce(new Error('用户加载失败'))
.mockRejectedValueOnce(new Error('角色加载失败'))
.mockRejectedValueOnce(new Error('路由组加载失败'))
const store = useOperationLogStore()
await store.loadLookups()
expect(store.users).toEqual([])
expect(store.userMap.size).toBe(0)
expect(store.roleMap.size).toBe(0)
expect(store.routeGroupMap.size).toBe(0)
})
it('S7a: partial lookup failure does not affect other maps', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.get)
.mockResolvedValueOnce(mockUsers)
.mockRejectedValueOnce(new Error('角色加载失败'))
.mockResolvedValueOnce(mockRouteGroups)
const store = useOperationLogStore()
await store.loadLookups()
expect(store.userMap.get(1)).toBe('admin')
expect(store.roleMap.size).toBe(0)
expect(store.routeGroupMap.get(10)).toBe('default')
})
})
})
@@ -237,6 +289,8 @@ describe('OperationLogPage', () => {
const { api } = await import('@/utils/request')
vi.mocked(api.get).mockImplementation((url: string) => {
if (url === '/api/v1/users') return Promise.resolve(mockUsers)
if (url === '/api/v1/roles') return Promise.resolve(mockRoles)
if (url === '/api/v1/route-groups') return Promise.resolve(mockRouteGroups)
if (url === '/api/v1/logs/operations') {
return Promise.resolve({
items: mockLogs,
@@ -293,11 +347,14 @@ describe('OperationLogPage', () => {
expect(tagTexts).toContain('order.delete')
})
it('P5: displays target as type + id', async () => {
it('P5: displays target with name for small datasets, #id for large', async () => {
await mountPage()
const html = wrapper.html()
// user id=5 not in mockUsers, so fallback to #id
expect(html).toContain('用户 #5')
expect(html).toContain('角色 #3')
// role id=3 is in mockRoles as 'developer'
expect(html).toContain('角色 developer')
// order is large dataset, always #id
expect(html).toContain('订单 #100')
})
@@ -358,6 +415,35 @@ describe('OperationLogPage', () => {
expect(api.get).toHaveBeenCalled()
})
it('P9a: renders operator username instead of #id', async () => {
await mountPage()
const html = wrapper.html()
expect(html).toContain('admin')
expect(html).not.toMatch(/#1(?![\d])/)
})
it('P9b: renders role name in target column', async () => {
await mountPage()
const html = wrapper.html()
// mockLogs[1] has target_type='role', target_id=3, which maps to 'developer'
expect(html).toContain('角色 developer')
expect(html).not.toContain('角色 #3')
})
it('P9c: preserves #id for large dataset targets', async () => {
await mountPage()
const html = wrapper.html()
// mockLogs[2] has target_type='order', target_id=100 — large dataset, keep #id
expect(html).toContain('订单 #100')
})
it('P9d: calls all three lookup APIs on mount', async () => {
const { api } = await mountPage()
expect(api.get).toHaveBeenCalledWith('/api/v1/users', { per_page: 200 })
expect(api.get).toHaveBeenCalledWith('/api/v1/roles', { per_page: 200 })
expect(api.get).toHaveBeenCalledWith('/api/v1/route-groups', { per_page: 200 })
})
it('P9: view button opens drawer and loads detail', async () => {
const { api } = await mountPage()
@@ -214,6 +214,19 @@ describe('useRequestLogStore', () => {
expect(store.userMap.get(2)).toBe('editor')
})
it('S7a: userMap handles string id from API via Number() defense', async () => {
const store = useRequestLogStore()
// Simulate API returning string-typed ids (the actual bug scenario)
store.users = [
{ id: '1' as any, username: 'admin' },
{ id: '2' as any, username: 'editor' },
] as any
// Number() defense in store ensures Map keys are numbers
expect(store.userMap.get(1)).toBe('admin')
expect(store.userMap.get(2)).toBe('editor')
})
it('S7: handles loadLookups failure gracefully', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.get).mockRejectedValueOnce(new Error('加载失败'))
@@ -369,6 +382,25 @@ describe('RequestLogPage', () => {
expect(api.get).toHaveBeenCalled()
})
it('P9a: renders username instead of #id in user column', async () => {
await mountPage()
const html = wrapper.html()
// user_id=1 should render as 'admin', not '#1'
expect(html).toContain('admin')
expect(html).not.toMatch(/#1(?![\d])/)
// user_id=2 should render as 'editor', not '#2'
expect(html).toContain('editor')
})
it('P9b: renders dash for null user_id', async () => {
await mountPage()
// mockLogs[1] has user_id: null, should render '-'
// The table should contain '-' for the null user_id row
const cells = wrapper.findAll('td')
const userCells = cells.filter((c) => c.text() === '-')
expect(userCells.length).toBeGreaterThan(0)
})
it('P9: view button opens drawer and loads detail', async () => {
const { api } = await mountPage()
+17 -5
View File
@@ -63,8 +63,8 @@ const createdAtRange = computed({
},
})
onMounted(() => {
store.loadLookups()
onMounted(async () => {
await store.loadLookups()
store.fetchLogs()
})
@@ -89,8 +89,20 @@ function formatTime(time: string | null) {
return time.replace('T', ' ').substring(0, 16)
}
function formatTarget(type: string, id: number) {
function formatTarget(type: string, id: number): string {
const label = targetTypeLabels[type] || type
if (type === 'user') {
const name = store.userMap.get(Number(id))
return name ? `${label} ${name}` : `${label} #${id}`
}
if (type === 'role') {
const name = store.roleMap.get(Number(id))
return name ? `${label} ${name}` : `${label} #${id}`
}
if (type === 'route_group') {
const name = store.routeGroupMap.get(Number(id))
return name ? `${label} ${name}` : `${label} #${id}`
}
return `${label} #${id}`
}
@@ -198,7 +210,7 @@ async function handleViewDetail(record: { id: number }) {
<template v-else-if="column.key === 'user_id'">
{{
record.user_id
? (store.userMap.get(record.user_id) || `#${record.user_id}`)
? (store.userMap.get(Number(record.user_id)) || `#${record.user_id}`)
: '-'
}}
</template>
@@ -255,7 +267,7 @@ async function handleViewDetail(record: { id: number }) {
<a-descriptions-item label="操作人">
{{
detail.user_id
? (store.userMap.get(detail.user_id) || `#${detail.user_id}`)
? (store.userMap.get(Number(detail.user_id)) || `#${detail.user_id}`)
: '-'
}}
</a-descriptions-item>
+4 -4
View File
@@ -72,8 +72,8 @@ const createdAtRange = computed({
},
})
onMounted(() => {
store.loadLookups()
onMounted(async () => {
await store.loadLookups()
store.fetchLogs()
})
@@ -214,7 +214,7 @@ async function handleViewDetail(record: { id: number }) {
</a-tag>
</template>
<template v-else-if="column.key === 'user_id'">
{{ record.user_id ? (store.userMap.get(record.user_id) || `#${record.user_id}`) : '-' }}
{{ record.user_id ? (store.userMap.get(Number(record.user_id)) || `#${record.user_id}`) : '-' }}
</template>
<template v-else-if="column.key === 'duration_ms'">
<span :class="durationClass(record.duration_ms)">
@@ -274,7 +274,7 @@ async function handleViewDetail(record: { id: number }) {
<a-descriptions-item label="用户">
{{
detail.user_id
? (store.userMap.get(detail.user_id) || `#${detail.user_id}`)
? (store.userMap.get(Number(detail.user_id)) || `#${detail.user_id}`)
: '-'
}}
</a-descriptions-item>
+30 -9
View File
@@ -12,6 +12,16 @@ interface UserLookup {
username: string
}
interface RoleLookup {
id: number
name: string
}
interface RouteGroupLookup {
id: number
name: string
}
export const useOperationLogStore = defineStore('operationLog', () => {
const logs = ref<OperationLogRecord[]>([])
const loading = ref(false)
@@ -28,19 +38,28 @@ export const useOperationLogStore = defineStore('operationLog', () => {
})
const users = ref<UserLookup[]>([])
const roles = ref<RoleLookup[]>([])
const routeGroups = ref<RouteGroupLookup[]>([])
const userMap = computed(
() => new Map(users.value.map((u) => [u.id, u.username])),
() => new Map(users.value.map((u) => [Number(u.id), u.username])),
)
const roleMap = computed(
() => new Map(roles.value.map((r) => [Number(r.id), r.name])),
)
const routeGroupMap = computed(
() => new Map(routeGroups.value.map((rg) => [Number(rg.id), rg.name])),
)
async function loadLookups() {
try {
const data = await api.get<PaginatedData<UserLookup>>('/api/v1/users', {
per_page: 200,
})
users.value = data.items
} catch (err: unknown) {
console.warn('加载用户列表失败', err)
}
const [userData, roleData, rgData] = await Promise.allSettled([
api.get<PaginatedData<UserLookup>>('/api/v1/users', { per_page: 200 }),
api.get<PaginatedData<RoleLookup>>('/api/v1/roles', { per_page: 200 }),
api.get<PaginatedData<RouteGroupLookup>>('/api/v1/route-groups', { per_page: 200 }),
])
if (userData.status === 'fulfilled') users.value = userData.value.items
if (roleData.status === 'fulfilled') roles.value = roleData.value.items
if (rgData.status === 'fulfilled') routeGroups.value = rgData.value.items
}
async function fetchLogs() {
@@ -86,6 +105,8 @@ export const useOperationLogStore = defineStore('operationLog', () => {
filters,
users,
userMap,
roleMap,
routeGroupMap,
loadLookups,
fetchLogs,
resetFilters,
+1 -1
View File
@@ -30,7 +30,7 @@ export const useRequestLogStore = defineStore('requestLog', () => {
const users = ref<UserLookup[]>([])
const userMap = computed(
() => new Map(users.value.map((u) => [u.id, u.username])),
() => new Map(users.value.map((u) => [Number(u.id), u.username])),
)
async function loadLookups() {