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`.
|
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
|
## 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`.
|
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/stores/index.vue`: 门店管理,筛选、重置、启停、删除后都会重新调用接口,支持新增和编辑。
|
||||||
- `src/views/roles/index.vue`: 角色管理,支持角色编码校验、新增、编辑和删除。
|
- `src/views/roles/index.vue`: 角色管理,搜索、重置、删除和保存后都会重新调用接口,支持角色编码校验。
|
||||||
- `src/views/employees/index.vue`: 员工管理,支持门店/状态/关键词筛选、分页、新增、编辑、启停和软删除。
|
- `src/views/employees/index.vue`: 员工管理,门店/状态/关键词筛选、重置、分页、启停、删除和保存后都会重新调用接口。
|
||||||
- `src/api/access.ts`: 门店、角色、员工接口类型与 HTTP 方法封装。
|
- `src/api/access.ts`: 门店、角色、员工接口类型与 HTTP 方法封装。
|
||||||
- `src/router/modules/employees.ts`: 权限管理菜单入口,挂载门店、角色、员工三个页面。
|
- `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`
|
- `GET /api/stores/:id`
|
||||||
- `POST /api/stores`
|
- `POST /api/stores`
|
||||||
- `PATCH /api/stores/:id`
|
- `PATCH /api/stores/:id`
|
||||||
- `DELETE /api/stores/:id`
|
- `DELETE /api/stores/:id`
|
||||||
- `GET /api/roles`
|
- `GET /api/roles`,搜索时会携带 `keyword`
|
||||||
- `GET /api/roles/:id`
|
- `GET /api/roles/:id`
|
||||||
- `POST /api/roles`
|
- `POST /api/roles`
|
||||||
- `PATCH /api/roles/:id`
|
- `PATCH /api/roles/:id`
|
||||||
- `DELETE /api/roles/:id`
|
- `DELETE /api/roles/:id`
|
||||||
- `GET /api/employees`
|
- `GET /api/employees`,列表会携带 `page`、`pageSize`,筛选时会携带 `storeId`、`status`、`keyword`
|
||||||
- `GET /api/employees/:id`
|
- `GET /api/employees/:id`
|
||||||
- `POST /api/employees`
|
- `POST /api/employees`
|
||||||
- `PATCH /api/employees/:id`
|
- `PATCH /api/employees/:id`
|
||||||
- `PATCH /api/employees/:id/status`
|
- `PATCH /api/employees/:id/status`
|
||||||
- `DELETE /api/employees/:id`
|
- `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;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 员工列表是服务端分页,门店和角色当前由前端做轻量筛选。 */
|
/** 员工列表是服务端分页,筛选条件统一通过查询参数传递。 */
|
||||||
export interface EmployeeListParams {
|
export interface EmployeeListParams {
|
||||||
storeId?: number;
|
storeId?: number;
|
||||||
status?: EmployeeStatus;
|
status?: EmployeeStatus;
|
||||||
@@ -68,6 +68,12 @@ export interface EmployeeListParams {
|
|||||||
|
|
||||||
export interface StoreListParams {
|
export interface StoreListParams {
|
||||||
includeInactive?: boolean;
|
includeInactive?: boolean;
|
||||||
|
status?: StoreStatus;
|
||||||
|
keyword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleListParams {
|
||||||
|
keyword?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StorePayload {
|
export interface StorePayload {
|
||||||
@@ -117,8 +123,10 @@ export const listStores = (params?: StoreListParams) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listRoles = () => {
|
export const listRoles = (params?: RoleListParams) => {
|
||||||
return http.request<ApiResult<Role[]>>("get", `${API_PREFIX}/roles`);
|
return http.request<ApiResult<Role[]>>("get", `${API_PREFIX}/roles`, {
|
||||||
|
params
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStore = (id: number) => {
|
export const getStore = (id: number) => {
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ onMounted(async () => {
|
|||||||
placeholder="全部门店"
|
placeholder="全部门店"
|
||||||
class="toolbar-control"
|
class="toolbar-control"
|
||||||
:loading="catalogLoading"
|
:loading="catalogLoading"
|
||||||
|
@change="handleSearch"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="store in stores"
|
v-for="store in stores"
|
||||||
@@ -366,6 +367,7 @@ onMounted(async () => {
|
|||||||
clearable
|
clearable
|
||||||
placeholder="全部状态"
|
placeholder="全部状态"
|
||||||
class="toolbar-control"
|
class="toolbar-control"
|
||||||
|
@change="handleSearch"
|
||||||
>
|
>
|
||||||
<el-option label="启用" value="ACTIVE" />
|
<el-option label="启用" value="ACTIVE" />
|
||||||
<el-option label="停用" value="INACTIVE" />
|
<el-option label="停用" value="INACTIVE" />
|
||||||
@@ -375,6 +377,7 @@ onMounted(async () => {
|
|||||||
clearable
|
clearable
|
||||||
class="keyword-input"
|
class="keyword-input"
|
||||||
placeholder="搜索姓名或手机号"
|
placeholder="搜索姓名或手机号"
|
||||||
|
@clear="handleSearch"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
/>
|
/>
|
||||||
<div class="toolbar-actions">
|
<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(
|
const describedCount = computed(
|
||||||
() => roles.value.filter(item => Boolean(item.description)).length
|
() => roles.value.filter(item => Boolean(item.description)).length
|
||||||
);
|
);
|
||||||
@@ -92,6 +75,22 @@ const systemRoleCount = computed(
|
|||||||
);
|
);
|
||||||
const dialogTitle = computed(() => (form.id ? "编辑角色" : "新增角色"));
|
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) {
|
function getErrorMessage(error: unknown, fallback: string) {
|
||||||
const message = (
|
const message = (
|
||||||
error as { response?: { data?: { error?: { message?: string } } } }
|
error as { response?: { data?: { error?: { message?: string } } } }
|
||||||
@@ -133,12 +132,14 @@ function buildPayload(): RolePayload {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 角色是员工绑定的基础字典,页面加载和保存后都需要刷新。 */
|
/** 角色查询必须走接口,避免搜索条件只在前端过滤当前缓存。 */
|
||||||
async function fetchRoles() {
|
async function fetchRoles() {
|
||||||
tableLoading.value = true;
|
tableLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await listRoles();
|
const result = await listRoles({
|
||||||
roles.value = result.data;
|
keyword: query.keyword.trim() || undefined
|
||||||
|
});
|
||||||
|
roles.value = applyRoleQuery(result.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(getErrorMessage(error, "加载角色列表失败"));
|
ElMessage.error(getErrorMessage(error, "加载角色列表失败"));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -148,10 +149,12 @@ async function fetchRoles() {
|
|||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
query.keyword = "";
|
query.keyword = "";
|
||||||
|
fetchRoles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
query.keyword = query.keyword.trim();
|
query.keyword = query.keyword.trim();
|
||||||
|
fetchRoles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreateDialog() {
|
function openCreateDialog() {
|
||||||
@@ -244,8 +247,8 @@ onMounted(fetchRoles);
|
|||||||
<strong>{{ systemRoleCount }}</strong>
|
<strong>{{ systemRoleCount }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<span>当前筛选</span>
|
<span>当前结果</span>
|
||||||
<strong>{{ filteredRoles.length }}</strong>
|
<strong>{{ roles.length }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -255,6 +258,7 @@ onMounted(fetchRoles);
|
|||||||
clearable
|
clearable
|
||||||
class="keyword-input"
|
class="keyword-input"
|
||||||
placeholder="搜索编码、名称或说明"
|
placeholder="搜索编码、名称或说明"
|
||||||
|
@clear="handleSearch"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
/>
|
/>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
@@ -268,7 +272,7 @@ onMounted(fetchRoles);
|
|||||||
<div class="table-shell">
|
<div class="table-shell">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="tableLoading"
|
v-loading="tableLoading"
|
||||||
:data="filteredRoles"
|
:data="roles"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
stripe
|
stripe
|
||||||
class="management-table"
|
class="management-table"
|
||||||
|
|||||||
+25
-18
@@ -63,11 +63,18 @@ const rules: FormRules<StoreFormState> = {
|
|||||||
status: [{ required: true, message: "请选择门店状态", trigger: "change" }]
|
status: [{ required: true, message: "请选择门店状态", trigger: "change" }]
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 门店数据量预计较小,列表一次性拉取后在前端做状态和关键词筛选。 */
|
const activeCount = computed(
|
||||||
const filteredStores = 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();
|
const keyword = query.keyword.trim().toLowerCase();
|
||||||
|
|
||||||
return stores.value.filter(store => {
|
return items.filter(store => {
|
||||||
const matchedStatus =
|
const matchedStatus =
|
||||||
query.status === undefined || store.status === query.status;
|
query.status === undefined || store.status === query.status;
|
||||||
const matchedKeyword =
|
const matchedKeyword =
|
||||||
@@ -78,15 +85,7 @@ const filteredStores = computed(() => {
|
|||||||
|
|
||||||
return matchedStatus && matchedKeyword;
|
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) {
|
function getErrorMessage(error: unknown, fallback: string) {
|
||||||
const message = (
|
const message = (
|
||||||
@@ -132,12 +131,16 @@ function buildPayload(): StorePayload {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 拉取包含停用门店的完整列表,便于后台直接做启停管理。 */
|
/** 门店筛选必须走接口,避免查询条件只停留在当前页面内存里。 */
|
||||||
async function fetchStores() {
|
async function fetchStores() {
|
||||||
tableLoading.value = true;
|
tableLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await listStores({ includeInactive: true });
|
const result = await listStores({
|
||||||
stores.value = result.data;
|
includeInactive: true,
|
||||||
|
status: query.status,
|
||||||
|
keyword: query.keyword.trim() || undefined
|
||||||
|
});
|
||||||
|
stores.value = applyStoreQuery(result.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(getErrorMessage(error, "加载门店列表失败"));
|
ElMessage.error(getErrorMessage(error, "加载门店列表失败"));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -148,10 +151,12 @@ async function fetchStores() {
|
|||||||
function handleReset() {
|
function handleReset() {
|
||||||
query.status = undefined;
|
query.status = undefined;
|
||||||
query.keyword = "";
|
query.keyword = "";
|
||||||
|
fetchStores();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
query.keyword = query.keyword.trim();
|
query.keyword = query.keyword.trim();
|
||||||
|
fetchStores();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreateDialog() {
|
function openCreateDialog() {
|
||||||
@@ -270,8 +275,8 @@ onMounted(fetchStores);
|
|||||||
<strong>{{ inactiveCount }}</strong>
|
<strong>{{ inactiveCount }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<span>当前筛选</span>
|
<span>当前结果</span>
|
||||||
<strong>{{ filteredStores.length }}</strong>
|
<strong>{{ stores.length }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -281,6 +286,7 @@ onMounted(fetchStores);
|
|||||||
clearable
|
clearable
|
||||||
placeholder="全部状态"
|
placeholder="全部状态"
|
||||||
class="toolbar-control"
|
class="toolbar-control"
|
||||||
|
@change="handleSearch"
|
||||||
>
|
>
|
||||||
<el-option label="启用" value="ACTIVE" />
|
<el-option label="启用" value="ACTIVE" />
|
||||||
<el-option label="停用" value="INACTIVE" />
|
<el-option label="停用" value="INACTIVE" />
|
||||||
@@ -290,6 +296,7 @@ onMounted(fetchStores);
|
|||||||
clearable
|
clearable
|
||||||
class="keyword-input"
|
class="keyword-input"
|
||||||
placeholder="搜索门店、地址或电话"
|
placeholder="搜索门店、地址或电话"
|
||||||
|
@clear="handleSearch"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
/>
|
/>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
@@ -303,7 +310,7 @@ onMounted(fetchStores);
|
|||||||
<div class="table-shell">
|
<div class="table-shell">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="tableLoading"
|
v-loading="tableLoading"
|
||||||
:data="filteredStores"
|
:data="stores"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
stripe
|
stripe
|
||||||
class="management-table"
|
class="management-table"
|
||||||
|
|||||||
Reference in New Issue
Block a user