diff --git a/admin-frontend-vue/src/pages/account/UsersPage.vue b/admin-frontend-vue/src/pages/account/UsersPage.vue index 8393a125..fcb886bd 100644 --- a/admin-frontend-vue/src/pages/account/UsersPage.vue +++ b/admin-frontend-vue/src/pages/account/UsersPage.vue @@ -4,16 +4,17 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { http } from '@/api/http' import type { AdminUser } from '@/types/admin' import { fetchUserList } from '@/api/users' +import { createUserFilterState, toUserListParams } from './users-filter' const loading = ref(false) const rows = ref([]) const total = ref(0) -const form = reactive({ username: '', page: 1, pageSize: 15 }) +const filters = reactive(createUserFilterState()) async function loadUsers() { loading.value = true try { - const page = await fetchUserList({ page: form.page, pageSize: form.pageSize, keyword: form.username || undefined }) + const page = await fetchUserList(toUserListParams(filters)) rows.value = page.items total.value = page.total } catch (error) { @@ -23,6 +24,12 @@ async function loadUsers() { } } +/** 提交新筛选条件:重置回第一页再查询。 */ +function search() { + filters.page = 1 + void loadUsers() +} + async function removeUser(row: AdminUser) { try { await ElMessageBox.confirm(`确定删除用户“${row.username}”吗?`, '删除确认', { type: 'warning' }) @@ -35,7 +42,7 @@ async function removeUser(row: AdminUser) { } function changePage(page: number) { - form.page = page + filters.page = page void loadUsers() } @@ -52,12 +59,12 @@ onMounted(loadUsers) 新建用户 - + - + - 查询 + 查询 @@ -79,8 +86,8 @@ onMounted(loadUsers) diff --git a/admin-frontend-vue/src/pages/account/users-filter.ts b/admin-frontend-vue/src/pages/account/users-filter.ts new file mode 100644 index 00000000..8c3fd7d7 --- /dev/null +++ b/admin-frontend-vue/src/pages/account/users-filter.ts @@ -0,0 +1,45 @@ +/** 用户页局部筛选/分页状态(任务 43):纯逻辑、不入 Pinia,页面级可测状态机。 */ +import { USER_PAGE_DEFAULT_SIZE, USER_PAGE_MIN_PAGE, type UserListParams } from '../../api/users-dto.ts' + +/** 页面局部查询状态:筛选条件 + 分页;业务表状态不落 Pinia。 */ +export interface UserFilterState { + /** 用户名模糊关键字。 */ + keyword: string + /** 按创建管理员过滤(可选)。 */ + createdByAdminId: number | null + page: number + pageSize: number +} + +export function createUserFilterState(): UserFilterState { + return { keyword: '', createdByAdminId: null, page: USER_PAGE_MIN_PAGE, pageSize: USER_PAGE_DEFAULT_SIZE } +} + +/** 修改筛选条件时重置回第一页(避免停在可能越界的旧页)。 */ +export function withFilterReset( + state: UserFilterState, + next: Partial>, +): UserFilterState { + return { ...state, ...next, page: USER_PAGE_MIN_PAGE } +} + +/** 翻页只改页码并钳到 >=1,保留筛选条件。 */ +export function withPage(state: UserFilterState, page: number): UserFilterState { + return { ...state, page: Math.max(page, USER_PAGE_MIN_PAGE) } +} + +/** 是否存在生效筛选(仅空白关键字不算)。 */ +export function isFilterActive(state: UserFilterState): boolean { + return Boolean(state.keyword && state.keyword.trim()) || state.createdByAdminId != null +} + +/** 页面状态 → 查询适配层参数(空关键字不下发)。 */ +export function toUserListParams(state: UserFilterState): UserListParams { + const keyword = state.keyword.trim() + return { + page: state.page, + pageSize: state.pageSize, + keyword: keyword || undefined, + createdById: state.createdByAdminId, + } +} diff --git a/admin-frontend-vue/tests/task-43.test.ts b/admin-frontend-vue/tests/task-43.test.ts new file mode 100644 index 00000000..dad36fe1 --- /dev/null +++ b/admin-frontend-vue/tests/task-43.test.ts @@ -0,0 +1,71 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + createUserFilterState, + isFilterActive, + toUserListParams, + withFilterReset, + withPage, +} from '../src/pages/account/users-filter.ts' +import { USER_PAGE_DEFAULT_SIZE } from '../src/api/users-dto.ts' + +test('test_task_043_user_filter_state_normal_primary_path', () => { + // 正常主路径:初始筛选状态默认空关键字、无管理员过滤、回到首页。 + const s = createUserFilterState() + assert.equal(s.keyword, '') + assert.equal(s.createdByAdminId, null) + assert.equal(s.page, 1) + assert.equal(s.pageSize, USER_PAGE_DEFAULT_SIZE) + assert.equal(isFilterActive(s), false) +}) + +test('test_task_043_user_filter_state_normal_variant_input', () => { + // 正常变体:填关键字/管理员过滤后为激活态。 + const s = { ...createUserFilterState(), keyword: '张', createdByAdminId: 3 } + assert.equal(isFilterActive(s), true) +}) + +test('test_task_043_user_filter_state_normal_repeated_operation_is_idempotent', () => { + // 正常重复:工厂每次返回独立等价状态;序列化不改输入。 + assert.deepEqual(createUserFilterState(), createUserFilterState()) + const s = withPage(createUserFilterState(), 3) + assert.deepEqual(s, s) +}) + +test('test_task_043_user_filter_state_boundary_empty_input', () => { + // 边界空值:空关键字在序列化时不下发空串。 + const params = toUserListParams(createUserFilterState()) + assert.equal(params.keyword, undefined) +}) + +test('test_task_043_user_filter_state_boundary_single_item', () => { + // 边界单元素:改关键字自动回到第一页,保留分页大小。 + const s = withFilterReset({ ...createUserFilterState(), page: 4 }, { keyword: 'a' }) + assert.equal(s.page, 1) + assert.equal(s.keyword, 'a') + assert.equal(s.pageSize, USER_PAGE_DEFAULT_SIZE) +}) + +test('test_task_043_user_filter_state_boundary_limit_or_missing_field', () => { + // 边界上限/越界页码:翻页钳到 >=1;大于当前也保留。 + assert.equal(withPage(createUserFilterState(), 0).page, 1) + assert.equal(withPage(createUserFilterState(), 99).page, 99) +}) + +test('test_task_043_user_filter_state_invalid_input_rejected', () => { + // 异常输入:仅空白关键字不视为激活;筛选与分页参数映射正确。 + assert.equal(isFilterActive({ ...createUserFilterState(), keyword: ' ' }), false) + const params = toUserListParams({ ...createUserFilterState(), keyword: ' x ', page: 2, pageSize: 20 }) + assert.deepEqual(params, { page: 2, pageSize: 20, keyword: 'x', createdById: null }) +}) + +test('test_task_043_user_filter_state_dependency_failure_returns_actionable_message', () => { + // 依赖失败:页面把筛选/分页状态局部化到 users-filter,不入 Pinia。 + const page = readSource('src/pages/account/UsersPage.vue') + assert.match(page, /users-filter/, '页面复用局部筛选状态模块') + assert.equal(/defineStore|useAdminSessionStore/.test(page), false, '筛选状态不落在 session store') + const mod = readSource('src/pages/account/users-filter.ts') + assert.match(mod, /export function createUserFilterState/) + assert.match(mod, /users-dto/, '筛选模块复用分页 DTO 常量') +})