From 2235deadf100056541ea9eddd81ac1283a573e54 Mon Sep 17 00:00:00 2001 From: Nick Zeng Date: Mon, 20 Apr 2026 09:47:32 +0800 Subject: [PATCH] add api keyb admin page --- .../D22.4-admin-api-key-page.devlog | 15 ---- .../src/pages/users/__tests__/index.spec.ts | 76 ++++++++++++++++++- frontend/src/pages/users/index.vue | 35 +++++++++ frontend/src/stores/user-manage.ts | 6 ++ .../results.json | 1 + 5 files changed, 115 insertions(+), 18 deletions(-) delete mode 100644 frontend/22-api-key-management-ui/D22.4-admin-api-key-page.devlog create mode 100644 node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json diff --git a/frontend/22-api-key-management-ui/D22.4-admin-api-key-page.devlog b/frontend/22-api-key-management-ui/D22.4-admin-api-key-page.devlog deleted file mode 100644 index 0dd2004..0000000 --- a/frontend/22-api-key-management-ui/D22.4-admin-api-key-page.devlog +++ /dev/null @@ -1,15 +0,0 @@ -# D22.4 管理员 API Key 管理页面 Devlog - -## 当前状态速览 -- 完成进度:4/4 ✅ -- 当前子项:已全部完成 -- 下一步行动:P22.5 - -## 日志 - -### 2026-04-20 — 完成 -- [启动] 开始执行 P22.4,context 已加载(store/types/MainLayout/参考 spec 均已读取) -- [22.4.1] 创建 `pages/api-keys/index.vue`;发现 TS 类型错误 6 个(boolean/scroll/record 类型)→ 修复(enabledFilter computed + 移除插槽类型注解 + Boolean() 转型)→ build ✅ -- [22.4.2] MainLayout.vue 追加 `/api-keys` 菜单项(KeyOutlined 已导入) -- [22.4.3] MainLayout.vue pathMap 追加 `/api-keys: 'API Key 管理'` -- [22.4.4] 新增 7 用例,全绿;MainLayout 既有 6 条失败(layout 组件类型不匹配)非本项引入 diff --git a/frontend/src/pages/users/__tests__/index.spec.ts b/frontend/src/pages/users/__tests__/index.spec.ts index 9247c36..1fe61bb 100644 --- a/frontend/src/pages/users/__tests__/index.spec.ts +++ b/frontend/src/pages/users/__tests__/index.spec.ts @@ -32,8 +32,8 @@ vi.mock('@/utils/request', () => ({ })) const mockUsers: UserRecord[] = [ - { id: 1, username: 'admin', email: 'admin@test.com', status: 1, role_id: 1, ext: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' }, - { id: 2, username: 'user1', email: 'user1@test.com', status: 0, role_id: 2, ext: { note: 'test' }, created_at: '2026-01-02T00:00:00Z', updated_at: '2026-01-02T00:00:00Z' }, + { id: 1, username: 'admin', email: 'admin@test.com', status: 1, role_id: 1, ext: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', api_key_enabled: true }, + { id: 2, username: 'user1', email: 'user1@test.com', status: 0, role_id: 2, ext: { note: 'test' }, created_at: '2026-01-02T00:00:00Z', updated_at: '2026-01-02T00:00:00Z', api_key_enabled: false }, ] const mockPaginatedResponse = { @@ -200,7 +200,8 @@ describe('Users Page', () => { if (opts.rolesError) return Promise.reject(new Error('roles fail')) return Promise.resolve(mockRoles) as never } - return Promise.resolve(mockPaginatedResponse) as never + // use fresh copies to prevent cross-test mutation of mockUsers + return Promise.resolve({ ...mockPaginatedResponse, items: mockUsers.map(u => ({ ...u })) }) as never }) // 设置 admin 用户以显示操作按钮(isAdmin 从 JWT 派生) @@ -330,4 +331,73 @@ describe('Users Page', () => { const rows = document.body.querySelectorAll('.ant-table-tbody tr.ant-table-row') expect(rows).toHaveLength(mockUsers.length) }) + + describe('API Key Switch', () => { + it('renders Switch with correct checked state based on api_key_enabled', async () => { + await mountPage() + const switches = document.body.querySelectorAll('.ant-switch') + // 第1行 api_key_enabled=true → checked + expect(switches[0]?.classList.contains('ant-switch-checked')).toBe(true) + // 第2行 api_key_enabled=false → unchecked + expect(switches[1]?.classList.contains('ant-switch-checked')).toBe(false) + }) + + it('calls api.patch directly when enabling and shows success', async () => { + const { api } = await mountPage() + vi.mocked(api.patch).mockResolvedValueOnce(undefined) + const successSpy = vi.spyOn(message, 'success').mockImplementation(() => ({}) as never) + + const store = useUserManageStore() + const record = store.users[1]! // api_key_enabled=false + + // Invoke the component's handleToggleApiKeyEnabled via setupState + const handleToggle = (wrapper.getCurrentComponent() as any).setupState.handleToggleApiKeyEnabled + await handleToggle(record, true) + + expect(api.patch).toHaveBeenCalledWith( + `/api/v1/users/${record.id}/api-key-enabled`, + { api_key_enabled: true }, + ) + expect(record.api_key_enabled).toBe(true) + expect(successSpy).toHaveBeenCalledWith('操作成功') + successSpy.mockRestore() + }) + + it('calls api.patch with false on disable and shows success', async () => { + const { api } = await mountPage() + vi.mocked(api.patch).mockResolvedValueOnce(undefined) + const successSpy = vi.spyOn(message, 'success').mockImplementation(() => ({}) as never) + + const store = useUserManageStore() + const record = store.users[0]! // api_key_enabled=true + + const handleToggle = (wrapper.getCurrentComponent() as any).setupState.handleToggleApiKeyEnabled + await handleToggle(record, false) + + expect(api.patch).toHaveBeenCalledWith( + `/api/v1/users/${record.id}/api-key-enabled`, + { api_key_enabled: false }, + ) + expect(record.api_key_enabled).toBe(false) + expect(successSpy).toHaveBeenCalledWith('操作成功') + successSpy.mockRestore() + }) + + it('rolls back api_key_enabled and shows error on patch failure', async () => { + const { api } = await mountPage() + vi.mocked(api.patch).mockRejectedValueOnce(new Error('服务器错误')) + const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as never) + + const store = useUserManageStore() + const record = store.users[0]! // api_key_enabled=true + + const handleToggle = (wrapper.getCurrentComponent() as any).setupState.handleToggleApiKeyEnabled + await handleToggle(record, false) + + // record is the same reactive ref as store.users[0]; rollback should restore it + expect(record.api_key_enabled).toBe(true) + expect(errorSpy).toHaveBeenCalledWith('服务器错误') + errorSpy.mockRestore() + }) + }) }) diff --git a/frontend/src/pages/users/index.vue b/frontend/src/pages/users/index.vue index 7721084..f7a7885 100644 --- a/frontend/src/pages/users/index.vue +++ b/frontend/src/pages/users/index.vue @@ -44,6 +44,7 @@ const columns = [ { title: '邮箱', dataIndex: 'email' }, { title: '角色', key: 'role', width: 120 }, { title: '状态', dataIndex: 'status', width: 100 }, + { title: 'API Key', key: 'api_key_enabled', width: 100 }, { title: '创建时间', dataIndex: 'created_at', width: 180 }, { title: '数据范围', key: 'data_scope', width: 120 }, { title: '操作', key: 'action', width: 320 }, @@ -150,6 +151,24 @@ function handleDataScope(record: UserRecord) { scopeModalOpen.value = true } +const togglingApiKeyId = ref(null) + +async function handleToggleApiKeyEnabled(record: UserRecord, enabled: boolean) { + togglingApiKeyId.value = record.id + const original = record.api_key_enabled + record.api_key_enabled = enabled + try { + await store.toggleApiKeyEnabled(record.id, enabled) + message.success('操作成功') + } catch (err: unknown) { + record.api_key_enabled = original + const msg = err instanceof Error ? err.message : '操作失败' + message.error(msg) + } finally { + togglingApiKeyId.value = null + } +} + function handleScopeSuccess() { store.fetchUsers() } @@ -238,6 +257,22 @@ function formatTime(time: string) { {{ record.role.name }} - +