From a6c9f5dee3886c165aff05c3824011e91e0241a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=9B=E5=85=AE?= Date: Tue, 26 May 2026 11:43:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=88=97=E8=A1=A8=E7=AD=9B=E9=80=89?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=B0=83=E7=94=A8=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en-US.md | 2 ++ README.md | 14 +++++----- src/api/access.ts | 14 +++++++--- src/views/employees/index.vue | 3 +++ src/views/roles/index.vue | 50 +++++++++++++++++++---------------- src/views/stores/index.vue | 43 +++++++++++++++++------------- 6 files changed, 75 insertions(+), 51 deletions(-) diff --git a/README.en-US.md b/README.en-US.md index be8ef1d..4faaafe 100644 --- a/README.en-US.md +++ b/README.en-US.md @@ -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`. diff --git a/README.md b/README.md index 93eda86..76cc8c1 100644 --- a/README.md +++ b/README.md @@ -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` 或 `PaginatedData` 描述,页面层只消费 `result.data`,避免在视图里重复拼接接口路径。 +接口响应统一在 `src/api/access.ts` 中使用 `ApiResult` 或 `PaginatedData` 描述,页面层只消费 `result.data`,避免在视图里重复拼接接口路径。列表搜索、重置、分页和状态变更后的刷新都应通过接口层完成,不直接依赖页面内存里的旧列表。 ## 配置说明 diff --git a/src/api/access.ts b/src/api/access.ts index abd441c..d973d08 100644 --- a/src/api/access.ts +++ b/src/api/access.ts @@ -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>("get", `${API_PREFIX}/roles`); +export const listRoles = (params?: RoleListParams) => { + return http.request>("get", `${API_PREFIX}/roles`, { + params + }); }; export const getStore = (id: number) => { diff --git a/src/views/employees/index.vue b/src/views/employees/index.vue index 0636d98..16170c3 100644 --- a/src/views/employees/index.vue +++ b/src/views/employees/index.vue @@ -353,6 +353,7 @@ onMounted(async () => { placeholder="全部门店" class="toolbar-control" :loading="catalogLoading" + @change="handleSearch" > { clearable placeholder="全部状态" class="toolbar-control" + @change="handleSearch" > @@ -375,6 +377,7 @@ onMounted(async () => { clearable class="keyword-input" placeholder="搜索姓名或手机号" + @clear="handleSearch" @keyup.enter="handleSearch" />
diff --git a/src/views/roles/index.vue b/src/views/roles/index.vue index e74d0a9..bbb3b09 100644 --- a/src/views/roles/index.vue +++ b/src/views/roles/index.vue @@ -67,23 +67,6 @@ const rules: FormRules = { ] }; -/** 角色列表当前不分页,搜索在前端完成,降低简单维护场景的交互成本。 */ -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); {{ systemRoleCount }}
- 当前筛选 - {{ filteredRoles.length }} + 当前结果 + {{ roles.length }}
@@ -255,6 +258,7 @@ onMounted(fetchRoles); clearable class="keyword-input" placeholder="搜索编码、名称或说明" + @clear="handleSearch" @keyup.enter="handleSearch" />
@@ -268,7 +272,7 @@ onMounted(fetchRoles);
= { 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); {{ inactiveCount }}
- 当前筛选 - {{ filteredStores.length }} + 当前结果 + {{ stores.length }}
@@ -281,6 +286,7 @@ onMounted(fetchStores); clearable placeholder="全部状态" class="toolbar-control" + @change="handleSearch" > @@ -290,6 +296,7 @@ onMounted(fetchStores); clearable class="keyword-input" placeholder="搜索门店、地址或电话" + @clear="handleSearch" @keyup.enter="handleSearch" />
@@ -303,7 +310,7 @@ onMounted(fetchStores);