task-43(账号/权限页面): 实现用户筛选条件状态
新增 users-filter.ts 页面局部状态机:keyword/createdByAdminId/page/pageSize 默认值、改条件重置第一页(withFilterReset)、翻页钳到>=1、空白关键字不算激活、 toUserListParams 映射查询参数;UsersPage 改用它,搜索按钮/回车重置页码。 筛选状态不入 Pinia。8 用例全过,307 单测 + build 绿。
This commit is contained in:
@@ -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<AdminUser[]>([])
|
||||
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)
|
||||
<el-button type="primary">新建用户</el-button>
|
||||
</div>
|
||||
<el-card shadow="never">
|
||||
<el-form inline @submit.prevent="loadUsers">
|
||||
<el-form inline @submit.prevent="search">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.username" clearable placeholder="模糊搜索" @keyup.enter="loadUsers" />
|
||||
<el-input v-model="filters.keyword" clearable placeholder="模糊搜索" @keyup.enter="search" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadUsers">查询</el-button>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -79,8 +86,8 @@ onMounted(loadUsers)
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:page-size="form.pageSize"
|
||||
:current-page="form.page"
|
||||
:page-size="filters.pageSize"
|
||||
:current-page="filters.page"
|
||||
:total="total"
|
||||
@current-change="changePage"
|
||||
/>
|
||||
|
||||
@@ -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<Pick<UserFilterState, 'keyword' | 'createdByAdminId'>>,
|
||||
): 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,
|
||||
}
|
||||
}
|
||||
@@ -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 常量')
|
||||
})
|
||||
Reference in New Issue
Block a user