From 16df59ca9de118798481cf163eac1f11b1b15a5d Mon Sep 17 00:00:00 2001 From: Nick Zeng Date: Fri, 3 Apr 2026 11:18:23 +0800 Subject: [PATCH] update log page --- .../pages/logs/__tests__/operations.spec.ts | 102 ++++++++++++++++-- .../src/pages/logs/__tests__/requests.spec.ts | 32 ++++++ frontend/src/pages/logs/operations.vue | 22 +++- frontend/src/pages/logs/requests.vue | 8 +- frontend/src/stores/operation-log.ts | 39 +++++-- frontend/src/stores/request-log.ts | 2 +- 6 files changed, 178 insertions(+), 27 deletions(-) diff --git a/frontend/src/pages/logs/__tests__/operations.spec.ts b/frontend/src/pages/logs/__tests__/operations.spec.ts index c4110b4..398b2d9 100644 --- a/frontend/src/pages/logs/__tests__/operations.spec.ts +++ b/frontend/src/pages/logs/__tests__/operations.spec.ts @@ -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() diff --git a/frontend/src/pages/logs/__tests__/requests.spec.ts b/frontend/src/pages/logs/__tests__/requests.spec.ts index 94d6dce..5cbe16a 100644 --- a/frontend/src/pages/logs/__tests__/requests.spec.ts +++ b/frontend/src/pages/logs/__tests__/requests.spec.ts @@ -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() diff --git a/frontend/src/pages/logs/operations.vue b/frontend/src/pages/logs/operations.vue index c6a9cef..52b938e 100644 --- a/frontend/src/pages/logs/operations.vue +++ b/frontend/src/pages/logs/operations.vue @@ -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 }) { @@ -255,7 +267,7 @@ async function handleViewDetail(record: { id: number }) { {{ detail.user_id - ? (store.userMap.get(detail.user_id) || `#${detail.user_id}`) + ? (store.userMap.get(Number(detail.user_id)) || `#${detail.user_id}`) : '-' }} diff --git a/frontend/src/pages/logs/requests.vue b/frontend/src/pages/logs/requests.vue index e3767e7..364c157 100644 --- a/frontend/src/pages/logs/requests.vue +++ b/frontend/src/pages/logs/requests.vue @@ -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 }) {