update log page
This commit is contained in:
@@ -83,6 +83,26 @@ const mockUsers = {
|
|||||||
per_page: 200,
|
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 ───────────────────────────────────────
|
// ─── Store Tests ───────────────────────────────────────
|
||||||
|
|
||||||
describe('useOperationLogStore', () => {
|
describe('useOperationLogStore', () => {
|
||||||
@@ -192,27 +212,59 @@ describe('useOperationLogStore', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('userMap', () => {
|
describe('lookup maps', () => {
|
||||||
it('S6: builds userMap from loaded users', async () => {
|
it('S6: builds all maps from loaded lookups', async () => {
|
||||||
const { api } = await import('@/utils/request')
|
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()
|
const store = useOperationLogStore()
|
||||||
await store.loadLookups()
|
await store.loadLookups()
|
||||||
|
|
||||||
expect(store.userMap.get(1)).toBe('admin')
|
expect(store.userMap.get(1)).toBe('admin')
|
||||||
expect(store.userMap.get(2)).toBe('editor')
|
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')
|
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()
|
const store = useOperationLogStore()
|
||||||
await store.loadLookups()
|
await store.loadLookups()
|
||||||
|
|
||||||
expect(store.users).toEqual([])
|
|
||||||
expect(store.userMap.size).toBe(0)
|
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')
|
const { api } = await import('@/utils/request')
|
||||||
vi.mocked(api.get).mockImplementation((url: string) => {
|
vi.mocked(api.get).mockImplementation((url: string) => {
|
||||||
if (url === '/api/v1/users') return Promise.resolve(mockUsers)
|
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') {
|
if (url === '/api/v1/logs/operations') {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
items: mockLogs,
|
items: mockLogs,
|
||||||
@@ -293,11 +347,14 @@ describe('OperationLogPage', () => {
|
|||||||
expect(tagTexts).toContain('order.delete')
|
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()
|
await mountPage()
|
||||||
const html = wrapper.html()
|
const html = wrapper.html()
|
||||||
|
// user id=5 not in mockUsers, so fallback to #id
|
||||||
expect(html).toContain('用户 #5')
|
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')
|
expect(html).toContain('订单 #100')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -358,6 +415,35 @@ describe('OperationLogPage', () => {
|
|||||||
expect(api.get).toHaveBeenCalled()
|
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 () => {
|
it('P9: view button opens drawer and loads detail', async () => {
|
||||||
const { api } = await mountPage()
|
const { api } = await mountPage()
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,19 @@ describe('useRequestLogStore', () => {
|
|||||||
expect(store.userMap.get(2)).toBe('editor')
|
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 () => {
|
it('S7: handles loadLookups failure gracefully', async () => {
|
||||||
const { api } = await import('@/utils/request')
|
const { api } = await import('@/utils/request')
|
||||||
vi.mocked(api.get).mockRejectedValueOnce(new Error('加载失败'))
|
vi.mocked(api.get).mockRejectedValueOnce(new Error('加载失败'))
|
||||||
@@ -369,6 +382,25 @@ describe('RequestLogPage', () => {
|
|||||||
expect(api.get).toHaveBeenCalled()
|
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 () => {
|
it('P9: view button opens drawer and loads detail', async () => {
|
||||||
const { api } = await mountPage()
|
const { api } = await mountPage()
|
||||||
|
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ const createdAtRange = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
store.loadLookups()
|
await store.loadLookups()
|
||||||
store.fetchLogs()
|
store.fetchLogs()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -89,8 +89,20 @@ function formatTime(time: string | null) {
|
|||||||
return time.replace('T', ' ').substring(0, 16)
|
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
|
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}`
|
return `${label} #${id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +210,7 @@ async function handleViewDetail(record: { id: number }) {
|
|||||||
<template v-else-if="column.key === 'user_id'">
|
<template v-else-if="column.key === 'user_id'">
|
||||||
{{
|
{{
|
||||||
record.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>
|
</template>
|
||||||
@@ -255,7 +267,7 @@ async function handleViewDetail(record: { id: number }) {
|
|||||||
<a-descriptions-item label="操作人">
|
<a-descriptions-item label="操作人">
|
||||||
{{
|
{{
|
||||||
detail.user_id
|
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>
|
</a-descriptions-item>
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ const createdAtRange = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
store.loadLookups()
|
await store.loadLookups()
|
||||||
store.fetchLogs()
|
store.fetchLogs()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ async function handleViewDetail(record: { id: number }) {
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'user_id'">
|
<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>
|
||||||
<template v-else-if="column.key === 'duration_ms'">
|
<template v-else-if="column.key === 'duration_ms'">
|
||||||
<span :class="durationClass(record.duration_ms)">
|
<span :class="durationClass(record.duration_ms)">
|
||||||
@@ -274,7 +274,7 @@ async function handleViewDetail(record: { id: number }) {
|
|||||||
<a-descriptions-item label="用户">
|
<a-descriptions-item label="用户">
|
||||||
{{
|
{{
|
||||||
detail.user_id
|
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>
|
</a-descriptions-item>
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ interface UserLookup {
|
|||||||
username: string
|
username: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RoleLookup {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RouteGroupLookup {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
export const useOperationLogStore = defineStore('operationLog', () => {
|
export const useOperationLogStore = defineStore('operationLog', () => {
|
||||||
const logs = ref<OperationLogRecord[]>([])
|
const logs = ref<OperationLogRecord[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -28,19 +38,28 @@ export const useOperationLogStore = defineStore('operationLog', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const users = ref<UserLookup[]>([])
|
const users = ref<UserLookup[]>([])
|
||||||
|
const roles = ref<RoleLookup[]>([])
|
||||||
|
const routeGroups = ref<RouteGroupLookup[]>([])
|
||||||
|
|
||||||
const userMap = computed(
|
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() {
|
async function loadLookups() {
|
||||||
try {
|
const [userData, roleData, rgData] = await Promise.allSettled([
|
||||||
const data = await api.get<PaginatedData<UserLookup>>('/api/v1/users', {
|
api.get<PaginatedData<UserLookup>>('/api/v1/users', { per_page: 200 }),
|
||||||
per_page: 200,
|
api.get<PaginatedData<RoleLookup>>('/api/v1/roles', { per_page: 200 }),
|
||||||
})
|
api.get<PaginatedData<RouteGroupLookup>>('/api/v1/route-groups', { per_page: 200 }),
|
||||||
users.value = data.items
|
])
|
||||||
} catch (err: unknown) {
|
if (userData.status === 'fulfilled') users.value = userData.value.items
|
||||||
console.warn('加载用户列表失败', err)
|
if (roleData.status === 'fulfilled') roles.value = roleData.value.items
|
||||||
}
|
if (rgData.status === 'fulfilled') routeGroups.value = rgData.value.items
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchLogs() {
|
async function fetchLogs() {
|
||||||
@@ -86,6 +105,8 @@ export const useOperationLogStore = defineStore('operationLog', () => {
|
|||||||
filters,
|
filters,
|
||||||
users,
|
users,
|
||||||
userMap,
|
userMap,
|
||||||
|
roleMap,
|
||||||
|
routeGroupMap,
|
||||||
loadLookups,
|
loadLookups,
|
||||||
fetchLogs,
|
fetchLogs,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const useRequestLogStore = defineStore('requestLog', () => {
|
|||||||
|
|
||||||
const users = ref<UserLookup[]>([])
|
const users = ref<UserLookup[]>([])
|
||||||
const userMap = computed(
|
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() {
|
async function loadLookups() {
|
||||||
|
|||||||
Reference in New Issue
Block a user