fix: 列表筛选统一调用接口
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user