diff --git a/app/Http/Controllers/Client/BannerController.php b/app/Http/Controllers/Client/BannerController.php new file mode 100644 index 0000000..526c1b1 --- /dev/null +++ b/app/Http/Controllers/Client/BannerController.php @@ -0,0 +1,90 @@ + '=', + ]; + + protected array $quickSearchField = ['title']; + + /** 轮播图列表(默认按排序升序) */ + #[GetRoute(authorize: 'query')] + public function query(Request $request): JsonResponse + { + $params = $request->all(); + $pageSize = $params['pageSize'] ?? 10; + $data = $this->buildSearch($params, HomeBannerModel::query()) + ->orderBy('sort') + ->orderBy('id') + ->paginate($pageSize) + ->toArray(); + return $this->success($data); + } + + /** 上传轮播图片(文件分组:客户端配置) */ + #[PostRoute('/upload', 'create')] + public function uploadImage(Request $request, SysFileService $service): JsonResponse + { + $data = $request->validate(['file' => 'required|file']); + $result = $service->upload( + $data['file'], + 12, + 20, + Auth::id() + ); + return $this->success($result); + } + + /** 新增轮播图 */ + #[PostRoute(authorize: 'create')] + public function create(BannerFormRequest $request): JsonResponse + { + HomeBannerModel::create($request->validated()); + return $this->success(); + } + + /** 编辑轮播图 */ + #[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] + public function update(int $id, BannerFormRequest $request): JsonResponse + { + $model = HomeBannerModel::find($id); + if (empty($model)) { + throw new RepositoryException('轮播图不存在'); + } + $model->update($request->validated()); + return $this->success(); + } + + /** 删除轮播图 */ + #[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])] + public function delete(int $id): JsonResponse + { + $model = HomeBannerModel::find($id); + if (empty($model)) { + throw new RepositoryException('轮播图不存在'); + } + $model->delete(); + return $this->success(); + } +} diff --git a/app/Http/Controllers/Client/NavController.php b/app/Http/Controllers/Client/NavController.php new file mode 100644 index 0000000..c6f7d16 --- /dev/null +++ b/app/Http/Controllers/Client/NavController.php @@ -0,0 +1,90 @@ + '=', + ]; + + protected array $quickSearchField = ['name']; + + /** 导航列表(默认按排序升序) */ + #[GetRoute(authorize: 'query')] + public function query(Request $request): JsonResponse + { + $params = $request->all(); + $pageSize = $params['pageSize'] ?? 10; + $data = $this->buildSearch($params, HomeNavModel::query()) + ->orderBy('sort') + ->orderBy('id') + ->paginate($pageSize) + ->toArray(); + return $this->success($data); + } + + /** 上传导航图标(文件分组:客户端配置) */ + #[PostRoute('/upload', 'create')] + public function uploadImage(Request $request, SysFileService $service): JsonResponse + { + $data = $request->validate(['file' => 'required|file']); + $result = $service->upload( + $data['file'], + 12, + 20, + Auth::id() + ); + return $this->success($result); + } + + /** 新增导航 */ + #[PostRoute(authorize: 'create')] + public function create(NavFormRequest $request): JsonResponse + { + HomeNavModel::create($request->validated()); + return $this->success(); + } + + /** 编辑导航 */ + #[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] + public function update(int $id, NavFormRequest $request): JsonResponse + { + $model = HomeNavModel::find($id); + if (empty($model)) { + throw new RepositoryException('导航不存在'); + } + $model->update($request->validated()); + return $this->success(); + } + + /** 删除导航 */ + #[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])] + public function delete(int $id): JsonResponse + { + $model = HomeNavModel::find($id); + if (empty($model)) { + throw new RepositoryException('导航不存在'); + } + $model->delete(); + return $this->success(); + } +} diff --git a/app/Http/Controllers/Client/PromoController.php b/app/Http/Controllers/Client/PromoController.php new file mode 100644 index 0000000..ffe38cd --- /dev/null +++ b/app/Http/Controllers/Client/PromoController.php @@ -0,0 +1,90 @@ + '=', + ]; + + protected array $quickSearchField = ['title', 'sub_title']; + + /** 促销卡片列表(默认按排序升序) */ + #[GetRoute(authorize: 'query')] + public function query(Request $request): JsonResponse + { + $params = $request->all(); + $pageSize = $params['pageSize'] ?? 10; + $data = $this->buildSearch($params, HomePromoModel::query()) + ->orderBy('sort') + ->orderBy('id') + ->paginate($pageSize) + ->toArray(); + return $this->success($data); + } + + /** 上传卡片图片(文件分组:客户端配置) */ + #[PostRoute('/upload', 'create')] + public function uploadImage(Request $request, SysFileService $service): JsonResponse + { + $data = $request->validate(['file' => 'required|file']); + $result = $service->upload( + $data['file'], + 12, + 20, + Auth::id() + ); + return $this->success($result); + } + + /** 新增促销卡片 */ + #[PostRoute(authorize: 'create')] + public function create(PromoFormRequest $request): JsonResponse + { + HomePromoModel::create($request->validated()); + return $this->success(); + } + + /** 编辑促销卡片 */ + #[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] + public function update(int $id, PromoFormRequest $request): JsonResponse + { + $model = HomePromoModel::find($id); + if (empty($model)) { + throw new RepositoryException('促销卡片不存在'); + } + $model->update($request->validated()); + return $this->success(); + } + + /** 删除促销卡片 */ + #[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])] + public function delete(int $id): JsonResponse + { + $model = HomePromoModel::find($id); + if (empty($model)) { + throw new RepositoryException('促销卡片不存在'); + } + $model->delete(); + return $this->success(); + } +} diff --git a/app/Http/Controllers/Mini/HomeController.php b/app/Http/Controllers/Mini/HomeController.php new file mode 100644 index 0000000..f8f0c9e --- /dev/null +++ b/app/Http/Controllers/Mini/HomeController.php @@ -0,0 +1,46 @@ +where('status', HomeBannerModel::STATUS_NORMAL) + ->orderBy('sort') + ->orderBy('id') + ->get(['id', 'title', 'image_id', 'link', 'sort']); + + $navs = HomeNavModel::query() + ->where('status', HomeNavModel::STATUS_NORMAL) + ->orderBy('sort') + ->orderBy('id') + ->get(['id', 'name', 'image_id', 'link', 'sort']); + + $promos = HomePromoModel::query() + ->where('status', HomePromoModel::STATUS_NORMAL) + ->orderBy('sort') + ->orderBy('id') + ->get(['id', 'title', 'sub_title', 'image_id', 'link', 'sort']); + + return $this->success([ + 'banners' => $banners, + 'navs' => $navs, + 'promos' => $promos, + ]); + } +} diff --git a/app/Http/Controllers/Mini/ProductController.php b/app/Http/Controllers/Mini/ProductController.php index e949fe2..bae55d6 100644 --- a/app/Http/Controllers/Mini/ProductController.php +++ b/app/Http/Controllers/Mini/ProductController.php @@ -57,10 +57,8 @@ class ProductController extends BaseMiniController /** * 商品列表:价格取当前门店客户等级价; - * 未绑定门店/门店未设客户等级 → 仍可浏览,price 为 null(不可见价格,加购/下单另由前置校验拦截); - * ?category_id=&keyword=&page=&pageSize= */ - #[GetRoute('/product/list', authorize: true)] + #[GetRoute('/product/list', authorize: false)] public function products(Request $request): JsonResponse { $user = $this->currentUser($request); diff --git a/app/Http/Requests/Client/BannerFormRequest.php b/app/Http/Requests/Client/BannerFormRequest.php new file mode 100644 index 0000000..15d9420 --- /dev/null +++ b/app/Http/Requests/Client/BannerFormRequest.php @@ -0,0 +1,35 @@ + 'required|string|max:50', + 'image_id' => 'required|integer|min:1', + 'link' => 'nullable|string|max:255', + 'sort' => 'nullable|integer|min:0', + 'status' => 'required|integer|in:0,1', + ]; + } + + public function messages(): array + { + return [ + 'title.required' => '轮播图标题不能为空', + 'title.max' => '轮播图标题最长 50 个字符', + 'image_id.required' => '请上传轮播图片', + 'link.max' => '跳转链接最长 255 个字符', + 'status.in' => '状态只能是 0 或 1', + ]; + } +} diff --git a/app/Http/Requests/Client/NavFormRequest.php b/app/Http/Requests/Client/NavFormRequest.php new file mode 100644 index 0000000..fbd2306 --- /dev/null +++ b/app/Http/Requests/Client/NavFormRequest.php @@ -0,0 +1,35 @@ + 'required|string|max:50', + 'image_id' => 'required|integer|min:1', + 'link' => 'nullable|string|max:255', + 'sort' => 'nullable|integer|min:0', + 'status' => 'required|integer|in:0,1', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => '导航名称不能为空', + 'name.max' => '导航名称最长 50 个字符', + 'image_id.required' => '请上传导航图标', + 'link.max' => '跳转链接最长 255 个字符', + 'status.in' => '状态只能是 0 或 1', + ]; + } +} diff --git a/app/Http/Requests/Client/PromoFormRequest.php b/app/Http/Requests/Client/PromoFormRequest.php new file mode 100644 index 0000000..a966cc7 --- /dev/null +++ b/app/Http/Requests/Client/PromoFormRequest.php @@ -0,0 +1,37 @@ + 'nullable|string|max:50', + 'sub_title' => 'nullable|string|max:100', + 'image_id' => 'required|integer|min:1', + 'link' => 'nullable|string|max:255', + 'sort' => 'nullable|integer|min:0', + 'status' => 'required|integer|in:0,1', + ]; + } + + public function messages(): array + { + return [ + 'title.required' => '卡片标题不能为空', + 'title.max' => '卡片标题最长 50 个字符', + 'sub_title.max' => '副标题最长 100 个字符', + 'image_id.required' => '请上传卡片图片', + 'link.max' => '跳转链接最长 255 个字符', + 'status.in' => '状态只能是 0 或 1', + ]; + } +} diff --git a/app/Models/HomeBannerModel.php b/app/Models/HomeBannerModel.php new file mode 100644 index 0000000..45af9f4 --- /dev/null +++ b/app/Models/HomeBannerModel.php @@ -0,0 +1,57 @@ + 'integer', + 'sort' => 'integer', + 'status' => 'integer', + 'created_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + ]; + + protected $appends = ['image_url']; + + protected $with = ['image']; + + /** + * 关联图片 + */ + public function image(): HasOne + { + return $this->hasOne(SysFileModel::class, 'id', 'image_id'); + } + + /** + * 图片链接 + */ + public function getImageUrlAttribute(): ?string + { + return $this->image?->preview_url; + } +} diff --git a/app/Models/HomeNavModel.php b/app/Models/HomeNavModel.php new file mode 100644 index 0000000..d86b9b3 --- /dev/null +++ b/app/Models/HomeNavModel.php @@ -0,0 +1,57 @@ + 'integer', + 'sort' => 'integer', + 'status' => 'integer', + 'created_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + ]; + + protected $appends = ['image_url']; + + protected $with = ['image']; + + /** + * 关联图标 + */ + public function image(): HasOne + { + return $this->hasOne(SysFileModel::class, 'id', 'image_id'); + } + + /** + * 图标链接 + */ + public function getImageUrlAttribute(): ?string + { + return $this->image?->preview_url; + } +} diff --git a/app/Models/HomePromoModel.php b/app/Models/HomePromoModel.php new file mode 100644 index 0000000..d910c33 --- /dev/null +++ b/app/Models/HomePromoModel.php @@ -0,0 +1,58 @@ + 'integer', + 'sort' => 'integer', + 'status' => 'integer', + 'created_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + ]; + + protected $appends = ['image_url']; + + protected $with = ['image']; + + /** + * 关联图片 + */ + public function image(): HasOne + { + return $this->hasOne(SysFileModel::class, 'id', 'image_id'); + } + + /** + * 图片链接 + */ + public function getImageUrlAttribute(): ?string + { + return $this->image?->preview_url; + } +} diff --git a/database/migrations/2026_08_14_000003_create_mini_home_table.php b/database/migrations/2026_08_14_000003_create_mini_home_table.php new file mode 100644 index 0000000..dbdffa7 --- /dev/null +++ b/database/migrations/2026_08_14_000003_create_mini_home_table.php @@ -0,0 +1,76 @@ +increments('id')->comment('轮播图ID'); + $table->string('title', 50)->nullable()->comment('标题'); + $table->integer('image_id')->nullable()->default(0)->comment('轮播图片(sys_file ID)'); + $table->string('link', 255)->nullable()->default('')->comment('小程序跳转链接(如 /pages/goods/detail?id=1)'); + $table->integer('sort')->default(0)->comment('排序(越小越靠前)'); + $table->integer('status')->default(1)->comment('状态(1正常 0停用)'); + $table->timestamps(); + $table->comment('小程序首页轮播图'); + }); + } + + if (! Schema::hasTable('mini_home_nav')) { + Schema::create('mini_home_nav', function (Blueprint $table) { + $table->increments('id')->comment('导航ID'); + $table->string('name', 50)->nullable()->comment('导航名称'); + $table->integer('image_id')->nullable()->default(0)->comment('导航图标(sys_file ID)'); + $table->string('link', 255)->nullable()->default('')->comment('小程序跳转链接'); + $table->integer('sort')->default(0)->comment('排序(越小越靠前)'); + $table->integer('status')->default(1)->comment('状态(1正常 0停用)'); + $table->timestamps(); + $table->comment('小程序首页宫格导航'); + }); + } + + if (! Schema::hasTable('mini_home_promo')) { + Schema::create('mini_home_promo', function (Blueprint $table) { + $table->increments('id')->comment('卡片ID'); + $table->string('title', 50)->nullable()->comment('卡片标题'); + $table->string('sub_title', 100)->nullable()->default('')->comment('副标题/促销文案'); + $table->integer('image_id')->nullable()->default(0)->comment('卡片图片(sys_file ID)'); + $table->string('link', 255)->nullable()->default('')->comment('小程序跳转链接'); + $table->integer('sort')->default(0)->comment('排序(越小越靠前)'); + $table->integer('status')->default(1)->comment('状态(1正常 0停用)'); + $table->timestamps(); + $table->comment('小程序首页促销推荐卡片'); + }); + } + + // 文件分组:客户端配置图片(幂等,已有库补充分组记录) + DB::table('sys_file_group')->insertOrIgnore([ + 'id' => 12, + 'name' => '客户端配置', + 'sort' => 11, + 'describe' => '小程序首页轮播图/宫格导航/促销卡片图片', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('mini_home_promo'); + Schema::dropIfExists('mini_home_nav'); + Schema::dropIfExists('mini_home_banner'); + } +}; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index 49f0833..455f756 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -264,6 +264,50 @@ class PermissionSeeder extends Seeder ], ], ], + [ + 'type' => 'menu', + 'key' => 'procurement.client', + 'name' => '客户端配置', + 'icon' => 'MobileOutlined', + 'children' => [ + [ + 'type' => 'route', + 'key' => 'client.banner', + 'name' => '首页轮播图', + 'path' => '/client/banner', + 'children' => [ + ['type' => 'rule', 'key' => 'client.banner.query', 'name' => '查询'], + ['type' => 'rule', 'key' => 'client.banner.create', 'name' => '新增'], + ['type' => 'rule', 'key' => 'client.banner.update', 'name' => '编辑'], + ['type' => 'rule', 'key' => 'client.banner.delete', 'name' => '删除'], + ], + ], + [ + 'type' => 'route', + 'key' => 'client.nav', + 'name' => '宫格导航', + 'path' => '/client/nav', + 'children' => [ + ['type' => 'rule', 'key' => 'client.nav.query', 'name' => '查询'], + ['type' => 'rule', 'key' => 'client.nav.create', 'name' => '新增'], + ['type' => 'rule', 'key' => 'client.nav.update', 'name' => '编辑'], + ['type' => 'rule', 'key' => 'client.nav.delete', 'name' => '删除'], + ], + ], + [ + 'type' => 'route', + 'key' => 'client.promo', + 'name' => '促销推荐卡片', + 'path' => '/client/promo', + 'children' => [ + ['type' => 'rule', 'key' => 'client.promo.query', 'name' => '查询'], + ['type' => 'rule', 'key' => 'client.promo.create', 'name' => '新增'], + ['type' => 'rule', 'key' => 'client.promo.update', 'name' => '编辑'], + ['type' => 'rule', 'key' => 'client.promo.delete', 'name' => '删除'], + ], + ], + ], + ], [ 'type' => 'menu', 'name' => 'AI', diff --git a/database/seeders/SysDataSeeder.php b/database/seeders/SysDataSeeder.php index 7acacb9..2c1c9c9 100644 --- a/database/seeders/SysDataSeeder.php +++ b/database/seeders/SysDataSeeder.php @@ -70,6 +70,7 @@ class SysDataSeeder extends Seeder ['id' => 9, 'name' => '分类图片', 'sort' => 8, 'describe' => '存放商品分类图标', 'created_at' => $date, 'updated_at' => $date], ['id' => 10, 'name' => '商品封面图片', 'sort' => 9, 'describe' => '存放商品封面图片', 'created_at' => $date, 'updated_at' => $date], ['id' => 11, 'name' => '商品详情图片', 'sort' => 10, 'describe' => '存放商品详情图片', 'created_at' => $date, 'updated_at' => $date], + ['id' => 12, 'name' => '客户端配置', 'sort' => 11, 'describe' => '小程序首页轮播图/宫格导航/促销卡片图片', 'created_at' => $date, 'updated_at' => $date], ]); } } diff --git a/docs/mini-home-api.md b/docs/mini-home-api.md new file mode 100644 index 0000000..e00d2fd --- /dev/null +++ b/docs/mini-home-api.md @@ -0,0 +1,172 @@ +# 小程序首页接口文档 + +> 适用端:微信小程序(h5 仓库,Taro) +> 后台配置入口:PC 后台「客户端配置」菜单(首页轮播图 / 宫格导航 / 促销推荐卡片) +> 更新日期:2026-08-14 + +## 通用约定 + +| 项 | 说明 | +|---|---| +| 根地址 | `BASE_URL`(见 `src/utils/request.ts`,如 `http://localhost:8000/index.php`) | +| 认证 | 请求头 `Authorization: Bearer {token}`,token 来自登录接口,本地存储 key `auth_token` | +| 响应包络 | `{ success: boolean, data: T, msg?: string, showType?: number }` | +| 失败处理 | `success=false` 时 `msg` 为中文错误信息;HTTP 401 表示登录过期,需重新登录 | + +## GET /mini/home + +首页配置聚合接口:一次返回轮播图、宫格导航、促销推荐卡片三组数据,均为**启用状态(status=1)**且按 `sort` 升序(越小越靠前)。 + +- **权限**:需登录(Bearer token) +- **请求参数**:无 + +### 响应示例 + +```json +{ + "success": true, + "msg": "ok", + "data": { + "banners": [ + { + "id": 1, + "title": "新鲜直采", + "image_id": 123, + "link": "/pages/goods/detail?id=1", + "sort": 0, + "image_url": "http://localhost:8000/storage/uploads/2026/08/14/xxx.jpg" + } + ], + "navs": [ + { + "id": 1, + "name": "蔬菜专区", + "image_id": 124, + "link": "/pages/category/index?id=1", + "sort": 0, + "image_url": "http://localhost:8000/storage/uploads/2026/08/14/yyy.png" + } + ], + "promos": [ + { + "id": 1, + "title": "限时特惠", + "sub_title": "全场 8 折起", + "image_id": 125, + "link": "/pages/promo/detail?id=1", + "sort": 0, + "image_url": "http://localhost:8000/storage/uploads/2026/08/14/zzz.jpg" + } + ] + } +} +``` + +### 字段说明 + +**banners(轮播图)** + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | number | 轮播图 ID | +| title | string | 标题(后台维护,可用于无障碍/占位) | +| image_id | number | 图片文件 ID(sys_file),一般无需使用 | +| **image_url** | string \| null | 轮播图片完整 URL,直接用于 ``;未传图时为 null | +| link | string | 小程序页面跳转路径,**空字符串表示点击不跳转** | +| sort | number | 排序值(已按此升序返回,前端无需再排) | + +**navs(宫格导航)** + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | number | 导航 ID | +| name | string | 导航名称(宫格文字) | +| **image_url** | string \| null | 导航图标完整 URL | +| link | string | 小程序页面跳转路径,空字符串不跳转 | +| sort | number | 排序值 | + +**promos(促销推荐卡片)** + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | number | 卡片 ID | +| title | string | 卡片标题 | +| sub_title | string | 副标题/促销文案,可能为空字符串 | +| **image_url** | string \| null | 卡片图片完整 URL | +| link | string | 小程序页面跳转路径,空字符串不跳转 | +| sort | number | 排序值 | + +### 前端接入示例 + +`src/services/home.ts`(新增): + +```ts +import { get } from '@/utils/request' + +/** 首页轮播图项 */ +export interface HomeBanner { + id: number + title: string + image_id: number + image_url: string | null + link: string + sort: number +} + +/** 首页宫格导航项 */ +export interface HomeNav { + id: number + name: string + image_id: number + image_url: string | null + link: string + sort: number +} + +/** 首页促销推荐卡片 */ +export interface HomePromo { + id: number + title: string + sub_title: string + image_id: number + image_url: string | null + link: string + sort: number +} + +/** 首页配置聚合数据 */ +export interface HomeConfig { + banners: HomeBanner[] + navs: HomeNav[] + promos: HomePromo[] +} + +/** 首页配置(轮播图 + 宫格导航 + 促销卡片):GET /mini/home */ +export function getHomeConfigApi() { + return get('/mini/home') +} +``` + +页面中使用(跳转需兼容空链接): + +```tsx +const [config, setConfig] = useState({ banners: [], navs: [], promos: [] }) + +useEffect(() => { + getHomeConfigApi().then(res => setConfig(res.data)) +}, []) + +/** 统一跳转:link 为空不跳转 */ +function handleLink(link: string) { + if (!link) return + Taro.navigateTo({ url: link }) +} +``` + +### 注意事项 + +1. **三组数据均可能为空数组**(后台未配置或全部停用),页面需做空态处理。 +2. `image_url` 可能为 `null`(后台未上传图片),渲染前判空。 +3. `link` 为小程序内部页面路径(以 `/` 开头),用 `Taro.navigateTo` 跳转;若目标为 tabBar 页面需改用 `Taro.switchTab`(建议后台配置时避免填 tabBar 路径)。 +4. 数据实时生效:后台修改后,小程序下次进入首页请求即为最新内容,无缓存。 +5. 接口需登录后调用;未登录(401)会由 request 封装自动跳登录页。 diff --git a/web/domain/iHomeBanner.ts b/web/domain/iHomeBanner.ts new file mode 100644 index 0000000..a853f58 --- /dev/null +++ b/web/domain/iHomeBanner.ts @@ -0,0 +1,20 @@ +import type {ISysFileInfo} from "@/domain/iSysFile.ts"; + +/** 小程序首页轮播图 */ +export default interface IHomeBanner { + id?: number; + title?: string; + image_id?: number; + image?: ISysFileInfo; + image_url?: string; + link?: string; + sort?: number; + status?: number; + created_at?: string; + updated_at?: string; +} + +export const HOME_BANNER_STATUS_MAP: Record = { + 0: { text: '停用', color: 'error' }, + 1: { text: '正常', color: 'success' }, +}; diff --git a/web/domain/iHomeNav.ts b/web/domain/iHomeNav.ts new file mode 100644 index 0000000..e86af8f --- /dev/null +++ b/web/domain/iHomeNav.ts @@ -0,0 +1,20 @@ +import type {ISysFileInfo} from "@/domain/iSysFile.ts"; + +/** 小程序首页宫格导航 */ +export default interface IHomeNav { + id?: number; + name?: string; + image_id?: number; + image?: ISysFileInfo; + image_url?: string; + link?: string; + sort?: number; + status?: number; + created_at?: string; + updated_at?: string; +} + +export const HOME_NAV_STATUS_MAP: Record = { + 0: { text: '停用', color: 'error' }, + 1: { text: '正常', color: 'success' }, +}; diff --git a/web/domain/iHomePromo.ts b/web/domain/iHomePromo.ts new file mode 100644 index 0000000..e5a7eaa --- /dev/null +++ b/web/domain/iHomePromo.ts @@ -0,0 +1,21 @@ +import type {ISysFileInfo} from "@/domain/iSysFile.ts"; + +/** 小程序首页促销推荐卡片 */ +export default interface IHomePromo { + id?: number; + title?: string; + sub_title?: string; + image_id?: number; + image?: ISysFileInfo; + image_url?: string; + link?: string; + sort?: number; + status?: number; + created_at?: string; + updated_at?: string; +} + +export const HOME_PROMO_STATUS_MAP: Record = { + 0: { text: '停用', color: 'error' }, + 1: { text: '正常', color: 'success' }, +}; diff --git a/web/pages/client/banner.tsx b/web/pages/client/banner.tsx new file mode 100644 index 0000000..770ec01 --- /dev/null +++ b/web/pages/client/banner.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { Image, Tag, Typography } from 'antd'; +import XinTable from '@/components/XinTable'; +import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts'; +import type IHomeBanner from '@/domain/iHomeBanner.ts'; +import { HOME_BANNER_STATUS_MAP } from '@/domain/iHomeBanner.ts'; + +const { Title, Text } = Typography; + +/** + * 首页轮播图配置(小程序首页) + */ +const HomeBannerPage: React.FC = () => { + const columns: XinTableColumn[] = [ + { + title: 'ID', + dataIndex: 'id', + hideInForm: true, + hideInSearch: true, + width: 70, + align: 'center', + }, + { + title: '标题', + dataIndex: 'title', + valueType: 'text', + required: true, + rules: [{ required: true, message: '请输入轮播图标题' }], + }, + { + title: '轮播图片', + dataIndex: 'image_id', + valueType: 'image', + required: true, + rules: [{ required: true, message: '请上传轮播图片' }], + fieldProps: { + action: '/client/banner/upload', + mode: 'single', + maxCount: 1, + changeType: 'id', + }, + render: (_, record: IHomeBanner) => { + const url = record.image_url; + if (!url) return '-'; + return ( + + ); + }, + align: 'center', + hideInSearch: true, + }, + { + title: '跳转链接', + dataIndex: 'link', + valueType: 'text', + hideInSearch: true, + fieldProps: { + placeholder: '小程序页面路径,如 /pages/goods/detail?id=1', + }, + render: (_, record) => record.link || '-', + }, + { + title: '排序', + dataIndex: 'sort', + valueType: 'digit', + hideInSearch: true, + initialValue: 0, + fieldProps: { min: 0, precision: 0 }, + align: 'center', + width: 80, + }, + { + title: '状态', + dataIndex: 'status', + valueType: 'radioButton', + initialValue: 1, + fieldProps: { + options: [ + { value: 1, label: '正常' }, + { value: 0, label: '停用' }, + ], + }, + render: (_, record) => { + const item = HOME_BANNER_STATUS_MAP[record.status ?? 1]; + return {item?.text}; + }, + align: 'center', + width: 90, + }, + { + title: '创建时间', + dataIndex: 'created_at', + hideInForm: true, + hideInSearch: true, + align: 'center', + }, + ]; + + const tableProps: XinTableProps = { + api: '/client/banner', + columns, + rowKey: 'id', + accessName: 'client.banner', + formProps: { + grid: true, + colProps: { span: 12 }, + layout: 'vertical', + rowProps: { gutter: 24 }, + }, + modalProps: { width: 640 }, + }; + + return ( + <> +
+ 首页轮播图 + + 小程序首页顶部轮播图;停用后不展示,排序越小越靠前;跳转链接为小程序页面路径,留空则点击不跳转。 + +
+ {...tableProps} /> + + ); +}; + +export default HomeBannerPage; diff --git a/web/pages/client/nav.tsx b/web/pages/client/nav.tsx new file mode 100644 index 0000000..5941304 --- /dev/null +++ b/web/pages/client/nav.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { Image, Tag, Typography } from 'antd'; +import XinTable from '@/components/XinTable'; +import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts'; +import type IHomeNav from '@/domain/iHomeNav.ts'; +import { HOME_NAV_STATUS_MAP } from '@/domain/iHomeNav.ts'; + +const { Title, Text } = Typography; + +/** + * 宫格导航配置(小程序首页) + */ +const HomeNavPage: React.FC = () => { + const columns: XinTableColumn[] = [ + { + title: 'ID', + dataIndex: 'id', + hideInForm: true, + hideInSearch: true, + width: 70, + align: 'center', + }, + { + title: '导航名称', + dataIndex: 'name', + valueType: 'text', + required: true, + rules: [{ required: true, message: '请输入导航名称' }], + }, + { + title: '导航图标', + dataIndex: 'image_id', + valueType: 'image', + required: true, + rules: [{ required: true, message: '请上传导航图标' }], + fieldProps: { + action: '/client/nav/upload', + mode: 'single', + maxCount: 1, + changeType: 'id', + }, + render: (_, record: IHomeNav) => { + const url = record.image_url; + if (!url) return '-'; + return ( + + ); + }, + align: 'center', + hideInSearch: true, + }, + { + title: '跳转链接', + dataIndex: 'link', + valueType: 'text', + hideInSearch: true, + fieldProps: { + placeholder: '小程序页面路径,如 /pages/category/index', + }, + render: (_, record) => record.link || '-', + }, + { + title: '排序', + dataIndex: 'sort', + valueType: 'digit', + hideInSearch: true, + initialValue: 0, + fieldProps: { min: 0, precision: 0 }, + align: 'center', + width: 80, + }, + { + title: '状态', + dataIndex: 'status', + valueType: 'radioButton', + initialValue: 1, + fieldProps: { + options: [ + { value: 1, label: '正常' }, + { value: 0, label: '停用' }, + ], + }, + render: (_, record) => { + const item = HOME_NAV_STATUS_MAP[record.status ?? 1]; + return {item?.text}; + }, + align: 'center', + width: 90, + }, + { + title: '创建时间', + dataIndex: 'created_at', + hideInForm: true, + hideInSearch: true, + align: 'center', + }, + ]; + + const tableProps: XinTableProps = { + api: '/client/nav', + columns, + rowKey: 'id', + accessName: 'client.nav', + formProps: { + grid: true, + colProps: { span: 12 }, + layout: 'vertical', + rowProps: { gutter: 24 }, + }, + modalProps: { width: 640 }, + }; + + return ( + <> +
+ 宫格导航 + + 小程序首页宫格入口(如商品分类、促销活动等);停用后不展示,排序越小越靠前。 + +
+ {...tableProps} /> + + ); +}; + +export default HomeNavPage; diff --git a/web/pages/client/promo.tsx b/web/pages/client/promo.tsx new file mode 100644 index 0000000..4b06e7e --- /dev/null +++ b/web/pages/client/promo.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { Image, Tag, Typography } from 'antd'; +import XinTable from '@/components/XinTable'; +import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts'; +import type IHomePromo from '@/domain/iHomePromo.ts'; +import { HOME_PROMO_STATUS_MAP } from '@/domain/iHomePromo.ts'; + +const { Title, Text } = Typography; + +/** + * 促销推荐卡片配置(小程序首页) + */ +const HomePromoPage: React.FC = () => { + const columns: XinTableColumn[] = [ + { + title: 'ID', + dataIndex: 'id', + hideInForm: true, + hideInSearch: true, + width: 70, + align: 'center', + }, + { + title: '卡片标题', + dataIndex: 'title', + valueType: 'text', + }, + { + title: '副标题', + dataIndex: 'sub_title', + valueType: 'text', + hideInSearch: true, + fieldProps: { + placeholder: '促销文案,如「限时特惠 8 折起」', + }, + render: (_, record) => record.sub_title || '-', + }, + { + title: '卡片图片', + dataIndex: 'image_id', + valueType: 'image', + required: true, + rules: [{ required: true, message: '请上传卡片图片' }], + fieldProps: { + action: '/client/promo/upload', + mode: 'single', + maxCount: 1, + changeType: 'id', + }, + render: (_, record: IHomePromo) => { + const url = record.image_url; + if (!url) return '-'; + return ( + + ); + }, + align: 'center', + hideInSearch: true, + }, + { + title: '跳转链接', + dataIndex: 'link', + valueType: 'text', + hideInSearch: true, + fieldProps: { + placeholder: '小程序页面路径,如 /pages/promo/detail?id=1', + }, + render: (_, record) => record.link || '-', + }, + { + title: '排序', + dataIndex: 'sort', + valueType: 'digit', + hideInSearch: true, + initialValue: 0, + fieldProps: { min: 0, precision: 0 }, + align: 'center', + width: 80, + }, + { + title: '状态', + dataIndex: 'status', + valueType: 'radioButton', + initialValue: 1, + fieldProps: { + options: [ + { value: 1, label: '正常' }, + { value: 0, label: '停用' }, + ], + }, + render: (_, record) => { + const item = HOME_PROMO_STATUS_MAP[record.status ?? 1]; + return {item?.text}; + }, + align: 'center', + width: 90, + }, + { + title: '创建时间', + dataIndex: 'created_at', + hideInForm: true, + hideInSearch: true, + align: 'center', + }, + ]; + + const tableProps: XinTableProps = { + api: '/client/promo', + columns, + rowKey: 'id', + accessName: 'client.promo', + formProps: { + grid: true, + colProps: { span: 12 }, + layout: 'vertical', + }, + modalProps: { width: 640 }, + }; + + return ( + <> +
+ 促销推荐卡片 + + 小程序首页促销位卡片;副标题展示促销文案,停用后不展示,排序越小越靠前。 + +
+ {...tableProps} /> + + ); +}; + +export default HomePromoPage;