From aac626da329a51c55aa2448a57b54f599339e1f4 Mon Sep 17 00:00:00 2001 From: xingyu4j Date: Mon, 18 May 2026 21:45:17 +0800 Subject: [PATCH] feat: add system user view --- apps/backend-mock/api/system/user/list.ts | 85 +++++++ playground/src/api/system/index.ts | 1 + playground/src/api/system/user.ts | 55 +++++ .../src/locales/langs/en-US/system.json | 13 + .../src/locales/langs/zh-CN/system.json | 13 + .../src/router/routes/modules/system.ts | 9 + playground/src/views/system/user/data.ts | 135 +++++++++++ playground/src/views/system/user/list.vue | 222 ++++++++++++++++++ .../src/views/system/user/modules/form.vue | 138 +++++++++++ 9 files changed, 671 insertions(+) create mode 100644 apps/backend-mock/api/system/user/list.ts create mode 100644 playground/src/api/system/user.ts create mode 100644 playground/src/views/system/user/data.ts create mode 100644 playground/src/views/system/user/list.vue create mode 100644 playground/src/views/system/user/modules/form.vue diff --git a/apps/backend-mock/api/system/user/list.ts b/apps/backend-mock/api/system/user/list.ts new file mode 100644 index 000000000..21d44c8ba --- /dev/null +++ b/apps/backend-mock/api/system/user/list.ts @@ -0,0 +1,85 @@ +import { faker } from '@faker-js/faker'; +import { eventHandler, getQuery } from 'h3'; +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; + +const formatterCN = new Intl.DateTimeFormat('zh-CN', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', +}); + +function generateMockDataList(count: number) { + const dataList = []; + + for (let i = 0; i < count; i++) { + const dataItem: Record = { + id: faker.string.uuid(), + name: faker.commerce.product(), + status: faker.helpers.arrayElement([0, 1]), + createTime: formatterCN.format( + faker.date.between({ from: '2022-01-01', to: '2025-01-01' }), + ), + deptId: faker.string.uuid(), + remark: faker.lorem.sentence(), + }; + + dataList.push(dataItem); + } + + return dataList; +} + +const mockData = generateMockDataList(100); + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + + const { + page = 1, + pageSize = 20, + name, + id, + remark, + startTime, + endTime, + deptId, + status, + } = getQuery(event); + let listData = structuredClone(mockData); + if (name) { + listData = listData.filter((item) => + item.name.toLowerCase().includes(String(name).toLowerCase()), + ); + } + if (id) { + listData = listData.filter((item) => + item.id.toLowerCase().includes(String(id).toLowerCase()), + ); + } + if (remark) { + listData = listData.filter((item) => + item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()), + ); + } + if (startTime) { + listData = listData.filter((item) => item.createTime >= startTime); + } + if (endTime) { + listData = listData.filter((item) => item.createTime <= endTime); + } + if (['0', '1'].includes(status as string)) { + listData = listData.filter((item) => item.status === Number(status)); + } + if (deptId) { + listData = listData.filter((item) => item.deptId === deptId); + } + return usePageResponseSuccess(page as string, pageSize as string, listData); +}); diff --git a/playground/src/api/system/index.ts b/playground/src/api/system/index.ts index f2a248f10..1b28fb644 100644 --- a/playground/src/api/system/index.ts +++ b/playground/src/api/system/index.ts @@ -1,3 +1,4 @@ export * from './dept'; export * from './menu'; export * from './role'; +export * from './user'; diff --git a/playground/src/api/system/user.ts b/playground/src/api/system/user.ts new file mode 100644 index 000000000..40a1b8068 --- /dev/null +++ b/playground/src/api/system/user.ts @@ -0,0 +1,55 @@ +import type { Recordable } from '@vben/types'; + +import { requestClient } from '#/api/request'; + +export namespace SystemUserApi { + export interface SystemUser { + [key: string]: any; + id: string; + name: string; + permissions: string[]; + remark?: string; + status: 0 | 1; + } +} + +/** + * 获取用户列表数据 + */ +async function getUserList(params: Recordable) { + return requestClient.get>( + '/system/user/list', + { params }, + ); +} + +/** + * 创建用户 + * @param data 用户数据 + */ +async function createUser(data: Omit) { + return requestClient.post('/system/user', data); +} + +/** + * 更新用户 + * + * @param id 用户 ID + * @param data 用户数据 + */ +async function updateUser( + id: string, + data: Omit, +) { + return requestClient.put(`/system/user/${id}`, data); +} + +/** + * 删除用户 + * @param id 用户 ID + */ +async function deleteUser(id: string) { + return requestClient.delete(`/system/user/${id}`); +} + +export { createUser, deleteUser, getUserList, updateUser }; diff --git a/playground/src/locales/langs/en-US/system.json b/playground/src/locales/langs/en-US/system.json index 003dfbbed..61ed76cb1 100644 --- a/playground/src/locales/langs/en-US/system.json +++ b/playground/src/locales/langs/en-US/system.json @@ -1,5 +1,18 @@ { "title": "System Management", + "user": { + "title": "User Management", + "list": "User List", + "name": "User", + "userName": "User Name", + "id": "User ID", + "dept": "Department", + "status": "Status", + "remark": "Remark", + "createTime": "Creation Time", + "operation": "Operation", + "placeholder": "Search Department..." + }, "dept": { "name": "Department", "title": "Department Management", diff --git a/playground/src/locales/langs/zh-CN/system.json b/playground/src/locales/langs/zh-CN/system.json index be5a7ae36..9e29248d2 100644 --- a/playground/src/locales/langs/zh-CN/system.json +++ b/playground/src/locales/langs/zh-CN/system.json @@ -1,5 +1,18 @@ { "title": "系统管理", + "user": { + "title": "用户管理", + "list": "用户列表", + "name": "用户名", + "userName": "用户名称", + "id": "用户ID", + "dept": "部门", + "status": "状态", + "remark": "备注", + "createTime": "创建时间", + "operation": "操作", + "placeholder": "搜索部门..." + }, "dept": { "list": "部门列表", "createTime": "创建时间", diff --git a/playground/src/router/routes/modules/system.ts b/playground/src/router/routes/modules/system.ts index e1bf71255..53a15bb13 100644 --- a/playground/src/router/routes/modules/system.ts +++ b/playground/src/router/routes/modules/system.ts @@ -12,6 +12,15 @@ const routes: RouteRecordRaw[] = [ name: 'System', path: '/system', children: [ + { + path: '/system/user', + name: 'SystemUser', + meta: { + icon: 'mdi:user', + title: $t('system.user.title'), + }, + component: () => import('#/views/system/user/list.vue'), + }, { path: '/system/role', name: 'SystemRole', diff --git a/playground/src/views/system/user/data.ts b/playground/src/views/system/user/data.ts new file mode 100644 index 000000000..f9dd24e8d --- /dev/null +++ b/playground/src/views/system/user/data.ts @@ -0,0 +1,135 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn, VxeTableGridColumns } from '#/adapter/vxe-table'; +import type { SystemUserApi } from '#/api'; + +import { getDeptList } from '#/api'; +import { $t } from '#/locales'; + +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'name', + label: $t('system.user.name'), + rules: 'required', + }, + { + component: 'ApiTreeSelect', + componentProps: { + allowClear: true, + api: getDeptList, + class: 'w-full', + labelField: 'name', + valueField: 'id', + childrenField: 'children', + }, + fieldName: 'deptId', + label: $t('system.user.dept'), + rules: 'required', + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: [ + { label: $t('common.enabled'), value: 1 }, + { label: $t('common.disabled'), value: 0 }, + ], + optionType: 'button', + }, + defaultValue: 1, + fieldName: 'status', + label: $t('system.user.status'), + }, + { + component: 'Textarea', + fieldName: 'remark', + label: $t('system.user.remark'), + }, + ]; +} + +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'name', + label: $t('system.user.name'), + }, + { component: 'Input', fieldName: 'id', label: $t('system.user.id') }, + { + component: 'Select', + componentProps: { + allowClear: true, + options: [ + { label: $t('common.enabled'), value: 1 }, + { label: $t('common.disabled'), value: 0 }, + ], + }, + fieldName: 'status', + label: $t('system.user.status'), + }, + { + component: 'Input', + fieldName: 'remark', + label: $t('system.user.remark'), + }, + { + component: 'RangePicker', + fieldName: 'createTime', + label: $t('system.user.createTime'), + }, + ]; +} + +export function useColumns( + onActionClick: OnActionClickFn, + onStatusChange?: (newStatus: any, row: T) => PromiseLike, +): VxeTableGridColumns { + return [ + { + field: 'name', + title: $t('system.user.name'), + width: 200, + }, + { + field: 'id', + title: $t('system.user.id'), + width: 200, + }, + { + cellRender: { + attrs: { beforeChange: onStatusChange }, + name: onStatusChange ? 'CellSwitch' : 'CellTag', + }, + field: 'status', + title: $t('system.user.status'), + width: 100, + }, + { + field: 'remark', + minWidth: 100, + title: $t('system.user.remark'), + }, + { + field: 'createTime', + title: $t('system.user.createTime'), + width: 200, + }, + { + align: 'center', + cellRender: { + attrs: { + nameField: 'name', + nameTitle: $t('system.user.name'), + onClick: onActionClick, + }, + name: 'CellOperation', + }, + field: 'operation', + fixed: 'right', + title: $t('system.user.operation'), + width: 130, + }, + ]; +} diff --git a/playground/src/views/system/user/list.vue b/playground/src/views/system/user/list.vue new file mode 100644 index 000000000..c0e2c4fa6 --- /dev/null +++ b/playground/src/views/system/user/list.vue @@ -0,0 +1,222 @@ + + diff --git a/playground/src/views/system/user/modules/form.vue b/playground/src/views/system/user/modules/form.vue new file mode 100644 index 000000000..70d13eedc --- /dev/null +++ b/playground/src/views/system/user/modules/form.vue @@ -0,0 +1,138 @@ + + +