fix: 列表筛选统一调用接口

This commit is contained in:
湛兮
2026-05-26 11:43:27 +08:00
parent 2fcfece32b
commit a6c9f5dee3
6 changed files with 75 additions and 51 deletions
+2
View File
@@ -94,6 +94,8 @@ This repository is not a monorepo and has no `packages/` directory. Important sc
Development mode proxies `/api` to `VITE_API_PROXY_TARGET`, defaulting to `http://localhost:3500`. The relevant files are `.env.development` and `vite.config.ts`.
List search, reset, pagination, status changes, deletes, and save refreshes should go through the API layer. Store list requests may send `includeInactive`, `status`, and `keyword`; role list requests may send `keyword`; employee list requests send `page`, `pageSize`, `storeId`, `status`, and `keyword` as needed.
## Documentation Sync Rule
When files, folders, package scripts, API modules, route modules, or key config files change, update `README.md` in the same change. The local skill is stored at `.codex/skills/readme-structure-sync/SKILL.md`.
+7 -7
View File
@@ -99,9 +99,9 @@ http://localhost:8848/
## 业务模块
- `src/views/stores/index.vue`: 门店管理,支持列表筛选、新增、编辑、启停删除。
- `src/views/roles/index.vue`: 角色管理,支持角色编码校验、新增、编辑和删除
- `src/views/employees/index.vue`: 员工管理,支持门店/状态/关键词筛选、分页、新增、编辑、启停和软删除
- `src/views/stores/index.vue`: 门店管理,筛选、重置、启停删除后都会重新调用接口,支持新增和编辑
- `src/views/roles/index.vue`: 角色管理,搜索、重置、删除和保存后都会重新调用接口,支持角色编码校验。
- `src/views/employees/index.vue`: 员工管理,门店/状态/关键词筛选、重置、分页、启停、删除和保存后都会重新调用接口
- `src/api/access.ts`: 门店、角色、员工接口类型与 HTTP 方法封装。
- `src/router/modules/employees.ts`: 权限管理菜单入口,挂载门店、角色、员工三个页面。
@@ -114,24 +114,24 @@ http://localhost:8848/
当前已对接接口:
- `GET /api/stores`
- `GET /api/stores`,管理列表会携带 `includeInactive`,筛选时会携带 `status``keyword`
- `GET /api/stores/:id`
- `POST /api/stores`
- `PATCH /api/stores/:id`
- `DELETE /api/stores/:id`
- `GET /api/roles`
- `GET /api/roles`,搜索时会携带 `keyword`
- `GET /api/roles/:id`
- `POST /api/roles`
- `PATCH /api/roles/:id`
- `DELETE /api/roles/:id`
- `GET /api/employees`
- `GET /api/employees`,列表会携带 `page``pageSize`,筛选时会携带 `storeId``status``keyword`
- `GET /api/employees/:id`
- `POST /api/employees`
- `PATCH /api/employees/:id`
- `PATCH /api/employees/:id/status`
- `DELETE /api/employees/:id`
接口响应统一在 `src/api/access.ts` 中使用 `ApiResult<T>``PaginatedData<T>` 描述,页面层只消费 `result.data`,避免在视图里重复拼接接口路径。
接口响应统一在 `src/api/access.ts` 中使用 `ApiResult<T>``PaginatedData<T>` 描述,页面层只消费 `result.data`,避免在视图里重复拼接接口路径。列表搜索、重置、分页和状态变更后的刷新都应通过接口层完成,不直接依赖页面内存里的旧列表。
## 配置说明
+11 -3
View File
@@ -57,7 +57,7 @@ export interface Role extends RoleOption {
updatedAt: string;
}
/** 员工列表是服务端分页,门店和角色当前由前端做轻量筛选。 */
/** 员工列表是服务端分页,筛选条件统一通过查询参数传递。 */
export interface EmployeeListParams {
storeId?: number;
status?: EmployeeStatus;
@@ -68,6 +68,12 @@ export interface EmployeeListParams {
export interface StoreListParams {
includeInactive?: boolean;
status?: StoreStatus;
keyword?: string;
}
export interface RoleListParams {
keyword?: string;
}
export interface StorePayload {
@@ -117,8 +123,10 @@ export const listStores = (params?: StoreListParams) => {
});
};
export const listRoles = () => {
return http.request<ApiResult<Role[]>>("get", `${API_PREFIX}/roles`);
export const listRoles = (params?: RoleListParams) => {
return http.request<ApiResult<Role[]>>("get", `${API_PREFIX}/roles`, {
params
});
};
export const getStore = (id: number) => {
+3
View File
@@ -353,6 +353,7 @@ onMounted(async () => {
placeholder="全部门店"
class="toolbar-control"
:loading="catalogLoading"
@change="handleSearch"
>
<el-option
v-for="store in stores"
@@ -366,6 +367,7 @@ onMounted(async () => {
clearable
placeholder="全部状态"
class="toolbar-control"
@change="handleSearch"
>
<el-option label="启用" value="ACTIVE" />
<el-option label="停用" value="INACTIVE" />
@@ -375,6 +377,7 @@ onMounted(async () => {
clearable
class="keyword-input"
placeholder="搜索姓名或手机号"
@clear="handleSearch"
@keyup.enter="handleSearch"
/>
<div class="toolbar-actions">
+27 -23
View File
@@ -67,23 +67,6 @@ const rules: FormRules<RoleFormState> = {
]
};
/** 角色列表当前不分页,搜索在前端完成,降低简单维护场景的交互成本。 */
const filteredRoles = computed(() => {
const keyword = query.keyword.trim().toLowerCase();
if (keyword.length === 0) {
return roles.value;
}
return roles.value.filter(role => {
return (
role.code.toLowerCase().includes(keyword) ||
role.name.toLowerCase().includes(keyword) ||
(role.description ?? "").toLowerCase().includes(keyword)
);
});
});
const describedCount = computed(
() => roles.value.filter(item => Boolean(item.description)).length
);
@@ -92,6 +75,22 @@ const systemRoleCount = computed(
);
const dialogTitle = computed(() => (form.id ? "编辑角色" : "新增角色"));
function applyRoleQuery(items: Role[]) {
const keyword = query.keyword.trim().toLowerCase();
if (keyword.length === 0) {
return items;
}
return items.filter(role => {
return (
role.code.toLowerCase().includes(keyword) ||
role.name.toLowerCase().includes(keyword) ||
(role.description ?? "").toLowerCase().includes(keyword)
);
});
}
function getErrorMessage(error: unknown, fallback: string) {
const message = (
error as { response?: { data?: { error?: { message?: string } } } }
@@ -133,12 +132,14 @@ function buildPayload(): RolePayload {
};
}
/** 角色是员工绑定的基础字典,页面加载和保存后都需要刷新。 */
/** 角色查询必须走接口,避免搜索条件只在前端过滤当前缓存。 */
async function fetchRoles() {
tableLoading.value = true;
try {
const result = await listRoles();
roles.value = result.data;
const result = await listRoles({
keyword: query.keyword.trim() || undefined
});
roles.value = applyRoleQuery(result.data);
} catch (error) {
ElMessage.error(getErrorMessage(error, "加载角色列表失败"));
} finally {
@@ -148,10 +149,12 @@ async function fetchRoles() {
function handleReset() {
query.keyword = "";
fetchRoles();
}
function handleSearch() {
query.keyword = query.keyword.trim();
fetchRoles();
}
function openCreateDialog() {
@@ -244,8 +247,8 @@ onMounted(fetchRoles);
<strong>{{ systemRoleCount }}</strong>
</div>
<div class="summary-item">
<span>当前筛选</span>
<strong>{{ filteredRoles.length }}</strong>
<span>当前结果</span>
<strong>{{ roles.length }}</strong>
</div>
</div>
@@ -255,6 +258,7 @@ onMounted(fetchRoles);
clearable
class="keyword-input"
placeholder="搜索编码、名称或说明"
@clear="handleSearch"
@keyup.enter="handleSearch"
/>
<div class="toolbar-actions">
@@ -268,7 +272,7 @@ onMounted(fetchRoles);
<div class="table-shell">
<el-table
v-loading="tableLoading"
:data="filteredRoles"
:data="roles"
row-key="id"
stripe
class="management-table"
+25 -18
View File
@@ -63,11 +63,18 @@ const rules: FormRules<StoreFormState> = {
status: [{ required: true, message: "请选择门店状态", trigger: "change" }]
};
/** 门店数据量预计较小,列表一次性拉取后在前端做状态和关键词筛选。 */
const filteredStores = computed(() => {
const activeCount = computed(
() => stores.value.filter(item => item.status === "ACTIVE").length
);
const inactiveCount = computed(
() => stores.value.filter(item => item.status === "INACTIVE").length
);
const dialogTitle = computed(() => (form.id ? "编辑门店" : "新增门店"));
function applyStoreQuery(items: Store[]) {
const keyword = query.keyword.trim().toLowerCase();
return stores.value.filter(store => {
return items.filter(store => {
const matchedStatus =
query.status === undefined || store.status === query.status;
const matchedKeyword =
@@ -78,15 +85,7 @@ const filteredStores = computed(() => {
return matchedStatus && matchedKeyword;
});
});
const activeCount = computed(
() => stores.value.filter(item => item.status === "ACTIVE").length
);
const inactiveCount = computed(
() => stores.value.filter(item => item.status === "INACTIVE").length
);
const dialogTitle = computed(() => (form.id ? "编辑门店" : "新增门店"));
}
function getErrorMessage(error: unknown, fallback: string) {
const message = (
@@ -132,12 +131,16 @@ function buildPayload(): StorePayload {
};
}
/** 拉取包含停用门店的完整列表,便于后台直接做启停管理。 */
/** 门店筛选必须走接口,避免查询条件只停留在当前页面内存里。 */
async function fetchStores() {
tableLoading.value = true;
try {
const result = await listStores({ includeInactive: true });
stores.value = result.data;
const result = await listStores({
includeInactive: true,
status: query.status,
keyword: query.keyword.trim() || undefined
});
stores.value = applyStoreQuery(result.data);
} catch (error) {
ElMessage.error(getErrorMessage(error, "加载门店列表失败"));
} finally {
@@ -148,10 +151,12 @@ async function fetchStores() {
function handleReset() {
query.status = undefined;
query.keyword = "";
fetchStores();
}
function handleSearch() {
query.keyword = query.keyword.trim();
fetchStores();
}
function openCreateDialog() {
@@ -270,8 +275,8 @@ onMounted(fetchStores);
<strong>{{ inactiveCount }}</strong>
</div>
<div class="summary-item">
<span>当前筛选</span>
<strong>{{ filteredStores.length }}</strong>
<span>当前结果</span>
<strong>{{ stores.length }}</strong>
</div>
</div>
@@ -281,6 +286,7 @@ onMounted(fetchStores);
clearable
placeholder="全部状态"
class="toolbar-control"
@change="handleSearch"
>
<el-option label="启用" value="ACTIVE" />
<el-option label="停用" value="INACTIVE" />
@@ -290,6 +296,7 @@ onMounted(fetchStores);
clearable
class="keyword-input"
placeholder="搜索门店、地址或电话"
@clear="handleSearch"
@keyup.enter="handleSearch"
/>
<div class="toolbar-actions">
@@ -303,7 +310,7 @@ onMounted(fetchStores);
<div class="table-shell">
<el-table
v-loading="tableLoading"
:data="filteredStores"
:data="stores"
row-key="id"
stripe
class="management-table"