From a1c6ecf2b923f158befb6572ea9732bda5bc427e Mon Sep 17 00:00:00 2001 From: liu <2302563948@qq.com> Date: Sun, 31 May 2026 03:33:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bootstrap/providers.php | 2 + database/seeders/SysUserSeeder.php | 35 ++++ .../Controllers/MemberAddressController.php | 75 +++++++ .../Http/Controllers/MemberUserController.php | 83 ++++++++ .../Requests/MemberAddressFormRequest.php | 41 ++++ .../Http/Requests/MemberUserFormRequest.php | 61 ++++++ modules/Member/Models/MemberUserModel.php | 42 ++++ modules/Member/Models/UserAddressModel.php | 31 +++ .../Providers/MemberServiceProvider.php | 14 ++ web/api/member/member_user.ts | 36 ++++ web/api/member/user_address.ts | 36 ++++ web/domain/iMemberUser.ts | 17 ++ web/domain/iUserAddress.ts | 19 ++ web/locales/en_US/index.ts | 5 + web/locales/en_US/member/address.ts | 25 +++ web/locales/en_US/member/user.ts | 25 +++ web/locales/en_US/menu.ts | 3 + web/locales/zh_CN/index.ts | 5 + web/locales/zh_CN/member/address.ts | 25 +++ web/locales/zh_CN/member/user.ts | 25 +++ web/locales/zh_CN/menu.ts | 3 + web/pages/member/address/index.tsx | 167 ++++++++++++++++ web/pages/member/user/index.tsx | 186 ++++++++++++++++++ 23 files changed, 961 insertions(+) create mode 100644 modules/Member/Http/Controllers/MemberAddressController.php create mode 100644 modules/Member/Http/Controllers/MemberUserController.php create mode 100644 modules/Member/Http/Requests/MemberAddressFormRequest.php create mode 100644 modules/Member/Http/Requests/MemberUserFormRequest.php create mode 100644 modules/Member/Models/MemberUserModel.php create mode 100644 modules/Member/Models/UserAddressModel.php create mode 100644 modules/Member/Providers/MemberServiceProvider.php create mode 100644 web/api/member/member_user.ts create mode 100644 web/api/member/user_address.ts create mode 100644 web/domain/iMemberUser.ts create mode 100644 web/domain/iUserAddress.ts create mode 100644 web/locales/en_US/member/address.ts create mode 100644 web/locales/en_US/member/user.ts create mode 100644 web/locales/zh_CN/member/address.ts create mode 100644 web/locales/zh_CN/member/user.ts create mode 100644 web/pages/member/address/index.tsx create mode 100644 web/pages/member/user/index.tsx diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 22c0ed9..2086dba 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -6,6 +6,7 @@ use Modules\Common\Providers\PaginationProvider; use Modules\SystemAgent\Providers\SystemAgentServiceProvider; use Modules\SystemTool\Providers\SystemToolServiceProvider; use Modules\SystemUser\Providers\SystemUserServiceProvider; +use Modules\Member\Providers\MemberServiceProvider; return [ AppServiceProvider::class, @@ -14,4 +15,5 @@ return [ SystemAgentServiceProvider::class, SystemUserServiceProvider::class, SystemToolServiceProvider::class, + MemberServiceProvider::class, ]; diff --git a/database/seeders/SysUserSeeder.php b/database/seeders/SysUserSeeder.php index 6ee15ca..2e3912e 100644 --- a/database/seeders/SysUserSeeder.php +++ b/database/seeders/SysUserSeeder.php @@ -359,6 +359,41 @@ class SysUserSeeder extends Seeder ] ] ], + [ + 'type' => 'menu', + 'name' => '小程序用户', + 'local' => 'menu.member', + 'icon' => 'UserOutlined', + 'key' => 'member', + 'children' => [ + [ + 'type' => 'route', + 'name' => '用户列表', + 'key' => 'member.user', + 'path' => '/member/user', + 'local' => 'menu.member.user', + 'children' => [ + ['type' => 'rule', 'name' => '查询列表', 'key' => 'member.user.query'], + ['type' => 'rule', 'name' => '新增用户', 'key' => 'member.user.create'], + ['type' => 'rule', 'name' => '修改用户', 'key' => 'member.user.update'], + ['type' => 'rule', 'name' => '删除用户', 'key' => 'member.user.delete'], + ], + ], + [ + 'type' => 'route', + 'name' => '地址管理', + 'key' => 'member.address', + 'path' => '/member/address', + 'local' => 'menu.member.address', + 'children' => [ + ['type' => 'rule', 'name' => '查询列表', 'key' => 'member.address.query'], + ['type' => 'rule', 'name' => '新增地址', 'key' => 'member.address.create'], + ['type' => 'rule', 'name' => '修改地址', 'key' => 'member.address.update'], + ['type' => 'rule', 'name' => '删除地址', 'key' => 'member.address.delete'], + ], + ], + ], + ], [ 'type' => 'route', 'name' => 'XinAdmin', diff --git a/modules/Member/Http/Controllers/MemberAddressController.php b/modules/Member/Http/Controllers/MemberAddressController.php new file mode 100644 index 0000000..ac010a1 --- /dev/null +++ b/modules/Member/Http/Controllers/MemberAddressController.php @@ -0,0 +1,75 @@ + '=', + ]; + + protected array $quickSearchField = ['name', 'mobile', 'detail']; + + #[GetRoute(authorize: 'query')] + public function query(Request $request): JsonResponse + { + $params = $request->all(); + $pageSize = $params['pageSize'] ?? 10; + $query = UserAddressModel::with('user:id,username,nickname,mobile'); + $data = $this->buildSearch($params, $query) + ->paginate($pageSize) + ->toArray(); + return $this->success($data); + } + + #[PostRoute(authorize: 'create')] + public function create(MemberAddressFormRequest $request): JsonResponse + { + $validated = $request->validated(); + UserAddressModel::create($validated); + return $this->success(); + } + + #[PutRoute( + route: '/{id}', + authorize: 'update', + where: ['id' => '[0-9]+'] + )] + public function update(int $id, MemberAddressFormRequest $request): JsonResponse + { + $validated = $request->validated(); + $model = UserAddressModel::find($id); + if (empty($model)) { + return $this->error(); + } + $model->update($validated); + return $this->success(); + } + + #[DeleteRoute( + route: '/{id}', + authorize: 'delete', + where: ['id' => '[0-9]+'] + )] + public function delete(int $id): JsonResponse + { + $model = UserAddressModel::find($id); + if (empty($model)) { + return $this->error('地址不存在'); + } + $model->delete(); + return $this->success(); + } +} diff --git a/modules/Member/Http/Controllers/MemberUserController.php b/modules/Member/Http/Controllers/MemberUserController.php new file mode 100644 index 0000000..3dd092a --- /dev/null +++ b/modules/Member/Http/Controllers/MemberUserController.php @@ -0,0 +1,83 @@ + '=', + 'gender' => '=', + ]; + + protected array $quickSearchField = ['username', 'nickname', 'mobile']; + + #[GetRoute(authorize: 'query')] + public function query(Request $request): JsonResponse + { + $params = $request->all(); + $pageSize = $params['pageSize'] ?? 10; + $query = MemberUserModel::query(); + $data = $this->buildSearch($params, $query) + ->paginate($pageSize) + ->toArray(); + return $this->success($data); + } + + #[PostRoute(authorize: 'create')] + public function create(MemberUserFormRequest $request): JsonResponse + { + $validated = $request->validated(); + $validated['password'] = Hash::make($validated['password']); + MemberUserModel::create($validated); + return $this->success(); + } + + #[PutRoute( + route: '/{id}', + authorize: 'update', + where: ['id' => '[0-9]+'] + )] + public function update(int $id, MemberUserFormRequest $request): JsonResponse + { + $validated = $request->validated(); + $model = MemberUserModel::find($id); + if (empty($model)) { + return $this->error(); + } + if (!empty($validated['password'])) { + $validated['password'] = Hash::make($validated['password']); + } else { + unset($validated['password']); + } + $model->update($validated); + return $this->success(); + } + + #[DeleteRoute( + route: '/{id}', + authorize: 'delete', + where: ['id' => '[0-9]+'] + )] + public function delete(int $id): JsonResponse + { + $user = MemberUserModel::find($id); + if (empty($user)) { + return $this->error('用户不存在'); + } + $user->delete(); + return $this->success(); + } +} diff --git a/modules/Member/Http/Requests/MemberAddressFormRequest.php b/modules/Member/Http/Requests/MemberAddressFormRequest.php new file mode 100644 index 0000000..d522b00 --- /dev/null +++ b/modules/Member/Http/Requests/MemberAddressFormRequest.php @@ -0,0 +1,41 @@ + 'required|integer|exists:user,id', + 'name' => 'required|max:30', + 'mobile' => 'required|max:20', + 'province' => 'required|max:30', + 'city' => 'required|max:30', + 'district' => 'required|max:30', + 'detail' => 'required|max:255', + 'is_default' => 'nullable|integer|in:0,1', + ]; + } + + public function messages(): array + { + return [ + 'user_id.required' => '用户ID不能为空', + 'user_id.exists' => '用户不存在', + 'name.required' => '收货人不能为空', + 'name.max' => '收货人名称不能超过30个字符', + 'mobile.required' => '联系电话不能为空', + 'mobile.max' => '联系电话不能超过20个字符', + 'province.required' => '省份不能为空', + 'city.required' => '城市不能为空', + 'district.required' => '区县不能为空', + 'detail.required' => '详细地址不能为空', + 'is_default.in' => '默认地址格式错误', + ]; + } +} diff --git a/modules/Member/Http/Requests/MemberUserFormRequest.php b/modules/Member/Http/Requests/MemberUserFormRequest.php new file mode 100644 index 0000000..148db3a --- /dev/null +++ b/modules/Member/Http/Requests/MemberUserFormRequest.php @@ -0,0 +1,61 @@ +isUpdate()) { + return [ + 'username' => 'required|unique:user,username', + 'password' => 'required|min:6', + 'nickname' => 'nullable', + 'email' => 'nullable|email', + 'mobile' => 'nullable', + 'gender' => 'nullable|integer|in:0,1,2', + 'status' => 'nullable|integer|in:0,1', + 'avatar' => 'nullable', + 'birthday' => 'nullable|date', + ]; + } + + $id = $this->route('id'); + return [ + 'username' => [ + 'required', + Rule::unique('user', 'username')->ignore($id), + ], + 'password' => 'nullable|min:6', + 'nickname' => 'nullable', + 'email' => [ + 'nullable', + 'email', + ], + 'mobile' => 'nullable', + 'gender' => 'nullable|integer|in:0,1,2', + 'status' => 'nullable|integer|in:0,1', + 'avatar' => 'nullable', + 'birthday' => 'nullable|date', + ]; + } + + public function messages(): array + { + return [ + 'username.required' => '用户名不能为空', + 'username.unique' => '用户名已存在', + 'password.required' => '密码不能为空', + 'password.min' => '密码至少6位', + 'email.email' => '邮箱格式错误', + 'gender.in' => '性别格式错误', + 'status.in' => '状态格式错误', + 'birthday.date' => '生日格式错误', + ]; + } +} diff --git a/modules/Member/Models/MemberUserModel.php b/modules/Member/Models/MemberUserModel.php new file mode 100644 index 0000000..3b6bf5e --- /dev/null +++ b/modules/Member/Models/MemberUserModel.php @@ -0,0 +1,42 @@ + 'datetime', + 'last_login_at' => 'datetime', + 'birthday' => 'date', + 'gender' => 'integer', + 'status' => 'integer', + 'balance' => 'decimal:2', + ]; +} diff --git a/modules/Member/Models/UserAddressModel.php b/modules/Member/Models/UserAddressModel.php new file mode 100644 index 0000000..79b7aee --- /dev/null +++ b/modules/Member/Models/UserAddressModel.php @@ -0,0 +1,31 @@ + 'integer', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(MemberUserModel::class, 'user_id', 'id'); + } +} diff --git a/modules/Member/Providers/MemberServiceProvider.php b/modules/Member/Providers/MemberServiceProvider.php new file mode 100644 index 0000000..00f0886 --- /dev/null +++ b/modules/Member/Providers/MemberServiceProvider.php @@ -0,0 +1,14 @@ +register(base_path('modules/Member/Http/Controllers')); + } +} diff --git a/web/api/member/member_user.ts b/web/api/member/member_user.ts new file mode 100644 index 0000000..75d1a85 --- /dev/null +++ b/web/api/member/member_user.ts @@ -0,0 +1,36 @@ +import createAxios from '@/utils/request'; + +/** 获取小程序用户列表 */ +export async function listMemberUser(params?: Record) { + return createAxios({ + url: '/member/user', + method: 'get', + params, + }); +} + +/** 创建小程序用户 */ +export async function createMemberUser(data: Record) { + return createAxios({ + url: '/member/user', + method: 'post', + data, + }); +} + +/** 更新小程序用户 */ +export async function updateMemberUser(id: number, data: Record) { + return createAxios({ + url: `/member/user/${id}`, + method: 'put', + data, + }); +} + +/** 删除小程序用户 */ +export async function deleteMemberUser(id: number) { + return createAxios({ + url: `/member/user/${id}`, + method: 'delete', + }); +} diff --git a/web/api/member/user_address.ts b/web/api/member/user_address.ts new file mode 100644 index 0000000..2b628cb --- /dev/null +++ b/web/api/member/user_address.ts @@ -0,0 +1,36 @@ +import createAxios from '@/utils/request'; + +/** 获取用户地址列表 */ +export async function listUserAddress(params?: Record) { + return createAxios({ + url: '/member/address', + method: 'get', + params, + }); +} + +/** 创建用户地址 */ +export async function createUserAddress(data: Record) { + return createAxios({ + url: '/member/address', + method: 'post', + data, + }); +} + +/** 更新用户地址 */ +export async function updateUserAddress(id: number, data: Record) { + return createAxios({ + url: `/member/address/${id}`, + method: 'put', + data, + }); +} + +/** 删除用户地址 */ +export async function deleteUserAddress(id: number) { + return createAxios({ + url: `/member/address/${id}`, + method: 'delete', + }); +} diff --git a/web/domain/iMemberUser.ts b/web/domain/iMemberUser.ts new file mode 100644 index 0000000..7edc5b0 --- /dev/null +++ b/web/domain/iMemberUser.ts @@ -0,0 +1,17 @@ +export default interface IMemberUser { + id?: number; + username?: string; + nickname?: string; + email?: string; + avatar?: string; + mobile?: string; + gender?: number; + birthday?: string; + balance?: number; + status?: number; + last_login_at?: string; + openid?: string; + unionid?: string; + created_at?: string; + updated_at?: string; +} diff --git a/web/domain/iUserAddress.ts b/web/domain/iUserAddress.ts new file mode 100644 index 0000000..4a29d54 --- /dev/null +++ b/web/domain/iUserAddress.ts @@ -0,0 +1,19 @@ +export default interface IUserAddress { + id?: number; + user_id?: number; + name?: string; + mobile?: string; + province?: string; + city?: string; + district?: string; + detail?: string; + is_default?: number; + created_at?: string; + updated_at?: string; + user?: { + id: number; + username: string; + nickname: string; + mobile: string; + }; +} diff --git a/web/locales/en_US/index.ts b/web/locales/en_US/index.ts index bda6edd..9131b5d 100644 --- a/web/locales/en_US/index.ts +++ b/web/locales/en_US/index.ts @@ -21,6 +21,9 @@ import aiChat from "./ai/chat"; import aiConversation from "./ai/conversation"; import aiAgent from "./ai/agent"; +import memberUser from "./member/user"; +import memberAddress from "./member/address"; + import userProfile from "./user/profile"; import xinForm from "./components/xin-form"; @@ -49,6 +52,8 @@ export default { ...aiChat, ...aiConversation, ...aiAgent, + ...memberUser, + ...memberAddress, ...userProfile, ...xinForm, ...xinTable, diff --git a/web/locales/en_US/member/address.ts b/web/locales/en_US/member/address.ts new file mode 100644 index 0000000..f83ddd2 --- /dev/null +++ b/web/locales/en_US/member/address.ts @@ -0,0 +1,25 @@ +export default { + "member.address.page.title": "Address Management", + "member.address.page.description": "Manage shipping addresses for mini-program users", + "member.address.id": "Address ID", + "member.address.user_id": "User", + "member.address.user_id.required": "User ID is required", + "member.address.name": "Recipient", + "member.address.name.required": "Recipient is required", + "member.address.mobile": "Contact Phone", + "member.address.mobile.required": "Contact phone is required", + "member.address.province": "Province", + "member.address.province.required": "Province is required", + "member.address.city": "City", + "member.address.city.required": "City is required", + "member.address.district": "District", + "member.address.district.required": "District is required", + "member.address.detail": "Detail Address", + "member.address.detail.required": "Detail address is required", + "member.address.full_address": "Full Address", + "member.address.is_default": "Default", + "member.address.is_default.0": "No", + "member.address.is_default.1": "Yes", + "member.address.created_at": "Created At", + "member.address.updated_at": "Updated At", +}; diff --git a/web/locales/en_US/member/user.ts b/web/locales/en_US/member/user.ts new file mode 100644 index 0000000..da228e3 --- /dev/null +++ b/web/locales/en_US/member/user.ts @@ -0,0 +1,25 @@ +export default { + "member.user.page.title": "User List", + "member.user.page.description": "Manage mini-program registered users, view user information and status", + "member.user.id": "User ID", + "member.user.avatar": "Avatar", + "member.user.username": "Username", + "member.user.username.required": "Username is required", + "member.user.nickname": "Nickname", + "member.user.mobile": "Mobile", + "member.user.email": "Email", + "member.user.gender": "Gender", + "member.user.gender.0": "Unknown", + "member.user.gender.1": "Male", + "member.user.gender.2": "Female", + "member.user.birthday": "Birthday", + "member.user.balance": "Balance", + "member.user.status": "Status", + "member.user.status.0": "Disabled", + "member.user.status.1": "Active", + "member.user.password": "Password", + "member.user.password.required": "Password is required", + "member.user.last_login_at": "Last Login", + "member.user.created_at": "Registered At", + "member.user.updated_at": "Updated At", +}; diff --git a/web/locales/en_US/menu.ts b/web/locales/en_US/menu.ts index 36ad553..d5baaaf 100644 --- a/web/locales/en_US/menu.ts +++ b/web/locales/en_US/menu.ts @@ -44,5 +44,8 @@ export default { "menu.system.storage": "Storage", "menu.system.config": "Configuration", "menu.system.ai": "AI", + "menu.member": "Mini Program Users", + "menu.member.user": "User List", + "menu.member.address": "Address Management", "menu.xin-admin": "XinAdmin", } diff --git a/web/locales/zh_CN/index.ts b/web/locales/zh_CN/index.ts index bda6edd..9131b5d 100644 --- a/web/locales/zh_CN/index.ts +++ b/web/locales/zh_CN/index.ts @@ -21,6 +21,9 @@ import aiChat from "./ai/chat"; import aiConversation from "./ai/conversation"; import aiAgent from "./ai/agent"; +import memberUser from "./member/user"; +import memberAddress from "./member/address"; + import userProfile from "./user/profile"; import xinForm from "./components/xin-form"; @@ -49,6 +52,8 @@ export default { ...aiChat, ...aiConversation, ...aiAgent, + ...memberUser, + ...memberAddress, ...userProfile, ...xinForm, ...xinTable, diff --git a/web/locales/zh_CN/member/address.ts b/web/locales/zh_CN/member/address.ts new file mode 100644 index 0000000..79402dd --- /dev/null +++ b/web/locales/zh_CN/member/address.ts @@ -0,0 +1,25 @@ +export default { + "member.address.page.title": "地址管理", + "member.address.page.description": "管理小程序用户的收货地址", + "member.address.id": "地址ID", + "member.address.user_id": "所属用户", + "member.address.user_id.required": "用户ID为必填项", + "member.address.name": "收货人", + "member.address.name.required": "收货人为必填项", + "member.address.mobile": "联系电话", + "member.address.mobile.required": "联系电话为必填项", + "member.address.province": "省", + "member.address.province.required": "省份为必填项", + "member.address.city": "市", + "member.address.city.required": "城市为必填项", + "member.address.district": "区", + "member.address.district.required": "区县为必填项", + "member.address.detail": "详细地址", + "member.address.detail.required": "详细地址为必填项", + "member.address.full_address": "完整地址", + "member.address.is_default": "默认地址", + "member.address.is_default.0": "否", + "member.address.is_default.1": "是", + "member.address.created_at": "创建时间", + "member.address.updated_at": "更新时间", +}; diff --git a/web/locales/zh_CN/member/user.ts b/web/locales/zh_CN/member/user.ts new file mode 100644 index 0000000..4d255c4 --- /dev/null +++ b/web/locales/zh_CN/member/user.ts @@ -0,0 +1,25 @@ +export default { + "member.user.page.title": "用户列表", + "member.user.page.description": "管理小程序注册用户,查看用户信息与状态", + "member.user.id": "用户ID", + "member.user.avatar": "头像", + "member.user.username": "用户名", + "member.user.username.required": "用户名为必填项", + "member.user.nickname": "昵称", + "member.user.mobile": "手机号", + "member.user.email": "邮箱", + "member.user.gender": "性别", + "member.user.gender.0": "未知", + "member.user.gender.1": "男", + "member.user.gender.2": "女", + "member.user.birthday": "生日", + "member.user.balance": "余额", + "member.user.status": "状态", + "member.user.status.0": "禁用", + "member.user.status.1": "正常", + "member.user.password": "密码", + "member.user.password.required": "密码为必填项", + "member.user.last_login_at": "最后登录", + "member.user.created_at": "注册时间", + "member.user.updated_at": "更新时间", +}; diff --git a/web/locales/zh_CN/menu.ts b/web/locales/zh_CN/menu.ts index 3af556d..cf73f49 100644 --- a/web/locales/zh_CN/menu.ts +++ b/web/locales/zh_CN/menu.ts @@ -44,5 +44,8 @@ export default { "menu.system.storage": "存储配置", "menu.system.config": "系统配置", "menu.system.ai": "AI 配置", + "menu.member": "小程序用户", + "menu.member.user": "用户列表", + "menu.member.address": "地址管理", "menu.xin-admin": "XinAdmin", }; diff --git a/web/pages/member/address/index.tsx b/web/pages/member/address/index.tsx new file mode 100644 index 0000000..bffce13 --- /dev/null +++ b/web/pages/member/address/index.tsx @@ -0,0 +1,167 @@ +import { Tag, Typography } from 'antd'; +import React from 'react'; +import XinTable from '@/components/XinTable'; +import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings'; +import type IUserAddress from '@/domain/iUserAddress'; +import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; + +const { Title, Text } = Typography; +dayjs.extend(relativeTime); + +const Table: React.FC = () => { + const { t } = useTranslation(); + + const columns: XinTableColumn[] = [ + { + title: t('member.address.id'), + dataIndex: 'id', + hideInForm: true, + sorter: true, + align: 'center', + width: 80, + }, + { + title: t('member.address.user_id'), + dataIndex: 'user_id', + valueType: 'digit', + align: 'center', + required: true, + rules: [{ required: true, message: t('member.address.user_id.required') }], + render: (_, record) => { + if (record.user) { + return `${record.user.nickname || record.user.username} (${record.user.mobile || '-'})`; + } + return `ID: ${record.user_id}`; + }, + }, + { + title: t('member.address.name'), + dataIndex: 'name', + valueType: 'text', + align: 'center', + required: true, + rules: [{ required: true, message: t('member.address.name.required') }], + }, + { + title: t('member.address.mobile'), + dataIndex: 'mobile', + valueType: 'text', + align: 'center', + required: true, + rules: [{ required: true, message: t('member.address.mobile.required') }], + }, + { + title: t('member.address.province'), + dataIndex: 'province', + valueType: 'text', + align: 'center', + required: true, + rules: [{ required: true, message: t('member.address.province.required') }], + hideInSearch: true, + }, + { + title: t('member.address.city'), + dataIndex: 'city', + valueType: 'text', + align: 'center', + required: true, + rules: [{ required: true, message: t('member.address.city.required') }], + hideInSearch: true, + }, + { + title: t('member.address.district'), + dataIndex: 'district', + valueType: 'text', + align: 'center', + required: true, + rules: [{ required: true, message: t('member.address.district.required') }], + hideInSearch: true, + }, + { + title: t('member.address.detail'), + dataIndex: 'detail', + valueType: 'text', + align: 'center', + required: true, + rules: [{ required: true, message: t('member.address.detail.required') }], + hideInSearch: true, + }, + { + title: t('member.address.full_address'), + dataIndex: 'full_address', + hideInForm: true, + hideInSearch: true, + align: 'center', + render: (_, record) => + [record.province, record.city, record.district, record.detail] + .filter(Boolean) + .join(''), + }, + { + title: t('member.address.is_default'), + dataIndex: 'is_default', + valueType: 'switch', + align: 'center', + render: (value: number) => + value === 1 ? ( + {t('member.address.is_default.1')} + ) : ( + {t('member.address.is_default.0')} + ), + }, + { + title: t('member.address.created_at'), + hideInForm: true, + hideInSearch: true, + dataIndex: 'created_at', + align: 'center', + render: (value: string) => (value ? dayjs(value).fromNow() : '-'), + }, + { + title: t('member.address.updated_at'), + hideInForm: true, + hideInSearch: true, + dataIndex: 'updated_at', + align: 'center', + render: (value: string) => (value ? dayjs(value).fromNow() : '-'), + }, + ]; + + const tableProps: XinTableProps = { + api: '/member/address', + columns, + rowKey: 'id', + accessName: 'member.address', + scroll: { x: 1400 }, + formProps: { + grid: true, + colProps: { span: 12 }, + rowProps: { gutter: [30, 0] }, + layout: 'vertical', + }, + modalProps: { + width: 800, + }, + cardProps: { + variant: 'borderless', + }, + pagination: { + size: 'small', + style: { marginBottom: 0 }, + }, + }; + + return ( + <> +
+ {t('member.address.page.title')} + {t('member.address.page.description')} +
+ {...tableProps} /> + + ); +}; + +export default Table; diff --git a/web/pages/member/user/index.tsx b/web/pages/member/user/index.tsx new file mode 100644 index 0000000..ff80b56 --- /dev/null +++ b/web/pages/member/user/index.tsx @@ -0,0 +1,186 @@ +import { Tag, Typography } from 'antd'; +import React from 'react'; +import XinTable from '@/components/XinTable'; +import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings'; +import type IMemberUser from '@/domain/iMemberUser'; +import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; + +const { Title, Text } = Typography; +dayjs.extend(relativeTime); + +const Table: React.FC = () => { + const { t } = useTranslation(); + + const columns: XinTableColumn[] = [ + { + title: t('member.user.id'), + dataIndex: 'id', + hideInForm: true, + sorter: true, + align: 'center', + width: 80, + }, + { + title: t('member.user.avatar'), + dataIndex: 'avatar', + valueType: 'text', + hideInTable: true, + hideInSearch: true, + }, + { + title: t('member.user.username'), + dataIndex: 'username', + valueType: 'text', + align: 'center', + required: true, + rules: [{ required: true, message: t('member.user.username.required') }], + }, + { + title: t('member.user.nickname'), + dataIndex: 'nickname', + valueType: 'text', + align: 'center', + }, + { + title: t('member.user.mobile'), + dataIndex: 'mobile', + valueType: 'text', + align: 'center', + }, + { + title: t('member.user.email'), + dataIndex: 'email', + valueType: 'text', + align: 'center', + }, + { + title: t('member.user.gender'), + dataIndex: 'gender', + valueType: 'radio', + align: 'center', + hideInSearch: true, + fieldProps: { + options: [ + { value: 0, label: t('member.user.gender.0') }, + { value: 1, label: t('member.user.gender.1') }, + { value: 2, label: t('member.user.gender.2') }, + ], + }, + render: (value: number) => { + const map: Record = { + 0: t('member.user.gender.0'), + 1: t('member.user.gender.1'), + 2: t('member.user.gender.2'), + }; + return map[value] || '-'; + }, + }, + { + title: t('member.user.birthday'), + dataIndex: 'birthday', + valueType: 'date', + align: 'center', + hideInSearch: true, + }, + { + title: t('member.user.balance'), + dataIndex: 'balance', + hideInForm: true, + align: 'center', + render: (value: string) => `¥${value || '0.00'}`, + }, + { + title: t('member.user.status'), + dataIndex: 'status', + valueType: 'radioButton', + fieldProps: { + options: [ + { value: 0, label: t('member.user.status.0') }, + { value: 1, label: t('member.user.status.1') }, + ], + }, + render: (value: number) => { + return value === 1 + ? {t('member.user.status.1')} + : {t('member.user.status.0')}; + }, + filters: [ + { text: t('member.user.status.0'), value: 0 }, + { text: t('member.user.status.1'), value: 1 }, + ], + align: 'center', + }, + { + title: t('member.user.password'), + dataIndex: 'password', + valueType: 'password', + hideInTable: true, + hideInSearch: true, + hideInUpdate: true, + rules: [{ required: true, message: t('member.user.password.required') }], + fieldProps: { autoComplete: 'new-password' }, + }, + { + title: t('member.user.last_login_at'), + dataIndex: 'last_login_at', + hideInForm: true, + hideInSearch: true, + align: 'center', + render: (value: string) => (value ? dayjs(value).fromNow() : '-'), + }, + { + title: t('member.user.created_at'), + hideInForm: true, + hideInSearch: true, + dataIndex: 'created_at', + align: 'center', + render: (value: string) => (value ? dayjs(value).fromNow() : '-'), + }, + { + title: t('member.user.updated_at'), + hideInForm: true, + hideInSearch: true, + dataIndex: 'updated_at', + align: 'center', + render: (value: string) => (value ? dayjs(value).fromNow() : '-'), + }, + ]; + + const tableProps: XinTableProps = { + api: '/member/user', + columns, + rowKey: 'id', + accessName: 'member.user', + scroll: { x: 1400 }, + formProps: { + grid: true, + colProps: { span: 12 }, + rowProps: { gutter: [30, 0] }, + layout: 'vertical', + }, + modalProps: { + width: 800, + }, + cardProps: { + variant: 'borderless', + }, + pagination: { + size: 'small', + style: { marginBottom: 0 }, + }, + }; + + return ( + <> +
+ {t('member.user.page.title')} + {t('member.user.page.description')} +
+ {...tableProps} /> + + ); +}; + +export default Table;