hookehuyr

feat(biz): 添加客户列表功能模块

实现客户列表功能,包括路由配置、API接口、mock数据和页面组件
1 +import { faker } from '@faker-js/faker';
2 +import { eventHandler, getQuery } from 'h3';
3 +import { verifyAccessToken } from '~/utils/jwt-utils';
4 +import {
5 + sleep,
6 + unAuthorizedResponse,
7 + usePageResponseSuccess,
8 +} from '~/utils/response';
9 +
10 +function createPhone() {
11 + const suffix = faker.string.numeric({ length: 8 });
12 + return `1${faker.helpers.arrayElement(['3', '5', '6', '7', '8', '9'])}${suffix}`;
13 +}
14 +
15 +function createCustomers(count: number) {
16 + return Array.from({ length: count }, () => ({
17 + id: faker.string.uuid(),
18 + name: faker.person.fullName(),
19 + phone: createPhone(),
20 + status: faker.helpers.arrayElement(['active', 'disabled']),
21 + createdAt: faker.date.past().toISOString().slice(0, 19).replace('T', ' '),
22 + }));
23 +}
24 +
25 +const mockData = createCustomers(120);
26 +
27 +export default eventHandler(async (event) => {
28 + const userinfo = verifyAccessToken(event);
29 + if (!userinfo) {
30 + return unAuthorizedResponse(event);
31 + }
32 +
33 + await sleep(300);
34 +
35 + const { keyword, page, pageSize, status } = getQuery(event);
36 + const keyword_raw = Array.isArray(keyword) ? keyword[0] : keyword;
37 + const status_raw = Array.isArray(status) ? status[0] : status;
38 + const page_raw = Array.isArray(page) ? page[0] : page;
39 + const page_size_raw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
40 +
41 + const page_number = Math.max(
42 + 1,
43 + Number.parseInt(String(page_raw ?? '1'), 10) || 1,
44 + );
45 + const page_size_number = Math.min(
46 + 100,
47 + Math.max(1, Number.parseInt(String(page_size_raw ?? '10'), 10) || 10),
48 + );
49 +
50 + const list_data = structuredClone(mockData);
51 +
52 + const keyword_str = typeof keyword_raw === 'string' ? keyword_raw.trim() : '';
53 + const status_str =
54 + status_raw === 'active' || status_raw === 'disabled' ? status_raw : '';
55 +
56 + const filtered = list_data.filter((item) => {
57 + const keyword_ok =
58 + !keyword_str ||
59 + item.name.includes(keyword_str) ||
60 + item.phone.includes(keyword_str);
61 + const status_ok = !status_str || item.status === status_str;
62 + return keyword_ok && status_ok;
63 + });
64 +
65 + return usePageResponseSuccess(
66 + String(page_number),
67 + String(page_size_number),
68 + filtered,
69 + );
70 +});
1 +import { requestClient } from '#/api/request';
2 +
3 +export namespace CustomersApi {
4 + export interface CustomerItem {
5 + createdAt: string;
6 + id: string;
7 + name: string;
8 + phone: string;
9 + status: 'active' | 'disabled';
10 + }
11 +
12 + export interface ListParams {
13 + keyword?: string;
14 + page: number;
15 + pageSize: number;
16 + status?: 'active' | 'disabled';
17 + }
18 +
19 + export interface ListResult {
20 + items: CustomerItem[];
21 + total: number;
22 + }
23 +}
24 +
25 +export async function listCustomersApi(params: CustomersApi.ListParams) {
26 + return requestClient.get<CustomersApi.ListResult>('/biz/customers/list', {
27 + params,
28 + });
29 +}
1 +export * from './customers';
1 +/*
2 + * @Date: 2025-11-17 19:45:50
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2026-01-06 13:48:51
5 + * @FilePath: /vben-admin/apps/web-ele/src/api/index.ts
6 + * @Description: 文件描述
7 + */
8 +export * from './biz';
1 export * from './core'; 9 export * from './core';
......
...@@ -11,5 +11,9 @@ ...@@ -11,5 +11,9 @@
11 "title": "Dashboard", 11 "title": "Dashboard",
12 "analytics": "Analytics", 12 "analytics": "Analytics",
13 "workspace": "Workspace" 13 "workspace": "Workspace"
14 + },
15 + "biz": {
16 + "title": "Business",
17 + "customers": "Customers"
14 } 18 }
15 } 19 }
......
...@@ -11,5 +11,9 @@ ...@@ -11,5 +11,9 @@
11 "title": "概览", 11 "title": "概览",
12 "analytics": "分析页", 12 "analytics": "分析页",
13 "workspace": "工作台" 13 "workspace": "工作台"
14 + },
15 + "biz": {
16 + "title": "业务示例",
17 + "customers": "客户列表"
14 } 18 }
15 } 19 }
......
1 +/*
2 + * @Date: 2026-01-06 13:48:56
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2026-01-06 14:53:56
5 + * @FilePath: /vben-admin/apps/web-ele/src/router/routes/modules/biz.ts
6 + * @Description: 文件描述
7 + */
8 +import type { RouteRecordRaw } from 'vue-router';
9 +
10 +import { $t } from '#/locales';
11 +
12 +const routes: RouteRecordRaw[] = [
13 + {
14 + meta: {
15 + icon: 'lucide:briefcase-business',
16 + order: 30,
17 + title: $t('page.biz.title'),
18 + },
19 + name: 'Biz',
20 + path: '/biz',
21 + children: [
22 + {
23 + meta: {
24 + icon: 'lucide:users',
25 + title: $t('page.biz.customers'),
26 + },
27 + name: 'BizCustomers',
28 + path: '/biz/customers',
29 + component: () => import('#/views/biz/customers/index.vue'),
30 + },
31 + ],
32 + },
33 +];
34 +
35 +export default routes;
1 +<script lang="ts" setup>
2 +import { onMounted, reactive, ref } from 'vue';
3 +
4 +import { Page } from '@vben/common-ui';
5 +
6 +import {
7 + ElButton,
8 + ElCard,
9 + ElForm,
10 + ElFormItem,
11 + ElInput,
12 + ElOption,
13 + ElPagination,
14 + ElSelect,
15 + ElTable,
16 + ElTag,
17 +} from 'element-plus';
18 +
19 +import { listCustomersApi } from '#/api';
20 +
21 +const loading = ref(false);
22 +const table_data = ref<any[]>([]);
23 +const total = ref(0);
24 +
25 +const query_form = reactive({
26 + keyword: '',
27 + page: 1,
28 + pageSize: 10,
29 + status: '' as '' | 'active' | 'disabled',
30 +});
31 +
32 +const fetch_list = async () => {
33 + loading.value = true;
34 + try {
35 + const data = await listCustomersApi({
36 + keyword: query_form.keyword || undefined,
37 + page: query_form.page,
38 + pageSize: query_form.pageSize,
39 + status: query_form.status || undefined,
40 + });
41 + table_data.value = data.items;
42 + total.value = data.total;
43 + } finally {
44 + loading.value = false;
45 + }
46 +};
47 +
48 +const handle_search = async () => {
49 + query_form.page = 1;
50 + await fetch_list();
51 +};
52 +
53 +const handle_reset = async () => {
54 + query_form.keyword = '';
55 + query_form.status = '';
56 + query_form.page = 1;
57 + await fetch_list();
58 +};
59 +
60 +const handle_page_change = async (page: number) => {
61 + query_form.page = page;
62 + await fetch_list();
63 +};
64 +
65 +const handle_page_size_change = async (page_size: number) => {
66 + query_form.pageSize = page_size;
67 + query_form.page = 1;
68 + await fetch_list();
69 +};
70 +
71 +onMounted(fetch_list);
72 +</script>
73 +
74 +<template>
75 + <Page
76 + title="客户列表(示例)"
77 + description="一个最小可运行的业务模块示例:路由 + 页面 + API + mock 接口"
78 + >
79 + <div class="flex flex-col gap-4">
80 + <ElCard shadow="never">
81 + <ElForm class="flex flex-wrap gap-x-4" inline>
82 + <ElFormItem label="关键词">
83 + <ElInput
84 + v-model="query_form.keyword"
85 + clearable
86 + placeholder="姓名/手机号"
87 + style="width: 220px"
88 + @keyup.enter="handle_search"
89 + />
90 + </ElFormItem>
91 + <ElFormItem label="状态">
92 + <ElSelect
93 + v-model="query_form.status"
94 + clearable
95 + placeholder="全部"
96 + style="width: 160px"
97 + >
98 + <ElOption label="启用" value="active" />
99 + <ElOption label="禁用" value="disabled" />
100 + </ElSelect>
101 + </ElFormItem>
102 +
103 + <ElFormItem>
104 + <div class="flex items-center gap-2">
105 + <ElButton
106 + type="primary"
107 + :loading="loading"
108 + @click="handle_search"
109 + >
110 + 查询
111 + </ElButton>
112 + <ElButton :disabled="loading" @click="handle_reset">
113 + 重置
114 + </ElButton>
115 + </div>
116 + </ElFormItem>
117 + </ElForm>
118 + </ElCard>
119 +
120 + <ElCard shadow="never">
121 + <ElTable :data="table_data" v-loading="loading" stripe>
122 + <ElTable.TableColumn label="客户ID" prop="id" min-width="220" />
123 + <ElTable.TableColumn label="姓名" prop="name" min-width="140" />
124 + <ElTable.TableColumn label="手机号" prop="phone" min-width="140" />
125 + <ElTable.TableColumn
126 + label="创建时间"
127 + prop="createdAt"
128 + min-width="180"
129 + />
130 + <ElTable.TableColumn label="状态" min-width="100">
131 + <template #default="{ row }">
132 + <ElTag :type="row.status === 'active' ? 'success' : 'info'">
133 + {{ row.status === 'active' ? '启用' : '禁用' }}
134 + </ElTag>
135 + </template>
136 + </ElTable.TableColumn>
137 + </ElTable>
138 +
139 + <div class="mt-4 flex justify-end">
140 + <ElPagination
141 + :current-page="query_form.page"
142 + :page-size="query_form.pageSize"
143 + :page-sizes="[10, 20, 50, 100]"
144 + :total="total"
145 + layout="total, sizes, prev, pager, next, jumper"
146 + @update:current-page="handle_page_change"
147 + @update:page-size="handle_page_size_change"
148 + />
149 + </div>
150 + </ElCard>
151 + </div>
152 + </Page>
153 +</template>