hookehuyr

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

实现客户列表功能,包括路由配置、API接口、mock数据和页面组件
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
usePageResponseSuccess,
} from '~/utils/response';
function createPhone() {
const suffix = faker.string.numeric({ length: 8 });
return `1${faker.helpers.arrayElement(['3', '5', '6', '7', '8', '9'])}${suffix}`;
}
function createCustomers(count: number) {
return Array.from({ length: count }, () => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
phone: createPhone(),
status: faker.helpers.arrayElement(['active', 'disabled']),
createdAt: faker.date.past().toISOString().slice(0, 19).replace('T', ' '),
}));
}
const mockData = createCustomers(120);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(300);
const { keyword, page, pageSize, status } = getQuery(event);
const keyword_raw = Array.isArray(keyword) ? keyword[0] : keyword;
const status_raw = Array.isArray(status) ? status[0] : status;
const page_raw = Array.isArray(page) ? page[0] : page;
const page_size_raw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
const page_number = Math.max(
1,
Number.parseInt(String(page_raw ?? '1'), 10) || 1,
);
const page_size_number = Math.min(
100,
Math.max(1, Number.parseInt(String(page_size_raw ?? '10'), 10) || 10),
);
const list_data = structuredClone(mockData);
const keyword_str = typeof keyword_raw === 'string' ? keyword_raw.trim() : '';
const status_str =
status_raw === 'active' || status_raw === 'disabled' ? status_raw : '';
const filtered = list_data.filter((item) => {
const keyword_ok =
!keyword_str ||
item.name.includes(keyword_str) ||
item.phone.includes(keyword_str);
const status_ok = !status_str || item.status === status_str;
return keyword_ok && status_ok;
});
return usePageResponseSuccess(
String(page_number),
String(page_size_number),
filtered,
);
});
import { requestClient } from '#/api/request';
export namespace CustomersApi {
export interface CustomerItem {
createdAt: string;
id: string;
name: string;
phone: string;
status: 'active' | 'disabled';
}
export interface ListParams {
keyword?: string;
page: number;
pageSize: number;
status?: 'active' | 'disabled';
}
export interface ListResult {
items: CustomerItem[];
total: number;
}
}
export async function listCustomersApi(params: CustomersApi.ListParams) {
return requestClient.get<CustomersApi.ListResult>('/biz/customers/list', {
params,
});
}
export * from './customers';
/*
* @Date: 2025-11-17 19:45:50
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-06 13:48:51
* @FilePath: /vben-admin/apps/web-ele/src/api/index.ts
* @Description: 文件描述
*/
export * from './biz';
export * from './core';
......
......@@ -11,5 +11,9 @@
"title": "Dashboard",
"analytics": "Analytics",
"workspace": "Workspace"
},
"biz": {
"title": "Business",
"customers": "Customers"
}
}
......
......@@ -11,5 +11,9 @@
"title": "概览",
"analytics": "分析页",
"workspace": "工作台"
},
"biz": {
"title": "业务示例",
"customers": "客户列表"
}
}
......
/*
* @Date: 2026-01-06 13:48:56
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-06 14:53:56
* @FilePath: /vben-admin/apps/web-ele/src/router/routes/modules/biz.ts
* @Description: 文件描述
*/
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:briefcase-business',
order: 30,
title: $t('page.biz.title'),
},
name: 'Biz',
path: '/biz',
children: [
{
meta: {
icon: 'lucide:users',
title: $t('page.biz.customers'),
},
name: 'BizCustomers',
path: '/biz/customers',
component: () => import('#/views/biz/customers/index.vue'),
},
],
},
];
export default routes;
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import { Page } from '@vben/common-ui';
import {
ElButton,
ElCard,
ElForm,
ElFormItem,
ElInput,
ElOption,
ElPagination,
ElSelect,
ElTable,
ElTag,
} from 'element-plus';
import { listCustomersApi } from '#/api';
const loading = ref(false);
const table_data = ref<any[]>([]);
const total = ref(0);
const query_form = reactive({
keyword: '',
page: 1,
pageSize: 10,
status: '' as '' | 'active' | 'disabled',
});
const fetch_list = async () => {
loading.value = true;
try {
const data = await listCustomersApi({
keyword: query_form.keyword || undefined,
page: query_form.page,
pageSize: query_form.pageSize,
status: query_form.status || undefined,
});
table_data.value = data.items;
total.value = data.total;
} finally {
loading.value = false;
}
};
const handle_search = async () => {
query_form.page = 1;
await fetch_list();
};
const handle_reset = async () => {
query_form.keyword = '';
query_form.status = '';
query_form.page = 1;
await fetch_list();
};
const handle_page_change = async (page: number) => {
query_form.page = page;
await fetch_list();
};
const handle_page_size_change = async (page_size: number) => {
query_form.pageSize = page_size;
query_form.page = 1;
await fetch_list();
};
onMounted(fetch_list);
</script>
<template>
<Page
title="客户列表(示例)"
description="一个最小可运行的业务模块示例:路由 + 页面 + API + mock 接口"
>
<div class="flex flex-col gap-4">
<ElCard shadow="never">
<ElForm class="flex flex-wrap gap-x-4" inline>
<ElFormItem label="关键词">
<ElInput
v-model="query_form.keyword"
clearable
placeholder="姓名/手机号"
style="width: 220px"
@keyup.enter="handle_search"
/>
</ElFormItem>
<ElFormItem label="状态">
<ElSelect
v-model="query_form.status"
clearable
placeholder="全部"
style="width: 160px"
>
<ElOption label="启用" value="active" />
<ElOption label="禁用" value="disabled" />
</ElSelect>
</ElFormItem>
<ElFormItem>
<div class="flex items-center gap-2">
<ElButton
type="primary"
:loading="loading"
@click="handle_search"
>
查询
</ElButton>
<ElButton :disabled="loading" @click="handle_reset">
重置
</ElButton>
</div>
</ElFormItem>
</ElForm>
</ElCard>
<ElCard shadow="never">
<ElTable :data="table_data" v-loading="loading" stripe>
<ElTable.TableColumn label="客户ID" prop="id" min-width="220" />
<ElTable.TableColumn label="姓名" prop="name" min-width="140" />
<ElTable.TableColumn label="手机号" prop="phone" min-width="140" />
<ElTable.TableColumn
label="创建时间"
prop="createdAt"
min-width="180"
/>
<ElTable.TableColumn label="状态" min-width="100">
<template #default="{ row }">
<ElTag :type="row.status === 'active' ? 'success' : 'info'">
{{ row.status === 'active' ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTable.TableColumn>
</ElTable>
<div class="mt-4 flex justify-end">
<ElPagination
:current-page="query_form.page"
:page-size="query_form.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@update:current-page="handle_page_change"
@update:page-size="handle_page_size_change"
/>
</div>
</ElCard>
</div>
</Page>
</template>