update log page
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user