Showing
8 changed files
with
304 additions
and
0 deletions
apps/backend-mock/api/biz/customers/list.ts
0 → 100644
| 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 | +}); |
apps/web-ele/src/api/biz/customers.ts
0 → 100644
| 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 | +} |
apps/web-ele/src/api/biz/index.ts
0 → 100644
| 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 | } | ... | ... |
| 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> |
-
Please register or login to post a comment