Compare commits
140 Commits
master
...
94e1e05fdf
| Author | SHA1 | Date | |
|---|---|---|---|
| 94e1e05fdf | |||
| 4d96ca4569 | |||
| daafca1d5d | |||
| 128a566162 | |||
| 54404aa48f | |||
| e0418df018 | |||
| e3181c8ade | |||
| 12917a4766 | |||
| 6fb8be275a | |||
| 190df391c3 | |||
| 5fe881b927 | |||
| 5ff710c6a0 | |||
| 6b1e506f43 | |||
| 4a45bc5242 | |||
| 0628a0f6b4 | |||
| 8b902be603 | |||
| 37ab933849 | |||
| e58a2fd915 | |||
| 4ffe3a8f4b | |||
| e7caee08c1 | |||
| cc58bd791d | |||
| ac194b93eb | |||
| 1cdb6404ad | |||
| ef6a55112f | |||
| 00f3954012 | |||
| 0c9a03d656 | |||
| 80d4db4156 | |||
| a6749bcedb | |||
| 49c801c751 | |||
| 3248315f6e | |||
| 58d3e884ab | |||
| b9eb64894f | |||
| 81c63e0e65 | |||
| 86f7506422 | |||
| fae144549e | |||
| 718eddff63 | |||
| a4a0a922fc | |||
| ca2436a2e8 | |||
| 83ba49d860 | |||
| 7375a3b1ce | |||
| 756b548bf9 | |||
| 76e76c62ef | |||
| 546d90cc28 | |||
| d4fd61376c | |||
| b27421fd6e | |||
| b929b8d35e | |||
| 23af704c68 | |||
| ab61aa9ee0 | |||
| 64d30e1b62 | |||
| a8eb9e11be | |||
| 338dc421db | |||
| 6f1e0a6a2b | |||
| 8b5609255a | |||
| 31d47f0a0b | |||
| 68d5848d3d | |||
| e40120138b | |||
| ef26a207b0 | |||
| 78ac461ef9 | |||
| f9dcaa9ce9 | |||
| d86cdad470 | |||
| 3d94125c5e | |||
| 63d0d64a1f | |||
| 5840bea66b | |||
| 929f173b95 | |||
| 049b2396c3 | |||
| fb06816e37 | |||
| 939d7b3ec2 | |||
| d6eb6d5e05 | |||
| 694b5e1e7a | |||
| 00d3ffaeee | |||
| e22cfe4646 | |||
| 52ef8d4199 | |||
| 93418912dc | |||
| 3535cf3a92 | |||
| b22ff991f0 | |||
| fc3b32215e | |||
| 4951c3202d | |||
| 50ac79d055 | |||
| 8d2188b928 | |||
| a1e1487d42 | |||
| 37c2f030f2 | |||
| 231723e960 | |||
| 94ed969d2d | |||
| 1ce6381248 | |||
| 7fb74e9977 | |||
| f8672dec34 | |||
| 6c83f6c082 | |||
| 5581493772 | |||
| 80653f7ac2 | |||
| 6e0a5aa1fe | |||
| 50ffd2c9da | |||
| 9e780e369c | |||
| 8751be5fb4 | |||
| f15933fc82 | |||
| f20c8b0961 | |||
| 25177d724e | |||
| 661e7574ef | |||
| 56d933ddf8 | |||
| c0954564a6 | |||
| 5bddf6e438 | |||
| 6197dbdb84 | |||
| 96d1bb959e | |||
| a1e5bf1c05 | |||
| 2a3b661478 | |||
| 6d33708601 | |||
| 8c7698a198 | |||
| 24354a38c5 | |||
| 5dc70a1c3c | |||
| 5e90c48b8b | |||
| 526c821a67 | |||
| 8679b26f74 | |||
| fcbaa970d0 | |||
| 5e36f243ef | |||
| afe8f93c32 | |||
| 174f9458e2 | |||
| f96918bf86 | |||
| a3c952d092 | |||
| cb17e48b03 | |||
| 945bf9af8d | |||
| dea40268fe | |||
| a2e34466d5 | |||
| 3d82a0f194 | |||
| f8e689e250 | |||
| e07fd4091e | |||
| 47d2eee486 | |||
| 3b98dfa150 | |||
| 3a68955f1c | |||
| b9c03be394 | |||
| 3a42eaf853 | |||
| f5c6d52b78 | |||
| 7227ec6d84 | |||
| ed5ef3fb19 | |||
| ed02db5a8d | |||
| a4938fbe31 | |||
| aff888794f | |||
| 0d6eb331c8 | |||
| 415e05cc4e | |||
| 0542b93dc7 | |||
| 0770eb1699 | |||
| 039af32fc3 |
@@ -1 +1 @@
|
||||
{"projectName":"trae_template-10550_mhk8"}
|
||||
{"projectName":"trae_template-10584_mhk8"}
|
||||
|
||||
72
.workbuddy/expert-history.json
Normal file
72
.workbuddy/expert-history.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"version": 2,
|
||||
"sessions": {
|
||||
"bb17ec0263344400afb0ecfafefc1ab1": [
|
||||
{
|
||||
"expertId": "SeniorDeveloper",
|
||||
"name": "Will",
|
||||
"profession": "高级开发工程师",
|
||||
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||
"usedAt": 1775132175089,
|
||||
"industryId": "all"
|
||||
}
|
||||
],
|
||||
"b1db410ffede4e03be74a98ac57ea478": [
|
||||
{
|
||||
"expertId": "SeniorDeveloper",
|
||||
"name": "Will",
|
||||
"profession": "高级开发工程师",
|
||||
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||
"usedAt": 1775753422786,
|
||||
"industryId": "all"
|
||||
}
|
||||
],
|
||||
"3ad590301d144e91ac2d9497ad0b1a22": [
|
||||
{
|
||||
"expertId": "SeniorDeveloper",
|
||||
"name": "Will",
|
||||
"profession": "高级开发工程师",
|
||||
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||
"usedAt": 1775754410884,
|
||||
"industryId": "all"
|
||||
}
|
||||
],
|
||||
"a07270b8a79e42f88a866c9e908129ad": [
|
||||
{
|
||||
"expertId": "SeniorDeveloper",
|
||||
"name": "Will",
|
||||
"profession": "高级开发工程师",
|
||||
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||
"usedAt": 1775755906815,
|
||||
"industryId": "all"
|
||||
}
|
||||
],
|
||||
"c8760ea899514a1aae49666ae8a1f5e1": [
|
||||
{
|
||||
"expertId": "SeniorDeveloper",
|
||||
"name": "Will",
|
||||
"profession": "高级开发工程师",
|
||||
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||
"usedAt": 1775755982035,
|
||||
"industryId": "all"
|
||||
}
|
||||
],
|
||||
"92099fa2ec454bf7b29102b6ab4b41a6": [
|
||||
{
|
||||
"expertId": "SeniorDeveloper",
|
||||
"name": "Will",
|
||||
"profession": "高级开发工程师",
|
||||
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||
"usedAt": 1775972794982,
|
||||
"industryId": "all"
|
||||
}
|
||||
]
|
||||
},
|
||||
"lastUpdated": 1775999935033
|
||||
}
|
||||
29
.workbuddy/memory/2026-03-31.md
Normal file
29
.workbuddy/memory/2026-03-31.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 2026-03-31 工作日志
|
||||
|
||||
## 新增 dealer/index.tsx 配送员解冻资金功能
|
||||
|
||||
将 `/rider/index.tsx` 的 `handleFreezeMoneyClick` 功能复制到 `/dealer/index.tsx`:
|
||||
- 导入 `useUser` hook 和 `updateShopDealerUser` API
|
||||
- 添加 `isRider` 判断(只有 `hasRole('rider')` 才可操作)
|
||||
- "待使用"卡片点击事件(仅配送员+有冻结金额时可用)
|
||||
- 确认后冻结金额转入可提现
|
||||
|
||||
## 修改 rider/index.tsx (React Taro 项目)
|
||||
|
||||
修改了 `/Users/gxwebsoft/VUE/template-10584/src/rider/index.tsx`:
|
||||
|
||||
### 修改内容:
|
||||
1. **统一显示格式** - 把"桶数"改成"待使用"
|
||||
2. **添加配送员权限控制** - 只有 `hasRole('rider')` 的用户才能点击操作
|
||||
3. **点击解冻功能**:
|
||||
- 配送员点击"待使用"金额时显示蓝色高亮提示"(点击转入)"
|
||||
- 弹出确认框:`确定要将 ¥xx.xx 待使用金额转入可提现吗?`
|
||||
- 确认后调用 `updateShopDealerUser` API
|
||||
- 将 `freezeMoney` 清零,加到 `money`
|
||||
- 成功后提示"更新成功"并刷新数据
|
||||
|
||||
### 关键代码:
|
||||
- 使用 `useUser` hook 获取 `hasRole` 函数
|
||||
- `isRider` 判断是否为配送员
|
||||
- `handleFreezeMoneyClick` 处理点击解冻逻辑
|
||||
- 使用 `Taro.showModal` / `Taro.showToast` 提示用户
|
||||
107
.workbuddy/memory/2026-04-10.md
Normal file
107
.workbuddy/memory/2026-04-10.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# 2026-04-10 工作日志
|
||||
|
||||
## 完成的功能:四种用户分级标签
|
||||
|
||||
### 需求
|
||||
用户 ID 下方"管理员"标签改为四种分级:
|
||||
- 0: 普通用户
|
||||
- 1: 超级管理员
|
||||
- 2: 合伙人(总店)
|
||||
- 3: 合伙人(分店)
|
||||
|
||||
### 修改的文件
|
||||
|
||||
#### 后端 (JAVA)
|
||||
1. `ShopDealerUser.java` - 添加 `dealerLevel` 字段
|
||||
2. `ShopDealerUserMapper.xml` - 添加 dealerLevel 查询条件
|
||||
|
||||
#### 前端 (VUE)
|
||||
1. `src/api/shop/shopDealerUser/model/index.ts` - 添加 dealerLevel 字段
|
||||
2. `src/utils/userLevel.ts` - 新建用户等级配置工具文件
|
||||
3. `src/pages/user/components/UserCard.tsx` - 修改角色标签显示逻辑
|
||||
|
||||
### 注意事项
|
||||
- 需要在数据库中添加 `dealer_level` 字段
|
||||
- 后端需要重启生效
|
||||
- 前端通过 dealerLevel 字段判断显示对应样式
|
||||
|
||||
## 配送员订单通知功能
|
||||
|
||||
### 需求
|
||||
客户下单后,配送员手机声音提示和红点提示功能
|
||||
|
||||
### 实现方案
|
||||
1. **红点提示**:在配送员首页「配送订单」图标上显示待配送订单数量
|
||||
2. **声音提示**:收到新订单时播放微信官方提示音
|
||||
3. **设置功能**:支持开启/关闭声音提醒
|
||||
|
||||
### 新增/修改的文件
|
||||
1. `src/api/glt/gltTicketOrder/index.ts` - 添加 `getRiderPendingCount` 接口
|
||||
2. `src/hooks/useRiderNotification.ts` - 新建配送员通知 Hook
|
||||
3. `src/rider/index.tsx` - 添加 Badge 红点和设置入口
|
||||
|
||||
### 技术实现
|
||||
- 使用 30 秒轮询获取待配送订单数量
|
||||
- 使用 NutUI 的 Badge 组件显示红点
|
||||
- 使用 Taro.createInnerAudioContext 播放提示音
|
||||
- 声音设置保存在本地存储 `rider_sound_enabled`
|
||||
|
||||
### 待后端配合
|
||||
- 需要后端提供 `/glt/glt-ticket-order/rider/count` 接口(可选,使用现有 page 接口也行)
|
||||
- 需要配置微信订阅消息模板(可选)
|
||||
|
||||
## 下单页配送方式 + 配送费功能
|
||||
|
||||
### 需求
|
||||
1. 下单页必选配送方式:电梯 / 步梯 / 一楼商铺·其他
|
||||
2. 步梯需二级选择是否送上楼,送上楼需选楼层
|
||||
3. 配送费计算:每桶每层1元,第1层不收费(即 `(楼层-1) × 数量`)
|
||||
4. 自提模式隐藏配送方式选择器
|
||||
|
||||
### 修改的文件
|
||||
1. `src/api/shop/shopOrder/model/index.ts` - OrderCreateRequest 新增 deliveryMethod、deliveryFloor 字段
|
||||
2. `src/utils/payment.ts` - buildSingleGoodsOrder 透传配送方式字段
|
||||
3. `src/shop/orderConfirm/index.tsx` - 配送方式选择UI、配送费计算、楼层选择弹窗、支付校验
|
||||
4. `src/shop/orderConfirm/index.scss` - 配送方式选择器、楼层网格样式
|
||||
|
||||
### 后端需配合
|
||||
- 订单表新增 `delivery_method` 和 `delivery_floor` 字段
|
||||
- 订单创建接口接收并存储这两个字段
|
||||
- 骑手端/后台展示配送方式和楼层信息
|
||||
|
||||
## 配送方式功能迁移:orderConfirm → user/ticket/use
|
||||
|
||||
### 原因
|
||||
配送方式选择功能从购买下单页(orderConfirm)迁移到水票核销/立即送水页面(user/ticket/use)。
|
||||
|
||||
### 修改的文件
|
||||
1. `src/shop/orderConfirm/index.tsx` - 回滚:移除配送方式状态变量、UI、校验、楼层选择弹窗、配送费计算
|
||||
2. `src/shop/orderConfirm/index.scss` - 回滚:移除配送方式/楼层选择相关样式
|
||||
3. `src/api/glt/gltTicketOrder/model/index.ts` - GltTicketOrder 新增 deliveryMethod、deliveryFloor、deliveryFee 字段
|
||||
4. `src/user/ticket/use.tsx` - 新增:配送方式选择UI、配送费计算、楼层选择弹窗、提交校验、编辑模式回显
|
||||
5. `src/user/ticket/use.scss` - 新增:配送方式/楼层选择样式
|
||||
|
||||
### 关键差异
|
||||
- orderConfirm 页面是付费购买,配送费加到实付金额中
|
||||
- use 页面是水票核销(不付费),配送费以"到付"形式展示在底部栏
|
||||
- use 页面同时支持新建和编辑模式,编辑时回显配送信息
|
||||
|
||||
## 微信订阅消息配置(补充)
|
||||
|
||||
### 需要做的配置
|
||||
|
||||
#### 后端配置
|
||||
1. `GltSubscribeMessageServiceImpl.java` - 订阅消息发送服务
|
||||
- 需要配置 `SUBSCRIBE_TEMPLATE_ID` 为实际模板ID
|
||||
|
||||
2. `GltTicketOrderController.java` - 订单创建时通知配送员
|
||||
- 注入 `GltSubscribeMessageService` 和 `UserMapper`
|
||||
- 添加 `notifyRidersOfNewOrder` 方法
|
||||
|
||||
#### 前端配置
|
||||
1. `src/rider/index.tsx`
|
||||
- `handleRequestSubscribeMessage` 函数需要配置实际的模板ID
|
||||
|
||||
### 微信后台需要申请的模板
|
||||
模板名称:订单配送通知
|
||||
关键词:订单状态、订单编号、配送地址、商品数量、通知时间
|
||||
25
.workbuddy/memory/2026-04-12.md
Normal file
25
.workbuddy/memory/2026-04-12.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 2026-04-12 工作日志
|
||||
|
||||
## 配送方式功能完善:全链路入库+展示
|
||||
|
||||
### 问题
|
||||
配送方式(deliveryMethod)、楼层(deliveryFloor)、配送费(deliveryFee)在小程序端用户下单页面已可选择并提交,但后端数据库表没有对应字段,数据实际没有保存。骑手端和后台管理端也没有展示这些信息。
|
||||
|
||||
### 修改的文件
|
||||
|
||||
#### 后端(/Users/gxwebsoft/JAVA/java-10584)
|
||||
1. `src/main/java/com/gxwebsoft/glt/entity/GltTicketOrder.java` - 新增 deliveryMethod(String)、deliveryFloor(Integer)、deliveryFee(BigDecimal) 三个数据库字段
|
||||
2. `sql/glt_ticket_order_delivery_fields.sql` - 新增 ALTER TABLE SQL,给 glt_ticket_order 表添加三个字段
|
||||
|
||||
#### 小程序端(/Users/gxwebsoft/VUE/template-10584)
|
||||
3. `src/rider/orders/index.tsx` - 骑手送水订单页面:新增配送方式中文映射函数,订单卡片中展示配送方式、楼层、配送费
|
||||
|
||||
#### 后台管理端(/Users/gxwebsoft/VUE/mp-10584)
|
||||
4. `src/api/glt/gltTicketOrder/model/index.ts` - GltTicketOrder 接口新增 deliveryMethod、deliveryFloor、deliveryFee 字段
|
||||
5. `src/views/glt/gltTicketOrder/index.vue` - 列表页"配送信息"列中展示配送方式、楼层、配送费
|
||||
6. `src/views/glt/gltTicketOrder/components/gltTicketOrderEdit.vue` - 编辑弹窗中新增配送方式只读展示
|
||||
|
||||
### 部署注意事项
|
||||
- **必须先执行 SQL**:`java-10584/sql/glt_ticket_order_delivery_fields.sql`
|
||||
- MyBatis-Plus 使用驼峰自动映射,`delivery_method` → `deliveryMethod`,无需修改 Mapper XML
|
||||
- 后端 save/updateById 自动包含新字段,无需修改 Controller/Service
|
||||
16
.workbuddy/memory/2026-04-25.md
Normal file
16
.workbuddy/memory/2026-04-25.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 2026-04-25 工作日志
|
||||
|
||||
## 微信支付"下单账号与支付账号不一致"问题修复
|
||||
|
||||
**问题**:用户支付时微信提示「下单账号与支付账号不一致,请核实后再支付」
|
||||
|
||||
**根因**:下单时后端使用的 openid 与当前实际支付的微信用户 openid 不匹配。前端在创建订单前没有刷新 openid,依赖登录/注册时的旧绑定。
|
||||
|
||||
**修改文件**:
|
||||
1. `src/utils/payment.ts` — 新增 `ensureOpenIdBeforePay()` 方法,在 `pay()` 方法中、微信支付创建订单前强制调用 `Taro.login()` + `getWxOpenId()` 刷新绑定当前用户的 openid
|
||||
2. `src/hooks/useUser.ts` — 在 `autoLoginByOpenId()` 登录成功后补充 openid 绑定逻辑(之前只调了 loginByOpenId 没有调 getWxOpenId)
|
||||
|
||||
**方案特点**:
|
||||
- 非阻塞设计:openid 刷新失败不阻止支付(网络波动时兼容)
|
||||
- 仅微信支付触发:余额支付/支付宝不受影响
|
||||
- 兼容已有 openid 的场景:即使已有 openid 也会刷新(防止切号)
|
||||
0
.workbuddy/memory/MEMORY.md
Normal file
0
.workbuddy/memory/MEMORY.md
Normal file
@@ -1,12 +1,15 @@
|
||||
import { API_BASE_URL } from './env'
|
||||
|
||||
// 租户ID - 请根据实际情况修改
|
||||
export const TenantId = '10550';
|
||||
export const TenantId = '10584';
|
||||
// 租户名称
|
||||
export const TenantName = '桂乐淘';
|
||||
// 接口地址 - 请根据实际情况修改
|
||||
export const BaseUrl = API_BASE_URL;
|
||||
// 当前版本
|
||||
export const Version = 'v3.0.8';
|
||||
// 版权信息
|
||||
export const Copyright = 'WebSoft Inc.';
|
||||
export const Copyright = '桂乐淘·购享无界 乐惠万家';
|
||||
// export const Copyright = '测试环境 v3.2.6';
|
||||
|
||||
// java -jar CertificateDownloader.jar -k 0kF5OlPr482EZwtn9zGufUcqa7ovgxRL -m 1723321338 -f ./apiclient_key.pem -s 2B933F7C35014A1C363642623E4A62364B34C4EB -o ./
|
||||
|
||||
43
config/env.js
Normal file
43
config/env.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// 环境变量配置
|
||||
|
||||
// ============ 环境切换开关(修改这里即可切换环境)============
|
||||
// 可选值: 'development' | 'test' | 'production'
|
||||
const CURRENT_ENV = 'production'
|
||||
// ===========================================================
|
||||
|
||||
export const ENV_CONFIG = {
|
||||
// 开发环境
|
||||
development: {
|
||||
API_BASE_URL: 'https://glt-api.websoft.top/api',
|
||||
SERVER_API_URL: 'https://glt-server.websoft.top/api',
|
||||
APP_NAME: '开发环境',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
// 测试环境
|
||||
test: {
|
||||
API_BASE_URL: 'https://glt-api.websoft.top/api',
|
||||
SERVER_API_URL: 'https://glt-server.websoft.top/api',
|
||||
APP_NAME: '测试环境',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
// 生产环境
|
||||
production: {
|
||||
API_BASE_URL: 'https://glt-api.websoft.top/api',
|
||||
SERVER_API_URL: 'https://glt-server.websoft.top/api',
|
||||
APP_NAME: '桂乐淘',
|
||||
DEBUG: 'false',
|
||||
},
|
||||
}
|
||||
|
||||
// 获取当前环境配置
|
||||
export function getEnvConfig() {
|
||||
return ENV_CONFIG[CURRENT_ENV]
|
||||
}
|
||||
|
||||
// 导出环境变量
|
||||
export const {
|
||||
API_BASE_URL,
|
||||
SERVER_API_URL,
|
||||
APP_NAME,
|
||||
DEBUG
|
||||
} = getEnvConfig()
|
||||
@@ -1,43 +0,0 @@
|
||||
// 环境变量配置
|
||||
export const ENV_CONFIG = {
|
||||
// 开发环境
|
||||
development: {
|
||||
API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
// API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
APP_NAME: '开发环境',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
// 生产环境
|
||||
production: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
APP_NAME: '时里院子市集',
|
||||
DEBUG: 'false',
|
||||
},
|
||||
// 测试环境
|
||||
test: {
|
||||
API_BASE_URL: 'https://cms-api.s209.websoft.top/api',
|
||||
APP_NAME: '测试环境',
|
||||
DEBUG: 'true',
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境配置
|
||||
export function getEnvConfig() {
|
||||
const env = process.env.NODE_ENV || 'development'
|
||||
if (env === 'production') {
|
||||
return ENV_CONFIG.production
|
||||
} else { // @ts-ignore
|
||||
if (env === 'test') {
|
||||
return ENV_CONFIG.test
|
||||
} else {
|
||||
return ENV_CONFIG.development
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出环境变量
|
||||
export const {
|
||||
API_BASE_URL,
|
||||
APP_NAME,
|
||||
DEBUG
|
||||
} = getEnvConfig()
|
||||
@@ -2,7 +2,7 @@ import { defineConfig, type UserConfigExport } from '@tarojs/cli'
|
||||
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
|
||||
import devConfig from './dev'
|
||||
import prodConfig from './prod'
|
||||
import { getEnvConfig } from './env'
|
||||
import { getEnvConfig } from './env.js'
|
||||
|
||||
// import vitePluginImp from 'vite-plugin-imp'
|
||||
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
#### 新增功能
|
||||
- 用户头像和基本信息展示
|
||||
- 佣金统计(可提现、冻结中、累计收益)
|
||||
- 佣金统计(可提现、待使用、累计收益)
|
||||
- 团队统计(一级、二级、三级成员)
|
||||
- 功能导航网格(分销订单、提现申请、我的团队、推广二维码)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ dealer: {
|
||||
// 金额相关
|
||||
money: {
|
||||
available: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', // 可提现 - 绿色
|
||||
frozen: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', // 冻结中 - 蓝色
|
||||
frozen: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', // 待使用 - 蓝色
|
||||
total: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)' // 累计 - 橙色
|
||||
}
|
||||
```
|
||||
|
||||
@@ -90,7 +90,7 @@ const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?:
|
||||
success: function () {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code,
|
||||
|
||||
41
docs/水票配送订单-后端提示词.md
Normal file
41
docs/水票配送订单-后端提示词.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 水票配送订单:后端提示词(可直接发给后端)
|
||||
|
||||
## 1) 订单查询(配送员端)
|
||||
请在 `GET /glt/glt-ticket-order/page` 支持以下筛选,并保证权限隔离:
|
||||
- `riderId`:只返回该配送员的订单(必要)
|
||||
- `deliveryStatus`:10待配送、20配送中、30待客户确认、40已完成(必要)
|
||||
- 排序:建议 `sendTime asc` + `createTime desc`(或给前端一个可控排序字段)
|
||||
|
||||
## 2) 配送流程字段(建议后端落库并回传)
|
||||
订单表建议确保有以下字段(当前前端已按这些字段做流程判断/展示):
|
||||
- `riderId/riderName/riderPhone`:配送员信息
|
||||
- `deliveryStatus`:10/20/30/40
|
||||
- `sendStartTime`:配送员点击“开始配送”的时间
|
||||
- `sendEndTime`:配送员点击“确认送达”的时间
|
||||
- `sendEndImg`:送达拍照留档图片 URL(可选/必填由后端策略决定)
|
||||
- `receiveConfirmTime`:客户确认收货时间
|
||||
- `receiveConfirmType`:10客户手动确认、20配送照片自动确认、30超时自动确认
|
||||
|
||||
## 3) 状态流转与校验(强烈建议在后端做)
|
||||
请在更新订单时做状态机校验,避免前端绕过流程:
|
||||
- `10 -> 20`:仅允许订单属于当前配送员,且未开始/未送达
|
||||
- `20 -> 30`:配送员确认送达(可带 `sendEndImg`)
|
||||
- `20/30 -> 40`:完成;来源可能是
|
||||
- 客户手动确认(写 `receiveConfirmTime` + `receiveConfirmType=10`)
|
||||
- 配送照片直接完成(写 `receiveConfirmTime` + `receiveConfirmType=20`,并要求 `sendEndImg`)
|
||||
- 超时自动确认(写 `receiveConfirmTime` + `receiveConfirmType=30`,建议由定时任务执行)
|
||||
|
||||
## 4) 建议新增/明确的接口能力
|
||||
为了避免并发抢单/越权更新,建议新增更语义化的接口(或在 update 内做等价校验):
|
||||
- 接单(抢单/派单):`POST /glt/glt-ticket-order/{id}/accept`
|
||||
- 后端原子校验:仅当 `riderId is null` 才能写入当前 rider 信息
|
||||
- 开始配送:`POST /glt/glt-ticket-order/{id}/start`(写 `sendStartTime` + `deliveryStatus=20`)
|
||||
- 确认送达:`POST /glt/glt-ticket-order/{id}/delivered`(写 `sendEndTime` + `deliveryStatus=30` + 可选 `sendEndImg`)
|
||||
- 客户确认收货:`POST /glt/glt-ticket-order/{id}/confirm-receive`
|
||||
- 校验:只能本人 `userId` 操作,且必须已送达
|
||||
|
||||
## 5) 为了“导航到收货地址/取货点”的字段补充(建议)
|
||||
当前仅有 `address` 字符串,无法在小程序内 `openLocation` 精准导航;建议补充:
|
||||
- 收货地址:`receiverName`、`receiverPhone`、`province/city/district/detail`、`latitude/longitude`
|
||||
- 取货点(门店/仓库):`storeLatitude/storeLongitude` 或 `warehouseLatitude/warehouseLongitude`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "template-10550",
|
||||
"name": "template-10584",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "WebSoft Inc.",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"miniprogramRoot": "dist/",
|
||||
"projectname": "template-10550",
|
||||
"description": "时里院子市集",
|
||||
"appid": "wx5170f9f17a813877",
|
||||
"projectname": "template-10584",
|
||||
"description": "桂乐淘",
|
||||
"appid": "wxad831ba00ad6a026",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"miniprogramRoot": "./",
|
||||
"projectname": "mp-react",
|
||||
"description": "时里院子市集",
|
||||
"description": "桂乐淘",
|
||||
"appid": "touristappid",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
|
||||
@@ -144,7 +144,7 @@ function UserCard() {
|
||||
success: function () {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code,
|
||||
@@ -237,7 +237,7 @@ function UserCard() {
|
||||
</div>
|
||||
<div className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/gift/index', true)}>
|
||||
<span className={'text-sm text-gray-500'}>礼品卡</span>
|
||||
<span className={'text-sm text-gray-500'}>水票</span>
|
||||
<span className={'text-xl'}>{giftCount}</span>
|
||||
</div>
|
||||
{/*<div className={'item flex justify-center flex-col items-center'}>*/}
|
||||
|
||||
116
src/api/glt/gltTicketOrder/index.ts
Normal file
116
src/api/glt/gltTicketOrder/index.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api/index';
|
||||
import type { GltTicketOrder, GltTicketOrderParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询送水订单
|
||||
*/
|
||||
export async function pageGltTicketOrder(params: GltTicketOrderParam) {
|
||||
const res = await request.get<ApiResult<PageResult<GltTicketOrder>>>(
|
||||
'/glt/glt-ticket-order/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配送员待处理订单数量
|
||||
* @param riderId 配送员ID
|
||||
*/
|
||||
export async function getRiderPendingCount(riderId: number) {
|
||||
const res = await request.get<ApiResult<{ pendingCount: number; totalCount: number }>>(
|
||||
'/glt/glt-ticket-order/rider/count',
|
||||
{ riderId }
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return { pendingCount: 0, totalCount: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询送水订单列表
|
||||
*/
|
||||
export async function listGltTicketOrder(params?: GltTicketOrderParam) {
|
||||
const res = await request.get<ApiResult<GltTicketOrder[]>>(
|
||||
'/glt/glt-ticket-order',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加送水订单
|
||||
*/
|
||||
export async function addGltTicketOrder(data: GltTicketOrder) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/glt/glt-ticket-order',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改送水订单
|
||||
*/
|
||||
export async function updateGltTicketOrder(data: GltTicketOrder) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/glt/glt-ticket-order',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除送水订单
|
||||
*/
|
||||
export async function removeGltTicketOrder(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/glt/glt-ticket-order/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除送水订单
|
||||
*/
|
||||
export async function removeBatchGltTicketOrder(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/glt/glt-ticket-order/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询送水订单
|
||||
*/
|
||||
export async function getGltTicketOrder(id: number) {
|
||||
const res = await request.get<ApiResult<GltTicketOrder>>(
|
||||
'/glt/glt-ticket-order/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
100
src/api/glt/gltTicketOrder/model/index.ts
Normal file
100
src/api/glt/gltTicketOrder/model/index.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 送水订单
|
||||
*/
|
||||
export interface GltTicketOrder {
|
||||
//
|
||||
id?: number;
|
||||
// 用户水票ID
|
||||
userTicketId?: number;
|
||||
// 订单编号
|
||||
orderNo?: string;
|
||||
// 门店ID
|
||||
storeId?: number;
|
||||
// 门店名称
|
||||
storeName?: string;
|
||||
// 门店地址
|
||||
storeAddress?: string;
|
||||
// 门店电话
|
||||
storePhone?: string;
|
||||
// 配送员
|
||||
riderId?: number;
|
||||
// 配送员名称
|
||||
riderName?: string;
|
||||
// 配送员电话
|
||||
riderPhone?: string;
|
||||
// 仓库ID
|
||||
warehouseId?: number;
|
||||
// 仓库名称
|
||||
warehouseName?: string;
|
||||
// 仓库地址
|
||||
warehouseAddress?: string;
|
||||
// 关联收货地址
|
||||
addressId?: number;
|
||||
// 收货地址
|
||||
address?: string;
|
||||
// 配送时间
|
||||
sendTime?: string;
|
||||
// 配送开始时间(配送员点击“开始配送”)
|
||||
sendStartTime?: string;
|
||||
// 配送结束时间(配送员确认送达)
|
||||
sendEndTime?: string;
|
||||
// 配送员送达拍照(选填/必填由后端策略决定)
|
||||
sendEndImg?: string;
|
||||
// 发货/配送状态(建议:10待配送 20配送中 30待客户确认 40已完成)
|
||||
deliveryStatus?: number;
|
||||
// 客户确认收货时间(客户点击确认收货)
|
||||
receiveConfirmTime?: string;
|
||||
// 客户确认方式(建议:10客户手动确认 20配送照片自动确认 30后台超时自动确认)
|
||||
receiveConfirmType?: number;
|
||||
// 买家留言
|
||||
buyerRemarks?: string;
|
||||
// 用于统计
|
||||
price?: string;
|
||||
// 购买数量
|
||||
totalNum?: number;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 昵称
|
||||
nickname?: string;
|
||||
// 头像
|
||||
avatar?: string;
|
||||
// 手机号码
|
||||
phone?: string;
|
||||
// 排序(数字越小越靠前)
|
||||
sortNumber?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 状态, 0正常, 1冻结
|
||||
status?: number;
|
||||
// 是否删除, 0否, 1是
|
||||
deleted?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
// 配送方式:elevator(电梯) / stairs(步梯) / groundFloor(一楼商铺/其他)
|
||||
deliveryMethod?: string;
|
||||
// 楼层(步梯+送上楼时有值,从2开始)
|
||||
deliveryFloor?: number;
|
||||
// 配送费(步梯+送上楼时计算:数量 × (楼层-1))
|
||||
deliveryFee?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 送水订单搜索条件
|
||||
*/
|
||||
export interface GltTicketOrderParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
userId?: number;
|
||||
// 配送员用户ID(用于配送员端查询)
|
||||
riderId?: number;
|
||||
// 发货/配送状态(建议与 GltTicketOrder.deliveryStatus 对齐)
|
||||
deliveryStatus?: number;
|
||||
// 兼容 ShopOrderParam 的筛选字段(如后端已实现可直接复用)
|
||||
statusFilter?: number;
|
||||
}
|
||||
118
src/api/glt/gltTicketTemplate/index.ts
Normal file
118
src/api/glt/gltTicketTemplate/index.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { GltTicketTemplate, GltTicketTemplateParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询水票
|
||||
*/
|
||||
export async function pageGltTicketTemplate(params: GltTicketTemplateParam) {
|
||||
const res = await request.get<ApiResult<PageResult<GltTicketTemplate>>>(
|
||||
'/glt/glt-ticket-template/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询水票列表
|
||||
*/
|
||||
export async function listGltTicketTemplate(params?: GltTicketTemplateParam) {
|
||||
const res = await request.get<ApiResult<GltTicketTemplate[]>>(
|
||||
'/glt/glt-ticket-template',
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加水票
|
||||
*/
|
||||
export async function addGltTicketTemplate(data: GltTicketTemplate) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/glt/glt-ticket-template',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改水票
|
||||
*/
|
||||
export async function updateGltTicketTemplate(data: GltTicketTemplate) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/glt/glt-ticket-template',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除水票
|
||||
*/
|
||||
export async function removeGltTicketTemplate(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/glt/glt-ticket-template/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除水票
|
||||
*/
|
||||
export async function removeBatchGltTicketTemplate(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/glt/glt-ticket-template/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询水票
|
||||
*/
|
||||
export async function getGltTicketTemplate(id: number) {
|
||||
const res = await request.get<ApiResult<GltTicketTemplate>>(
|
||||
'/glt/glt-ticket-template/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据商品ID查询水票模板
|
||||
*/
|
||||
export async function getGltTicketTemplateByGoodsId(id: number) {
|
||||
const res = await request.get<ApiResult<GltTicketTemplate>>(
|
||||
'/glt/glt-ticket-template/getByGoodsId/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
57
src/api/glt/gltTicketTemplate/model/index.ts
Normal file
57
src/api/glt/gltTicketTemplate/model/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 水票
|
||||
*/
|
||||
export interface GltTicketTemplate {
|
||||
//
|
||||
id?: number;
|
||||
// 关联商品ID
|
||||
goodsId?: number;
|
||||
// 名称
|
||||
name?: string;
|
||||
// 启用
|
||||
enabled?: boolean;
|
||||
// 单位名称
|
||||
unitName?: string;
|
||||
// 最小购买数量
|
||||
minBuyQty?: number;
|
||||
// 购买步长(如:5 的倍数)
|
||||
step?: number;
|
||||
// 起始发送数量
|
||||
startSendQty?: number;
|
||||
// 买赠:买1送4 => gift_multiplier=4
|
||||
giftMultiplier?: number;
|
||||
// 是否把购买量也计入套票总量(默认仅计入赠送量)
|
||||
includeBuyQty?: boolean;
|
||||
// 每期释放数量(默认每月释放10)
|
||||
monthlyReleaseQty?: number;
|
||||
// 总共释放多少期(若配置>0,则按期数平均分摊)
|
||||
releasePeriods?: number;
|
||||
// 首期释放时机:0=支付成功当刻;1=下个月同日
|
||||
firstReleaseMode?: number;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 排序(数字越小越靠前)
|
||||
sortNumber?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 状态, 0正常, 1冻结
|
||||
status?: number;
|
||||
// 是否删除, 0否, 1是
|
||||
deleted?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 水票搜索条件
|
||||
*/
|
||||
export interface GltTicketTemplateParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
170
src/api/glt/gltUserTicket/index.ts
Normal file
170
src/api/glt/gltUserTicket/index.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { GltUserTicket, GltUserTicketParam } from './model';
|
||||
|
||||
function normalizeTotal(input: unknown): number {
|
||||
if (typeof input === 'number' && Number.isFinite(input)) return input;
|
||||
if (typeof input === 'string') {
|
||||
const n = Number(input);
|
||||
if (Number.isFinite(n)) return n;
|
||||
}
|
||||
if (input && typeof input === 'object') {
|
||||
const obj: any = input;
|
||||
// Common shapes from different backends.
|
||||
for (const key of ['total', 'count', 'value', 'num', 'ticketTotal', 'totalQty']) {
|
||||
const v = obj?.[key];
|
||||
const n = normalizeTotal(v);
|
||||
if (n) return n;
|
||||
}
|
||||
// Sometimes nested: { data: { total: ... } } / { data: 12 }
|
||||
if ('data' in obj) {
|
||||
const n = normalizeTotal(obj.data);
|
||||
if (n) return n;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询我的水票
|
||||
*/
|
||||
export async function pageGltUserTicket(params: GltUserTicketParam) {
|
||||
const res = await request.get<ApiResult<PageResult<GltUserTicket>>>(
|
||||
'/glt/glt-user-ticket/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询我的水票列表
|
||||
*/
|
||||
export async function listGltUserTicket(params?: GltUserTicketParam) {
|
||||
const res = await request.get<ApiResult<GltUserTicket[]>>(
|
||||
'/glt/glt-user-ticket',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加我的水票
|
||||
*/
|
||||
export async function addGltUserTicket(data: GltUserTicket) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改我的水票
|
||||
*/
|
||||
export async function updateGltUserTicket(data: GltUserTicket) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除我的水票
|
||||
*/
|
||||
export async function removeGltUserTicket(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除我的水票
|
||||
*/
|
||||
export async function removeBatchGltUserTicket(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询我的水票
|
||||
*/
|
||||
export async function getGltUserTicket(id: number) {
|
||||
const res = await request.get<ApiResult<GltUserTicket>>(
|
||||
'/glt/glt-user-ticket/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的水票总数
|
||||
*/
|
||||
export async function getMyGltUserTicketTotal(userId?: number) {
|
||||
const params = userId ? { userId } : undefined
|
||||
|
||||
const extract = (res: any) => {
|
||||
// Some backends may return a raw number instead of ApiResult.
|
||||
if (typeof res === 'number' || typeof res === 'string') return normalizeTotal(res)
|
||||
if (res && typeof res === 'object' && 'code' in res) {
|
||||
const apiRes = res as ApiResult<unknown>
|
||||
if (apiRes.code === 0) return normalizeTotal(apiRes.data)
|
||||
throw new Error(apiRes.message)
|
||||
}
|
||||
return normalizeTotal(res)
|
||||
}
|
||||
|
||||
// Try both the configured BaseUrl host and the auth-server host.
|
||||
// If the first one returns 0, keep trying; some tenants deploy GLT on a different host.
|
||||
const urls = [
|
||||
'/glt/glt-user-ticket/my-total'
|
||||
]
|
||||
|
||||
let lastError: unknown
|
||||
let firstTotal: number | undefined
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const res = await request.get<any>(url, params)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[getMyGltUserTicketTotal] response:', { url, res })
|
||||
}
|
||||
const total = extract(res)
|
||||
if (firstTotal === undefined) firstTotal = total
|
||||
if (total) return total
|
||||
} catch (e) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn('[getMyGltUserTicketTotal] failed:', { url, error: e })
|
||||
}
|
||||
lastError = e
|
||||
}
|
||||
}
|
||||
|
||||
if (firstTotal !== undefined) return firstTotal
|
||||
return Promise.reject(lastError instanceof Error ? lastError : new Error('获取水票总数失败'))
|
||||
}
|
||||
66
src/api/glt/gltUserTicket/model/index.ts
Normal file
66
src/api/glt/gltUserTicket/model/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 我的水票
|
||||
*/
|
||||
export interface GltUserTicket {
|
||||
//
|
||||
id?: number;
|
||||
// 模板ID
|
||||
templateId?: number;
|
||||
// 模板名称
|
||||
templateName?: string;
|
||||
// 商品ID
|
||||
goodsId?: number;
|
||||
// 订单ID
|
||||
orderId?: number;
|
||||
// 订单编号
|
||||
orderNo?: string;
|
||||
// 订单商品ID
|
||||
orderGoodsId?: number;
|
||||
// 总数量
|
||||
totalQty?: number;
|
||||
// 可用数量
|
||||
availableQty?: number;
|
||||
// 冻结数量
|
||||
frozenQty?: number;
|
||||
// 已使用数量
|
||||
usedQty?: number;
|
||||
// 已释放数量
|
||||
releasedQty?: number;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 用户昵称
|
||||
nickname?: string;
|
||||
// 用户头像
|
||||
avatar?: string;
|
||||
// 用户手机号
|
||||
phone?: string;
|
||||
// 排序(数字越小越靠前)
|
||||
sortNumber?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 状态, 0正常, 1冻结
|
||||
status?: number;
|
||||
// 是否删除, 0否, 1是
|
||||
deleted?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的水票搜索条件
|
||||
*/
|
||||
export interface GltUserTicketParam extends PageParam {
|
||||
id?: number;
|
||||
templateId?: number;
|
||||
userId?: number;
|
||||
phone?: string;
|
||||
keywords?: string;
|
||||
// 状态过滤:0正常,1冻结
|
||||
status?: number;
|
||||
}
|
||||
101
src/api/glt/gltUserTicketLog/index.ts
Normal file
101
src/api/glt/gltUserTicketLog/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { GltUserTicketLog, GltUserTicketLogParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询消费日志
|
||||
*/
|
||||
export async function pageGltUserTicketLog(params: GltUserTicketLogParam) {
|
||||
const res = await request.get<ApiResult<PageResult<GltUserTicketLog>>>(
|
||||
'/glt/glt-user-ticket-log/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询消费日志列表
|
||||
*/
|
||||
export async function listGltUserTicketLog(params?: GltUserTicketLogParam) {
|
||||
const res = await request.get<ApiResult<GltUserTicketLog[]>>(
|
||||
'/glt/glt-user-ticket-log',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消费日志
|
||||
*/
|
||||
export async function addGltUserTicketLog(data: GltUserTicketLog) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket-log',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改消费日志
|
||||
*/
|
||||
export async function updateGltUserTicketLog(data: GltUserTicketLog) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket-log',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除消费日志
|
||||
*/
|
||||
export async function removeGltUserTicketLog(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket-log/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除消费日志
|
||||
*/
|
||||
export async function removeBatchGltUserTicketLog(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket-log/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询消费日志
|
||||
*/
|
||||
export async function getGltUserTicketLog(id: number) {
|
||||
const res = await request.get<ApiResult<GltUserTicketLog>>(
|
||||
'/glt/glt-user-ticket-log/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
54
src/api/glt/gltUserTicketLog/model/index.ts
Normal file
54
src/api/glt/gltUserTicketLog/model/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 消费日志
|
||||
*/
|
||||
export interface GltUserTicketLog {
|
||||
//
|
||||
id?: number;
|
||||
// 用户水票ID
|
||||
userTicketId?: number;
|
||||
// 变更类型
|
||||
changeType?: number;
|
||||
// 可更改
|
||||
changeAvailable?: number;
|
||||
// 更改冻结状态
|
||||
changeFrozen?: number;
|
||||
// 已使用更改
|
||||
changeUsed?: number;
|
||||
// 可用后
|
||||
availableAfter?: number;
|
||||
// 冻结后
|
||||
frozenAfter?: number;
|
||||
// 使用后
|
||||
usedAfter?: number;
|
||||
// 订单ID
|
||||
orderId?: number;
|
||||
// 订单编号
|
||||
orderNo?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 排序(数字越小越靠前)
|
||||
sortNumber?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 状态, 0正常, 1冻结
|
||||
status?: number;
|
||||
// 是否删除, 0否, 1是
|
||||
deleted?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消费日志搜索条件
|
||||
*/
|
||||
export interface GltUserTicketLogParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
userId?: number;
|
||||
}
|
||||
101
src/api/glt/gltUserTicketRelease/index.ts
Normal file
101
src/api/glt/gltUserTicketRelease/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { GltUserTicketRelease, GltUserTicketReleaseParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询水票释放
|
||||
*/
|
||||
export async function pageGltUserTicketRelease(params: GltUserTicketReleaseParam) {
|
||||
const res = await request.get<ApiResult<PageResult<GltUserTicketRelease>>>(
|
||||
'/glt/glt-user-ticket-release/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询水票释放列表
|
||||
*/
|
||||
export async function listGltUserTicketRelease(params?: GltUserTicketReleaseParam) {
|
||||
const res = await request.get<ApiResult<GltUserTicketRelease[]>>(
|
||||
'/glt/glt-user-ticket-release',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加水票释放
|
||||
*/
|
||||
export async function addGltUserTicketRelease(data: GltUserTicketRelease) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket-release',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改水票释放
|
||||
*/
|
||||
export async function updateGltUserTicketRelease(data: GltUserTicketRelease) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket-release',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除水票释放
|
||||
*/
|
||||
export async function removeGltUserTicketRelease(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket-release/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除水票释放
|
||||
*/
|
||||
export async function removeBatchGltUserTicketRelease(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/glt/glt-user-ticket-release/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询水票释放
|
||||
*/
|
||||
export async function getGltUserTicketRelease(id: number) {
|
||||
const res = await request.get<ApiResult<GltUserTicketRelease>>(
|
||||
'/glt/glt-user-ticket-release/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
38
src/api/glt/gltUserTicketRelease/model/index.ts
Normal file
38
src/api/glt/gltUserTicketRelease/model/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 水票释放
|
||||
*/
|
||||
export interface GltUserTicketRelease {
|
||||
//
|
||||
id?: string;
|
||||
// 水票ID
|
||||
userTicketId?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 周期编号
|
||||
periodNo?: number;
|
||||
// 释放数量
|
||||
releaseQty?: number;
|
||||
// 释放时间
|
||||
releaseTime?: string;
|
||||
// 状态
|
||||
status?: number;
|
||||
// 是否删除, 0否, 1是
|
||||
deleted?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 水票释放搜索条件
|
||||
*/
|
||||
export interface GltUserTicketReleaseParam extends PageParam {
|
||||
id?: number;
|
||||
userId?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
@@ -31,5 +31,11 @@ export interface ShopDealerCapital {
|
||||
*/
|
||||
export interface ShopDealerCapitalParam extends PageParam {
|
||||
id?: number;
|
||||
// 仅查询当前分销商的收益/资金明细
|
||||
userId?: number;
|
||||
// 可选:按订单过滤
|
||||
orderId?: number;
|
||||
// 可选:资金流动类型过滤
|
||||
flowType?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ export interface ShopDealerOrder {
|
||||
id?: number;
|
||||
// 买家用户ID
|
||||
userId?: number;
|
||||
nickname?: string;
|
||||
// 订单编号(部分接口会直接返回订单号字符串)
|
||||
orderNo?: string;
|
||||
// 订单ID
|
||||
orderId?: number;
|
||||
// 订单总金额(不含运费)
|
||||
@@ -28,6 +31,10 @@ export interface ShopDealerOrder {
|
||||
isInvalid?: number;
|
||||
// 佣金结算(0未结算 1已结算)
|
||||
isSettled?: number;
|
||||
// 佣金解冻(0未解冻 1已解冻)
|
||||
isUnfreeze?: number;
|
||||
// 订单状态
|
||||
orderStatus?: number;
|
||||
// 结算时间
|
||||
settleTime?: number;
|
||||
// 商城ID
|
||||
@@ -47,5 +54,7 @@ export interface ShopDealerOrderParam extends PageParam {
|
||||
secondUserId?: number;
|
||||
thirdUserId?: number;
|
||||
userId?: number;
|
||||
// 数据权限/资源ID(通常传当前登录用户ID)
|
||||
resourceId?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -95,8 +95,9 @@ export async function getShopDealerUser(userId: number) {
|
||||
const res = await request.get<ApiResult<ShopDealerUser>>(
|
||||
'/shop/shop-dealer-user/' + userId
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
if (res.code === 0) {
|
||||
// 未注册为分销商时,后端可能返回 data=null,这里用 null 表示“没有分销商信息”
|
||||
return res.data || null;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface ShopDealerUser {
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
// 分销商等级:0-普通用户 1-超级管理员 2-合伙人(总店) 3-合伙人(分店)
|
||||
dealerLevel?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,21 @@ import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopDealerWithdraw, ShopDealerWithdrawParam } from './model';
|
||||
|
||||
// WeChat transfer v3: backend may return `package_info` for MiniProgram to open the
|
||||
// "confirm receipt" page via `wx.requestMerchantTransfer`.
|
||||
export type ShopDealerWithdrawCreateResult =
|
||||
| string
|
||||
| {
|
||||
package_info?: string;
|
||||
packageInfo?: string;
|
||||
[k: string]: any;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
// When applyStatus=20, user can "receive" (WeChat confirm receipt flow).
|
||||
export type ShopDealerWithdrawReceiveResult = ShopDealerWithdrawCreateResult;
|
||||
|
||||
/**
|
||||
* 分页查询分销商提现明细表
|
||||
*/
|
||||
@@ -33,11 +48,40 @@ export async function listShopDealerWithdraw(params?: ShopDealerWithdrawParam) {
|
||||
/**
|
||||
* 添加分销商提现明细表
|
||||
*/
|
||||
export async function addShopDealerWithdraw(data: ShopDealerWithdraw) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
export async function addShopDealerWithdraw(data: ShopDealerWithdraw): Promise<ShopDealerWithdrawCreateResult> {
|
||||
const res = await request.post<ApiResult<any>>(
|
||||
'/shop/shop-dealer-withdraw',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
// Some backends return `message`, while WeChat transfer flow returns `data.package_info`.
|
||||
return res.data ?? res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户领取(仅当 applyStatus=20 时)- 后台返回 package_info 供小程序调起确认收款页
|
||||
*/
|
||||
export async function receiveShopDealerWithdraw(id: number): Promise<ShopDealerWithdrawReceiveResult> {
|
||||
const res = await request.post<ApiResult<any>>(
|
||||
'/shop/shop-dealer-withdraw/receive/' + id,
|
||||
{}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data ?? res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取成功回调:前端确认收款后通知后台把状态置为 applyStatus=40
|
||||
*/
|
||||
export async function receiveSuccessShopDealerWithdraw(id: number) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-withdraw/receive-success/' + id,
|
||||
{}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 礼品卡
|
||||
* 水票
|
||||
*/
|
||||
export interface ShopGift {
|
||||
// 礼品卡ID
|
||||
|
||||
@@ -81,6 +81,8 @@ export interface ShopGoods {
|
||||
isNew?: number;
|
||||
// 库存
|
||||
stock?: number;
|
||||
// 步长
|
||||
step?: number;
|
||||
// 商品重量
|
||||
goodsWeight?: number;
|
||||
// 消费赚取积分
|
||||
@@ -126,6 +128,10 @@ export interface ShopGoods {
|
||||
expiredDay?: number;
|
||||
// 可购买数量
|
||||
canBuyNumber?: number;
|
||||
// 活动方式:0全平台 1新用户专享
|
||||
activityType?: number;
|
||||
// 配送方式:0送上门 1限自提
|
||||
deliveryMode?: number;
|
||||
}
|
||||
|
||||
export interface BathSet {
|
||||
@@ -146,4 +152,7 @@ export interface ShopGoodsParam extends PageParam {
|
||||
isShow?: number;
|
||||
stock?: number;
|
||||
keywords?: string;
|
||||
recommend?: number;
|
||||
// 0上架 1下架(以实际后端约定为准)
|
||||
status?: number;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import request from '@/utils/request';
|
||||
import request, { ErrorType, RequestError } from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopOrder, ShopOrderParam, OrderCreateRequest } from './model';
|
||||
|
||||
@@ -113,6 +113,44 @@ export interface WxPayResult {
|
||||
paySign: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单重新发起支付(对“已创建但未支付”的订单生成新的预支付参数,不应重复创建订单)
|
||||
*
|
||||
* 说明:不同后端版本可能暴露不同路径,这里做兼容探测;若全部失败,调用方可自行降级处理。
|
||||
*/
|
||||
export interface OrderPrepayRequest {
|
||||
orderId: number;
|
||||
payType: number;
|
||||
}
|
||||
|
||||
export async function prepayShopOrder(data: OrderPrepayRequest) {
|
||||
const urls = [
|
||||
'/shop/shop-order/pay',
|
||||
'/shop/shop-order/prepay',
|
||||
'/shop/shop-order/repay'
|
||||
];
|
||||
|
||||
let lastError: unknown;
|
||||
let businessError: unknown;
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const res = await request.post<ApiResult<WxPayResult>>(url, data, { showError: false });
|
||||
// request.ts 在 code!=0 时会直接 throw;走到这里通常都是 code===0
|
||||
if (res.code === 0) return res.data;
|
||||
} catch (e) {
|
||||
// 若已命中“业务错误”(例如订单已取消/已支付),优先保留该错误用于向上提示;
|
||||
// 不要被后续的 404/网络错误覆盖掉,避免调用方误判为“不支持该接口”而降级走创建订单。
|
||||
if (!businessError && e instanceof RequestError && e.type === ErrorType.BUSINESS_ERROR) {
|
||||
businessError = e;
|
||||
} else {
|
||||
lastError = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(businessError || lastError || new Error('发起支付失败'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
*/
|
||||
@@ -140,3 +178,18 @@ export async function repairOrder(data: ShopOrder) {
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 申请|同意退款
|
||||
*/
|
||||
export async function refundShopOrder(data: ShopOrder) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-order/refund',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PageParam } from '@/api/index';
|
||||
import {OrderGoods} from "@/api/system/orderGoods/model";
|
||||
import type { ShopOrderGoods } from '@/api/shop/shopOrderGoods/model';
|
||||
|
||||
/**
|
||||
* 订单
|
||||
@@ -27,6 +27,14 @@ export interface ShopOrder {
|
||||
merchantName?: string;
|
||||
// 商户编号
|
||||
merchantCode?: string;
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 使用的优惠券id
|
||||
couponId?: number;
|
||||
// 使用的会员卡id
|
||||
@@ -61,6 +69,8 @@ export interface ShopOrder {
|
||||
sendStartTime?: string;
|
||||
// 配送结束时间
|
||||
sendEndTime?: string;
|
||||
// 配送员送达拍照(选填)
|
||||
sendEndImg?: string;
|
||||
// 发货店铺id
|
||||
expressMerchantId?: number;
|
||||
// 发货店铺
|
||||
@@ -83,6 +93,8 @@ export interface ShopOrder {
|
||||
totalNum?: number;
|
||||
// 教练id
|
||||
coachId?: number;
|
||||
// 商品ID
|
||||
formId?: number;
|
||||
// 支付的用户id
|
||||
payUserId?: number;
|
||||
// 0余额支付, 1微信支付,102微信Native,2会员卡支付,3支付宝,4现金,5POS机,6VIP月卡,7VIP年卡,8VIP次卡,9IC月卡,10IC年卡,11IC次卡,12免费,13VIP充值卡,14IC充值卡,15积分支付,16VIP季卡,17IC季卡,18代付
|
||||
@@ -146,7 +158,7 @@ export interface ShopOrder {
|
||||
// 是否已收到赠品
|
||||
hasTakeGift?: string;
|
||||
// 订单商品项
|
||||
orderGoods?: OrderGoods[];
|
||||
orderGoods?: ShopOrderGoods[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,6 +177,14 @@ export interface OrderGoodsItem {
|
||||
export interface OrderCreateRequest {
|
||||
// 商品信息列表
|
||||
goodsItems: OrderGoodsItem[];
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称(可选)
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 收货地址ID
|
||||
addressId?: number;
|
||||
// 支付方式
|
||||
@@ -173,12 +193,18 @@ export interface OrderCreateRequest {
|
||||
couponId?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 配送开始时间(用于预约/配送时间)
|
||||
sendStartTime?: string;
|
||||
// 配送方式 0快递 1自提
|
||||
deliveryType?: number;
|
||||
// 自提店铺ID
|
||||
selfTakeMerchantId?: number;
|
||||
// 订单标题(可选,后端会自动生成)
|
||||
title?: string;
|
||||
// 配送方式:elevator(电梯) / stairs(步梯) / groundFloor(一楼商铺/其他)
|
||||
deliveryMethod?: string;
|
||||
// 楼层(步梯+送上楼时有值,从2开始)
|
||||
deliveryFloor?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,6 +223,14 @@ export interface OrderGoodsItem {
|
||||
export interface OrderCreateRequest {
|
||||
// 商品信息列表
|
||||
goodsItems: OrderGoodsItem[];
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称(可选)
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 收货地址ID
|
||||
addressId?: number;
|
||||
// 支付方式
|
||||
@@ -205,6 +239,8 @@ export interface OrderCreateRequest {
|
||||
couponId?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 配送开始时间(用于预约/配送时间)
|
||||
sendStartTime?: string;
|
||||
// 配送方式 0快递 1自提
|
||||
deliveryType?: number;
|
||||
// 自提店铺ID
|
||||
@@ -223,6 +259,12 @@ export interface ShopOrderParam extends PageParam {
|
||||
payType?: number;
|
||||
isInvoice?: boolean;
|
||||
userId?: number;
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 配送员用户ID
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
keywords?: string;
|
||||
deliveryStatus?: number;
|
||||
statusFilter?: number;
|
||||
|
||||
101
src/api/shop/shopStore/index.ts
Normal file
101
src/api/shop/shopStore/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopStore, ShopStoreParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询门店
|
||||
*/
|
||||
export async function pageShopStore(params: ShopStoreParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStore>>>(
|
||||
'/shop/shop-store/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询门店列表
|
||||
*/
|
||||
export async function listShopStore(params?: ShopStoreParam) {
|
||||
const res = await request.get<ApiResult<ShopStore[]>>(
|
||||
'/shop/shop-store',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加门店
|
||||
*/
|
||||
export async function addShopStore(data: ShopStore) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-store',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改门店
|
||||
*/
|
||||
export async function updateShopStore(data: ShopStore) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-store',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除门店
|
||||
*/
|
||||
export async function removeShopStore(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除门店
|
||||
*/
|
||||
export async function removeBatchShopStore(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询门店
|
||||
*/
|
||||
export async function getShopStore(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStore>>(
|
||||
'/shop/shop-store/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
63
src/api/shop/shopStore/model/index.ts
Normal file
63
src/api/shop/shopStore/model/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 门店
|
||||
*/
|
||||
export interface ShopStore {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
// 店铺名称
|
||||
name?: string;
|
||||
// 门店地址
|
||||
address?: string;
|
||||
// 手机号码
|
||||
phone?: string;
|
||||
// 邮箱
|
||||
email?: string;
|
||||
// 门店经理
|
||||
managerName?: string;
|
||||
// 门店banner
|
||||
shopBanner?: string;
|
||||
// 所在省份
|
||||
province?: string;
|
||||
// 所在城市
|
||||
city?: string;
|
||||
// 所在辖区
|
||||
region?: string;
|
||||
// 经度和纬度
|
||||
lngAndLat?: string;
|
||||
// 位置
|
||||
location?:string;
|
||||
// 区域
|
||||
district?: string;
|
||||
// 轮廓
|
||||
points?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 默认仓库ID(shop_warehouse.id)
|
||||
warehouseId?: number;
|
||||
// 默认仓库名称(可选)
|
||||
warehouseName?: string;
|
||||
// 状态
|
||||
status?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店搜索条件
|
||||
*/
|
||||
export interface ShopStoreParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
101
src/api/shop/shopStoreFence/index.ts
Normal file
101
src/api/shop/shopStoreFence/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api/index';
|
||||
import type { ShopStoreFence, ShopStoreFenceParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询黄家明_电子围栏
|
||||
*/
|
||||
export async function pageShopStoreFence(params: ShopStoreFenceParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStoreFence>>>(
|
||||
'/shop/shop-store-fence/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询黄家明_电子围栏列表
|
||||
*/
|
||||
export async function listShopStoreFence(params?: ShopStoreFenceParam) {
|
||||
const res = await request.get<ApiResult<ShopStoreFence[]>>(
|
||||
'/shop/shop-store-fence',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加黄家明_电子围栏
|
||||
*/
|
||||
export async function addShopStoreFence(data: ShopStoreFence) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-store-fence',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改黄家明_电子围栏
|
||||
*/
|
||||
export async function updateShopStoreFence(data: ShopStoreFence) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-store-fence',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除黄家明_电子围栏
|
||||
*/
|
||||
export async function removeShopStoreFence(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-fence/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除黄家明_电子围栏
|
||||
*/
|
||||
export async function removeBatchShopStoreFence(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-fence/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询黄家明_电子围栏
|
||||
*/
|
||||
export async function getShopStoreFence(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStoreFence>>(
|
||||
'/shop/shop-store-fence/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
43
src/api/shop/shopStoreFence/model/index.ts
Normal file
43
src/api/shop/shopStoreFence/model/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { PageParam } from '@/api/index';
|
||||
|
||||
/**
|
||||
* 黄家明_电子围栏
|
||||
*/
|
||||
export interface ShopStoreFence {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
// 围栏名称
|
||||
name?: string;
|
||||
// 类型 0圆形 1方形
|
||||
type?: number;
|
||||
// 定位
|
||||
location?: string;
|
||||
// 经度
|
||||
longitude?: string;
|
||||
// 纬度
|
||||
latitude?: string;
|
||||
// 区域
|
||||
district?: string;
|
||||
// 电子围栏轮廓
|
||||
points?: string;
|
||||
// 排序(数字越小越靠前)
|
||||
sortNumber?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 状态, 0正常, 1冻结
|
||||
status?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 黄家明_电子围栏搜索条件
|
||||
*/
|
||||
export interface ShopStoreFenceParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
101
src/api/shop/shopStoreRider/index.ts
Normal file
101
src/api/shop/shopStoreRider/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopStoreRider, ShopStoreRiderParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询配送员
|
||||
*/
|
||||
export async function pageShopStoreRider(params: ShopStoreRiderParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStoreRider>>>(
|
||||
'/shop/shop-store-rider/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询配送员列表
|
||||
*/
|
||||
export async function listShopStoreRider(params?: ShopStoreRiderParam) {
|
||||
const res = await request.get<ApiResult<ShopStoreRider[]>>(
|
||||
'/shop/shop-store-rider',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加配送员
|
||||
*/
|
||||
export async function addShopStoreRider(data: ShopStoreRider) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-store-rider',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改配送员
|
||||
*/
|
||||
export async function updateShopStoreRider(data: ShopStoreRider) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-store-rider',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配送员
|
||||
*/
|
||||
export async function removeShopStoreRider(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-rider/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除配送员
|
||||
*/
|
||||
export async function removeBatchShopStoreRider(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-rider/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询配送员
|
||||
*/
|
||||
export async function getShopStoreRider(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStoreRider>>(
|
||||
'/shop/shop-store-rider/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
71
src/api/shop/shopStoreRider/model/index.ts
Normal file
71
src/api/shop/shopStoreRider/model/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 配送员
|
||||
*/
|
||||
export interface ShopStoreRider {
|
||||
// 主键ID
|
||||
id?: string;
|
||||
// 配送点ID(shop_dealer.id)
|
||||
dealerId?: number;
|
||||
// 骑手编号(可选)
|
||||
riderNo?: string;
|
||||
// 姓名
|
||||
realName?: string;
|
||||
// 手机号
|
||||
mobile?: string;
|
||||
// 头像
|
||||
avatar?: string;
|
||||
// 身份证号(可选)
|
||||
idCardNo?: string;
|
||||
// 状态:1启用;0禁用
|
||||
status?: number;
|
||||
// 接单状态:0休息/下线;1在线;2忙碌
|
||||
workStatus?: number;
|
||||
// 是否开启自动派单:1是;0否
|
||||
autoDispatchEnabled?: number;
|
||||
// 派单优先级(同小区多骑手时可用,值越大越优先)
|
||||
dispatchPriority?: number;
|
||||
// 最大同时配送单数(0表示不限制)
|
||||
maxOnhandOrders?: number;
|
||||
// 是否计算工资(提成):1计算;0不计算(如三方配送点可设0)
|
||||
commissionCalcEnabled?: number;
|
||||
// 水每桶提成金额(元/桶)
|
||||
waterBucketUnitFee?: string;
|
||||
// 其他商品提成方式:1按订单固定金额;2按订单金额比例;3按商品规则(另表)
|
||||
otherGoodsCommissionType?: number;
|
||||
// 其他商品提成值:固定金额(元)或比例(%)
|
||||
otherGoodsCommissionValue?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 经度(配送员当前位置)
|
||||
longitude?: string;
|
||||
// 纬度(配送员当前位置)
|
||||
latitude?: string;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配送员搜索条件
|
||||
*/
|
||||
export interface ShopStoreRiderParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
// 配送点/门店ID(后端可能用 dealerId 或 storeId)
|
||||
dealerId?: number;
|
||||
storeId?: number;
|
||||
status?: number;
|
||||
workStatus?: number;
|
||||
autoDispatchEnabled?: number;
|
||||
}
|
||||
101
src/api/shop/shopStoreUser/index.ts
Normal file
101
src/api/shop/shopStoreUser/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopStoreUser, ShopStoreUserParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询店员
|
||||
*/
|
||||
export async function pageShopStoreUser(params: ShopStoreUserParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStoreUser>>>(
|
||||
'/shop/shop-store-user/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询店员列表
|
||||
*/
|
||||
export async function listShopStoreUser(params?: ShopStoreUserParam) {
|
||||
const res = await request.get<ApiResult<ShopStoreUser[]>>(
|
||||
'/shop/shop-store-user',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加店员
|
||||
*/
|
||||
export async function addShopStoreUser(data: ShopStoreUser) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改店员
|
||||
*/
|
||||
export async function updateShopStoreUser(data: ShopStoreUser) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除店员
|
||||
*/
|
||||
export async function removeShopStoreUser(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除店员
|
||||
*/
|
||||
export async function removeBatchShopStoreUser(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询店员
|
||||
*/
|
||||
export async function getShopStoreUser(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStoreUser>>(
|
||||
'/shop/shop-store-user/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
36
src/api/shop/shopStoreUser/model/index.ts
Normal file
36
src/api/shop/shopStoreUser/model/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 店员
|
||||
*/
|
||||
export interface ShopStoreUser {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 配送点ID(shop_dealer.id)
|
||||
storeId?: number;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 店员搜索条件
|
||||
*/
|
||||
export interface ShopStoreUserParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
storeId?: number;
|
||||
userId?: number;
|
||||
isDelete?: number;
|
||||
}
|
||||
101
src/api/shop/shopStoreWarehouse/index.ts
Normal file
101
src/api/shop/shopStoreWarehouse/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api/index';
|
||||
import type { ShopStoreWarehouse, ShopStoreWarehouseParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询仓库
|
||||
*/
|
||||
export async function pageShopStoreWarehouse(params: ShopStoreWarehouseParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStoreWarehouse>>>(
|
||||
'/shop/shop-store-warehouse/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询仓库列表
|
||||
*/
|
||||
export async function listShopStoreWarehouse(params?: ShopStoreWarehouseParam) {
|
||||
const res = await request.get<ApiResult<ShopStoreWarehouse[]>>(
|
||||
'/shop/shop-store-warehouse',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加仓库
|
||||
*/
|
||||
export async function addShopStoreWarehouse(data: ShopStoreWarehouse) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-store-warehouse',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改仓库
|
||||
*/
|
||||
export async function updateShopStoreWarehouse(data: ShopStoreWarehouse) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-store-warehouse',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除仓库
|
||||
*/
|
||||
export async function removeShopStoreWarehouse(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-warehouse/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除仓库
|
||||
*/
|
||||
export async function removeBatchShopStoreWarehouse(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-warehouse/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询仓库
|
||||
*/
|
||||
export async function getShopStoreWarehouse(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStoreWarehouse>>(
|
||||
'/shop/shop-store-warehouse/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
53
src/api/shop/shopStoreWarehouse/model/index.ts
Normal file
53
src/api/shop/shopStoreWarehouse/model/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { PageParam } from '@/api/index';
|
||||
|
||||
/**
|
||||
* 仓库
|
||||
*/
|
||||
export interface ShopStoreWarehouse {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
// 仓库名称
|
||||
name?: string;
|
||||
// 唯一标识
|
||||
code?: string;
|
||||
// 类型 中心仓,区域仓,门店仓
|
||||
type?: string;
|
||||
// 仓库地址
|
||||
address?: string;
|
||||
// 真实姓名
|
||||
realName?: string;
|
||||
// 联系电话
|
||||
phone?: string;
|
||||
// 所在省份
|
||||
province?: string;
|
||||
// 所在城市
|
||||
city?: string;
|
||||
// 所在辖区
|
||||
region?: string;
|
||||
// 经纬度
|
||||
lngAndLat?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 状态
|
||||
status?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仓库搜索条件
|
||||
*/
|
||||
export interface ShopStoreWarehouseParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
@@ -38,6 +38,8 @@ export interface ShopUserAddress {
|
||||
tenantId?: number;
|
||||
// 注册时间
|
||||
createTime?: string;
|
||||
// 更新时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function uploadOssByPath(filePath: string) {
|
||||
let stsExpired = Taro.getStorageSync('stsExpiredAt');
|
||||
if (!sts || (stsExpired && dayjs().isBefore(dayjs(stsExpired)))) {
|
||||
// @ts-ignore
|
||||
const {data: {data: {credentials}}} = await request.get(`https://server.websoft.top/api/oss/getSTSToken`)
|
||||
const {data: {data: {credentials}}} = await request.get(`https://gle-server.websoft.top/api/oss/getSTSToken`)
|
||||
Taro.setStorageSync('sts', credentials)
|
||||
Taro.setStorageSync('stsExpiredAt', credentials.expiration)
|
||||
sts = credentials
|
||||
@@ -49,7 +49,7 @@ export async function uploadOssByPath(filePath: string) {
|
||||
})
|
||||
}
|
||||
|
||||
const computeSignature = (accessKeySecret, canonicalString) => {
|
||||
const computeSignature = (accessKeySecret: string, canonicalString: string): string => {
|
||||
return crypto.enc.Base64.stringify(crypto.HmacSHA1(canonicalString, accessKeySecret));
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export async function uploadFile() {
|
||||
const tempFilePath = res.tempFilePaths[0];
|
||||
// 上传图片到OSS
|
||||
Taro.uploadFile({
|
||||
url: 'https://server.websoft.top/api/oss/upload',
|
||||
url: 'https://glt-server.websoft.top/api/oss/upload',
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
header: {
|
||||
|
||||
@@ -30,3 +30,18 @@ export async function updateUserRole(data: UserRole) {
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户角色
|
||||
* 说明:部分后端实现为 POST 新增、PUT 修改;这里补齐 API 以便新用户无角色时可以创建默认角色。
|
||||
*/
|
||||
export async function addUserRole(data: UserRole) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/system/user-role',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
@@ -43,6 +43,15 @@ export interface UserOrderStats {
|
||||
total: number
|
||||
}
|
||||
|
||||
// 用户卡片统计(个人中心头部:余额/积分/优惠券/水票)
|
||||
export interface UserCardStats {
|
||||
balance: string
|
||||
points: number
|
||||
coupons: number
|
||||
giftCards: number
|
||||
lastUpdateTime?: string
|
||||
}
|
||||
|
||||
// 用户完整数据
|
||||
export interface UserDashboard {
|
||||
balance: UserBalance
|
||||
@@ -108,6 +117,17 @@ export async function getUserOrderStats() {
|
||||
return Promise.reject(new Error(res.message))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户卡片统计(一次性返回余额/积分/可用优惠券/未使用礼品卡数量)
|
||||
*/
|
||||
export async function getUserCardStats() {
|
||||
const res = await request.get<ApiResult<UserCardStats>>('/user/card/stats')
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data
|
||||
}
|
||||
return Promise.reject(new Error(res.message))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户完整仪表板数据(一次性获取所有数据)
|
||||
*/
|
||||
|
||||
@@ -54,10 +54,16 @@ export default {
|
||||
"wallet/wallet",
|
||||
"coupon/index",
|
||||
"points/points",
|
||||
"gift/index",
|
||||
"gift/redeem",
|
||||
"gift/detail",
|
||||
"ticket/index",
|
||||
"ticket/use",
|
||||
"ticket/release/index",
|
||||
"ticket/orders/index",
|
||||
// "gift/index",
|
||||
// "gift/redeem",
|
||||
// "gift/detail",
|
||||
// "gift/add",
|
||||
"store/verification",
|
||||
"store/orders/index",
|
||||
"theme/index",
|
||||
"poster/poster",
|
||||
"chat/conversation/index",
|
||||
@@ -73,6 +79,7 @@ export default {
|
||||
"apply/add",
|
||||
"withdraw/index",
|
||||
"orders/index",
|
||||
"capital/index",
|
||||
"team/index",
|
||||
"qrcode/index",
|
||||
"invite-stats/index",
|
||||
@@ -90,6 +97,21 @@ export default {
|
||||
'comments/index',
|
||||
'search/index']
|
||||
},
|
||||
{
|
||||
"root": "store",
|
||||
"pages": [
|
||||
"index",
|
||||
"orders/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "rider",
|
||||
"pages": [
|
||||
"index",
|
||||
"orders/index",
|
||||
"ticket/verification/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "admin",
|
||||
"pages": [
|
||||
@@ -116,12 +138,6 @@ export default {
|
||||
selectedIconPath: "assets/tabbar/home-active.png",
|
||||
text: "首页",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/category/index",
|
||||
iconPath: "assets/tabbar/category.png",
|
||||
selectedIconPath: "assets/tabbar/category-active.png",
|
||||
text: "基地生活",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/cart/cart",
|
||||
iconPath: "assets/tabbar/cart.png",
|
||||
@@ -144,6 +160,9 @@ export default {
|
||||
permission: {
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||
},
|
||||
"scope.writePhotosAlbum": {
|
||||
"desc": "用于保存小程序码到相册,方便分享给好友"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,10 @@ button[open-type="chooseAvatar"] {
|
||||
justify-content: center;
|
||||
height: 80px;
|
||||
}
|
||||
.cart-buy-only{
|
||||
border-radius: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
image {
|
||||
|
||||
@@ -44,7 +44,7 @@ function Category() {
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: `${nav?.categoryName}_时里院子市集`,
|
||||
title: `${nav?.categoryName}_桂乐淘`,
|
||||
path: `/shop/category/index?id=${categoryId}`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
|
||||
@@ -5,6 +5,7 @@ import {getUserInfo} from "@/api/layout";
|
||||
import {useEffect, useState} from "react";
|
||||
import {getCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import { goToRegister } from '@/utils/auth'
|
||||
|
||||
function AddCartBar() {
|
||||
const { router } = getCurrentInstance();
|
||||
@@ -13,13 +14,8 @@ function AddCartBar() {
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||
const onPay = () => {
|
||||
if (!IsLogin) {
|
||||
Taro.showToast({title: `请先登录`, icon: 'error'})
|
||||
setTimeout(() => {
|
||||
Taro.switchTab(
|
||||
{
|
||||
url: '/pages/user/user',
|
||||
},
|
||||
)
|
||||
goToRegister({ redirect: '/pages/user/user' })
|
||||
}, 1000)
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface GiftCardProps {
|
||||
faceValue?: string
|
||||
/** 商品原价 */
|
||||
originalPrice?: string
|
||||
/** 礼品卡类型:10-实物礼品卡 20-虚拟礼品卡 30-服务礼品卡 */
|
||||
/** 礼品卡类型:10-礼品劵 20-虚拟礼品卡 30-服务礼品卡 */
|
||||
type?: number
|
||||
/** 状态:0-未使用 1-已使用 2-失效 */
|
||||
status?: number
|
||||
@@ -112,10 +112,10 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 10: return '礼品劵'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
default: return '水票'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const GiftCardGuide: React.FC<GiftCardGuideProps> = ({
|
||||
title: '礼品卡类型说明',
|
||||
icon: <Gift size="24" className="text-purple-500" />,
|
||||
content: [
|
||||
'🎁 实物礼品卡:需到指定地址领取商品',
|
||||
'🎁 礼品劵:需到指定地址领取商品',
|
||||
'💻 虚拟礼品卡:自动发放到账户余额',
|
||||
'🛎️ 服务礼品卡:联系客服预约服务',
|
||||
'⏰ 注意查看有效期,过期无法使用'
|
||||
|
||||
@@ -28,10 +28,10 @@ const GiftCardShare: React.FC<GiftCardShareProps> = ({
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (giftCard.type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 10: return '礼品劵'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
default: return '水票'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PaymentCountdown 支付倒计时组件
|
||||
|
||||
基于订单创建时间的支付倒计时组件,支持静态显示和实时更新两种模式。
|
||||
基于订单过期时间(`expirationTime`)的支付倒计时组件,支持静态显示和实时更新两种模式。
|
||||
|
||||
## 功能特性
|
||||
|
||||
@@ -19,7 +19,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
||||
|
||||
// 订单列表页 - 静态显示
|
||||
<PaymentCountdown
|
||||
createTime={order.createTime}
|
||||
expirationTime={order.expirationTime}
|
||||
payStatus={order.payStatus}
|
||||
realTime={false}
|
||||
mode="badge"
|
||||
@@ -27,7 +27,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
||||
|
||||
// 订单详情页 - 实时更新
|
||||
<PaymentCountdown
|
||||
createTime={order.createTime}
|
||||
expirationTime={order.expirationTime}
|
||||
payStatus={order.payStatus}
|
||||
realTime={true}
|
||||
showSeconds={true}
|
||||
@@ -43,7 +43,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
||||
```tsx
|
||||
// 自定义超时时间(12小时)
|
||||
<PaymentCountdown
|
||||
createTime={order.createTime}
|
||||
expirationTime={order.expirationTime}
|
||||
payStatus={order.payStatus}
|
||||
realTime={true}
|
||||
timeoutHours={12}
|
||||
@@ -55,7 +55,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
||||
|
||||
// 纯文本模式
|
||||
<PaymentCountdown
|
||||
createTime={order.createTime}
|
||||
expirationTime={order.expirationTime}
|
||||
payStatus={order.payStatus}
|
||||
realTime={false}
|
||||
mode="text"
|
||||
@@ -67,6 +67,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| createTime | string | - | 订单创建时间 |
|
||||
| expirationTime | string | - | 订单过期时间(推荐) |
|
||||
| payStatus | boolean | false | 支付状态 |
|
||||
| realTime | boolean | false | 是否实时更新 |
|
||||
| timeoutHours | number | 24 | 超时小时数 |
|
||||
@@ -102,12 +103,13 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
||||
import { usePaymentCountdown, formatCountdownText } from '@/hooks/usePaymentCountdown';
|
||||
|
||||
const MyComponent = ({ order }) => {
|
||||
const timeLeft = usePaymentCountdown(
|
||||
order.createTime,
|
||||
order.payStatus,
|
||||
true, // 实时更新
|
||||
24 // 24小时超时
|
||||
);
|
||||
const timeLeft = usePaymentCountdown({
|
||||
expirationTime: order.expirationTime,
|
||||
createTime: order.createTime, // expirationTime 缺失时回退
|
||||
payStatus: order.payStatus,
|
||||
realTime: true,
|
||||
timeoutHours: 24
|
||||
});
|
||||
|
||||
const countdownText = formatCountdownText(timeLeft, true);
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import './PaymentCountdown.scss';
|
||||
export interface PaymentCountdownProps {
|
||||
/** 订单创建时间 */
|
||||
createTime?: string;
|
||||
/** 订单过期时间(推荐直接传后端返回的 expirationTime) */
|
||||
expirationTime?: string;
|
||||
/** 支付状态 */
|
||||
payStatus?: boolean;
|
||||
/** 是否实时更新(详情页用true,列表页用false) */
|
||||
@@ -29,18 +31,25 @@ export interface PaymentCountdownProps {
|
||||
|
||||
const PaymentCountdown: React.FC<PaymentCountdownProps> = ({
|
||||
createTime,
|
||||
expirationTime,
|
||||
payStatus = false,
|
||||
realTime = false,
|
||||
timeoutHours = 1,
|
||||
timeoutHours = 24,
|
||||
showSeconds = false,
|
||||
className = '',
|
||||
onExpired,
|
||||
mode = 'badge'
|
||||
}) => {
|
||||
const timeLeft = usePaymentCountdown(createTime, payStatus, realTime, timeoutHours);
|
||||
const timeLeft = usePaymentCountdown({
|
||||
createTime,
|
||||
expirationTime,
|
||||
payStatus,
|
||||
realTime,
|
||||
timeoutHours
|
||||
});
|
||||
|
||||
// 如果已支付或没有创建时间,不显示倒计时
|
||||
if (payStatus || !createTime) {
|
||||
// 如果已支付或没有可计算的截止时间,不显示倒计时
|
||||
if (payStatus || (!expirationTime && !createTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ const SimpleQRCodeModal: React.FC<SimpleQRCodeModalProps> = ({
|
||||
{qrContent ? (
|
||||
<View className={'flex flex-col justify-center'}>
|
||||
<img
|
||||
src={`https://mp-api.websoft.top/api/qr-code/create-encrypted-qr-image?size=300x300&expireMinutes=60&businessType=gift&data=${encodeURIComponent(qrContent)}`}
|
||||
src={`https://glt-api.websoft.top/api/qr-code/create-encrypted-qr-image?size=300x300&expireMinutes=60&businessType=gift&data=${encodeURIComponent(qrContent)}`}
|
||||
alt="二维码"
|
||||
style={{width: '200px', height: '200px'}}
|
||||
className="mx-auto"
|
||||
|
||||
@@ -68,7 +68,7 @@ const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
||||
setTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '是否继续扫码核销其他礼品卡?',
|
||||
content: '是否继续扫码核销其他水票/礼品卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleClick(); // 递归调用继续扫码
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '邀请注册',
|
||||
navigationBarTitleText: '注册成为会员',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
|
||||
@@ -10,7 +10,9 @@ import {updateUser} from "@/api/system/user";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite";
|
||||
import {addShopDealerUser} from "@/api/shop/shopDealerUser";
|
||||
import {listUserRole, updateUserRole} from "@/api/system/userRole";
|
||||
import {addUserRole, listUserRole, updateUserRole} from "@/api/system/userRole";
|
||||
import { listRoles } from "@/api/system/role";
|
||||
import type { UserRole } from "@/api/system/userRole/model";
|
||||
|
||||
// 类型定义
|
||||
interface ChooseAvatarEvent {
|
||||
@@ -26,7 +28,7 @@ interface InputEvent {
|
||||
}
|
||||
|
||||
const AddUserAddress = () => {
|
||||
const {user, loginUser} = useUser()
|
||||
const {user, loginUser, fetchUserInfo} = useUser()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [FormData, setFormData] = useState<User>()
|
||||
const formRef = useRef<any>(null)
|
||||
@@ -59,7 +61,7 @@ const AddUserAddress = () => {
|
||||
setFormData(tempFormData)
|
||||
|
||||
Taro.uploadFile({
|
||||
url: 'https://server.websoft.top/api/oss/upload',
|
||||
url: 'https://glt-server.websoft.top/api/oss/upload',
|
||||
filePath: detail.avatarUrl,
|
||||
name: 'file',
|
||||
header: {
|
||||
@@ -127,7 +129,7 @@ const AddUserAddress = () => {
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
const submitSucceed = async (values: User) => {
|
||||
try {
|
||||
// 验证必填字段
|
||||
if (!values.phone && !FormData?.phone) {
|
||||
@@ -142,8 +144,8 @@ const AddUserAddress = () => {
|
||||
const nickname = values.realName || FormData?.nickname || '';
|
||||
if (!nickname || nickname.trim() === '') {
|
||||
Taro.showToast({
|
||||
title: '请填写昵称',
|
||||
icon: 'error'
|
||||
title: '请上传头像和填写昵称',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -176,12 +178,27 @@ const AddUserAddress = () => {
|
||||
}
|
||||
console.log(values,FormData)
|
||||
|
||||
const roles = await listUserRole({userId: user?.userId})
|
||||
if (!user?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息缺失,请先登录',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let roles: UserRole[] = [];
|
||||
try {
|
||||
roles = await listUserRole({userId: user.userId})
|
||||
console.log(roles, 'roles...')
|
||||
} catch (e) {
|
||||
// 新用户/权限限制时可能查不到角色列表,不影响基础注册流程
|
||||
console.warn('查询用户角色失败,将尝试直接写入默认角色:', e)
|
||||
roles = []
|
||||
}
|
||||
|
||||
// 准备提交的数据
|
||||
await updateUser({
|
||||
userId: user?.userId,
|
||||
userId: user.userId,
|
||||
nickname: values.realName || FormData?.nickname,
|
||||
phone: values.phone || FormData?.phone,
|
||||
avatar: values.avatar || FormData?.avatar,
|
||||
@@ -189,17 +206,55 @@ const AddUserAddress = () => {
|
||||
});
|
||||
|
||||
await addShopDealerUser({
|
||||
userId: user?.userId,
|
||||
userId: user.userId,
|
||||
realName: values.realName || FormData?.nickname,
|
||||
mobile: values.phone || FormData?.phone,
|
||||
refereeId: values.refereeId || FormData?.refereeId
|
||||
refereeId: Number(values.refereeId) || Number(FormData?.refereeId)
|
||||
})
|
||||
|
||||
// 通知其他页面(如“我的”页、分销中心页)刷新经销商信息
|
||||
Taro.eventCenter.trigger('dealerUser:changed')
|
||||
|
||||
// 角色为空时这里会导致“注册成功但没有角色”,这里做一次兜底写入默认 user 角色
|
||||
try {
|
||||
// 1) 先尝试通过 roleCode=user 查询角色ID(避免硬编码)
|
||||
// 2) 取不到就回退到旧的默认ID(1848)
|
||||
let userRoleId: number | undefined;
|
||||
try {
|
||||
// 注意:当前 request.get 的封装不支持 axios 风格的 { params: ... },
|
||||
// 某些自动生成的 API 可能无法按参数过滤;这里直接取全量再本地查找更稳。
|
||||
const roleList = await listRoles();
|
||||
userRoleId = roleList?.find(r => r.roleCode === 'user')?.roleId;
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
if (!userRoleId) userRoleId = 1848;
|
||||
|
||||
const baseRolePayload = {
|
||||
userId: user.userId,
|
||||
tenantId: Number(TenantId),
|
||||
roleId: userRoleId
|
||||
};
|
||||
|
||||
// 后端若已创建 user-role 记录则更新;否则尝试“无id更新”触发创建(多数实现会 upsert)
|
||||
if (roles.length > 0) {
|
||||
await updateUserRole({
|
||||
...roles[0],
|
||||
roleId: 1848
|
||||
})
|
||||
roleId: userRoleId
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
await addUserRole(baseRolePayload);
|
||||
} catch (_) {
|
||||
// 兼容后端仅支持 PUT upsert 的情况
|
||||
await updateUserRole(baseRolePayload);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新一次用户信息,确保 roles 写回本地缓存,避免“我的”页显示为空/不一致
|
||||
await fetchUserInfo();
|
||||
} catch (e) {
|
||||
console.warn('写入默认角色失败(不影响注册成功):', e)
|
||||
}
|
||||
|
||||
|
||||
@@ -209,7 +264,8 @@ const AddUserAddress = () => {
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
// “我的”是 tabBar 页面,注册完成后直接切到“我的”
|
||||
Taro.switchTab({ url: '/pages/user/user' });
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
@@ -241,7 +297,7 @@ const AddUserAddress = () => {
|
||||
success: (loginRes) => {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
authCode: loginRes.code,
|
||||
@@ -382,9 +438,9 @@ const AddUserAddress = () => {
|
||||
>
|
||||
<View className={'bg-gray-100 h-3'}></View>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>
|
||||
<Input placeholder="邀请人ID" disabled={true}/>
|
||||
</Form.Item>
|
||||
{/*<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>*/}
|
||||
{/* <Input placeholder="邀请人ID" disabled={false}/>*/}
|
||||
{/*</Form.Item>*/}
|
||||
<Form.Item name="phone" label="手机号" initialValue={FormData?.phone} required>
|
||||
<View className="flex items-center justify-between">
|
||||
<Input
|
||||
|
||||
4
src/dealer/capital/index.config.ts
Normal file
4
src/dealer/capital/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '收益明细'
|
||||
})
|
||||
|
||||
2
src/dealer/capital/index.scss
Normal file
2
src/dealer/capital/index.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
/* Intentionally empty: styling is done via utility classes. */
|
||||
|
||||
199
src/dealer/capital/index.tsx
Normal file
199
src/dealer/capital/index.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react'
|
||||
import {View, Text, ScrollView} from '@tarojs/components'
|
||||
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {pageShopDealerCapital} from '@/api/shop/shopDealerCapital'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import type {ShopDealerCapital} from '@/api/shop/shopDealerCapital/model'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const DealerCapital: React.FC = () => {
|
||||
const {dealerUser} = useDealerUser()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [records, setRecords] = useState<ShopDealerCapital[]>([])
|
||||
|
||||
const getFlowTypeText = (flowType?: number) => {
|
||||
switch (flowType) {
|
||||
case 10:
|
||||
return '佣金收入'
|
||||
case 20:
|
||||
return '提现支出'
|
||||
case 30:
|
||||
return '转账支出'
|
||||
case 40:
|
||||
return '转账收入'
|
||||
default:
|
||||
return '资金变动'
|
||||
}
|
||||
}
|
||||
|
||||
const getFlowTypeTag = (flowType?: number) => {
|
||||
// 收入:success;支出:danger;其它:default
|
||||
if (flowType === 10 || flowType === 40) return 'success'
|
||||
if (flowType === 20 || flowType === 30) return 'danger'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
const formatMoney = (flowType?: number, money?: string) => {
|
||||
const isIncome = flowType === 10 || flowType === 40
|
||||
const isExpense = flowType === 20 || flowType === 30
|
||||
const sign = isIncome ? '+' : isExpense ? '-' : ''
|
||||
return `${sign}${money || '0.00'}`
|
||||
}
|
||||
|
||||
const fetchRecords = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true)
|
||||
} else if (page === 1) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoadingMore(true)
|
||||
}
|
||||
|
||||
const result = await pageShopDealerCapital({
|
||||
page,
|
||||
limit: PAGE_SIZE,
|
||||
// 只显示与当前登录用户相关的收益明细
|
||||
userId: dealerUser.userId
|
||||
})
|
||||
|
||||
const list = result?.list || []
|
||||
if (page === 1) {
|
||||
setRecords(list)
|
||||
} else {
|
||||
setRecords(prev => [...prev, ...list])
|
||||
}
|
||||
|
||||
setHasMore(list.length === PAGE_SIZE)
|
||||
setCurrentPage(page)
|
||||
} catch (error) {
|
||||
console.error('获取收益明细失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取收益明细失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setRefreshing(false)
|
||||
setLoadingMore(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await fetchRecords(1, true)
|
||||
}
|
||||
|
||||
const handleLoadMore = async () => {
|
||||
if (!loadingMore && hasMore) {
|
||||
await fetchRecords(currentPage + 1)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchRecords(1)
|
||||
}
|
||||
}, [fetchRecords, dealerUser?.userId])
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50">
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshing}
|
||||
pullingText="下拉刷新"
|
||||
canReleaseText="释放刷新"
|
||||
refreshingText="刷新中..."
|
||||
completeText="刷新完成"
|
||||
>
|
||||
<ScrollView
|
||||
scrollY
|
||||
className="h-screen"
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={50}
|
||||
>
|
||||
<View className="p-4">
|
||||
{loading && records.length === 0 ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : records.length > 0 ? (
|
||||
<>
|
||||
{records.map((item) => (
|
||||
<View key={item.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-1">
|
||||
<Text className="font-semibold text-gray-800">
|
||||
{item.describe || '收益明细'}
|
||||
</Text>
|
||||
<Tag type={getFlowTypeTag(item.flowType)}>
|
||||
{getFlowTypeText(item.flowType)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center mb-1">
|
||||
<Text className="text-sm text-gray-400">
|
||||
佣金收入
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-sm font-semibold ${
|
||||
item.flowType === 10 || item.flowType === 40 ? 'text-green-600' :
|
||||
item.flowType === 20 || item.flowType === 30 ? 'text-red-500' :
|
||||
'text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{formatMoney(item.flowType, item.money)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-sm text-gray-400">
|
||||
{/*用户:{item.userId ?? '-'}*/}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">
|
||||
{item.createTime || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{loadingMore && (
|
||||
<View className="text-center py-4">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-1 text-sm">加载更多...</Text>
|
||||
</View>
|
||||
)}
|
||||
{!hasMore && records.length > 0 && (
|
||||
<View className="text-center py-4">
|
||||
<Text className="text-gray-400 text-sm">没有更多数据了</Text>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无收益明细"/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerCapital
|
||||
98
src/dealer/components/FreezeMoneyModal.tsx
Normal file
98
src/dealer/components/FreezeMoneyModal.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Popup, Button } from '@nutui/nutui-react-taro'
|
||||
import { Close } from '@nutui/icons-react-taro'
|
||||
|
||||
interface FreezeMoneyModalProps {
|
||||
visible: boolean
|
||||
amount: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 待使用明细弹窗组件
|
||||
* 展示待使用(冻结中)金额和解冻规则说明
|
||||
*/
|
||||
const FreezeMoneyModal: React.FC<FreezeMoneyModalProps> = ({
|
||||
visible,
|
||||
amount,
|
||||
onClose
|
||||
}) => {
|
||||
// 格式化金额,保留2位小数
|
||||
const formatAmount = (value: string | number): string => {
|
||||
const num = typeof value === 'string' ? parseFloat(value) : value
|
||||
if (isNaN(num)) return '0.00'
|
||||
return num.toFixed(2)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
style={{ padding: '0', borderRadius: '16px', overflow: 'hidden' }}
|
||||
onClose={onClose}
|
||||
closeOnOverlayClick={true}
|
||||
position="center"
|
||||
>
|
||||
<View className="w-72 bg-white">
|
||||
{/* 头部标题 */}
|
||||
<View className="relative px-4 py-4 border-b border-gray-100">
|
||||
<Text className="text-lg font-semibold text-gray-800 text-center block">
|
||||
待使用明细
|
||||
</Text>
|
||||
<View
|
||||
className="absolute right-3 top-1 -translate-y-1 p-1"
|
||||
onClick={onClose}
|
||||
>
|
||||
<Close size={20} className="text-gray-400" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 金额展示区域 */}
|
||||
<View className="px-6 py-8 text-center">
|
||||
<Text className="text-sm text-gray-500 mb-2 block">
|
||||
待使用(冻结中)
|
||||
</Text>
|
||||
<View className="flex items-baseline justify-center">
|
||||
<Text className="text-2xl font-bold text-gray-800">¥</Text>
|
||||
<Text className="text-4xl font-bold text-gray-800 ml-1">
|
||||
{formatAmount(amount)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 分隔线 */}
|
||||
<View className="mx-6 h-px bg-gray-100" />
|
||||
|
||||
{/* 温馨提示区域 */}
|
||||
<View className="px-6 py-6">
|
||||
<Text className="text-sm font-medium text-gray-700 mb-3 block">
|
||||
温馨提示:
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500 leading-relaxed block">
|
||||
需要直推用户进行下单配送后方可解冻到待提现资金组
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="px-6 pb-6">
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
height: '44px',
|
||||
lineHeight: '44px'
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default FreezeMoneyModal
|
||||
@@ -1,3 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '分销中心'
|
||||
navigationBarTitleText: '账户管理中心'
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
|
||||
import {
|
||||
@@ -10,8 +10,11 @@ import {
|
||||
People
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {useUser} from '@/hooks/useUser'
|
||||
import { useThemeStyles } from '@/hooks/useTheme'
|
||||
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
|
||||
import {updateShopDealerUser} from '@/api/shop/shopDealerUser'
|
||||
import FreezeMoneyModal from './components/FreezeMoneyModal'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const DealerIndex: React.FC = () => {
|
||||
@@ -21,6 +24,12 @@ const DealerIndex: React.FC = () => {
|
||||
refresh,
|
||||
} = useDealerUser()
|
||||
|
||||
// 待使用明细弹窗显示状态
|
||||
const [freezeMoneyModalVisible, setFreezeMoneyModalVisible] = useState(false)
|
||||
|
||||
// 获取用户角色信息
|
||||
const { hasRole } = useUser()
|
||||
|
||||
// 使用主题样式
|
||||
const themeStyles = useThemeStyles()
|
||||
|
||||
@@ -55,6 +64,75 @@ const DealerIndex: React.FC = () => {
|
||||
|
||||
console.log(getGradientBackground(),'getGradientBackground()')
|
||||
|
||||
// 判断是否是配送员
|
||||
const isRider = hasRole('rider')
|
||||
|
||||
// 点击待使用金额 - 显示待使用明细弹窗
|
||||
const handleFreezeMoneyClick = () => {
|
||||
console.log('点击待使用金额', dealerUser?.freezeMoney)
|
||||
const freezeMoney = Number(dealerUser?.freezeMoney ?? 0)
|
||||
// 只要有金额就显示弹窗,包括0元也显示(让用户知道当前状态)
|
||||
setFreezeMoneyModalVisible(true)
|
||||
}
|
||||
|
||||
// 关闭待使用明细弹窗
|
||||
const handleCloseFreezeMoneyModal = () => {
|
||||
setFreezeMoneyModalVisible(false)
|
||||
}
|
||||
|
||||
// 配送员专用:将冻结金额转入可提现
|
||||
const handleTransferFreezeMoney = async () => {
|
||||
// 检查是否是配送员
|
||||
if (!isRider) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查冻结金额是否为 0
|
||||
const freezeMoney = Number(dealerUser?.freezeMoney ?? 0)
|
||||
if (freezeMoney <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
setFreezeMoneyModalVisible(false)
|
||||
|
||||
// 弹出确认框
|
||||
Taro.showModal({
|
||||
title: '确认操作',
|
||||
content: `确定要将 ¥${freezeMoney.toFixed(2)} 转入钱包吗?`,
|
||||
confirmText: '确定',
|
||||
cancelText: '取消',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
Taro.showLoading({ title: '处理中...' })
|
||||
const currentMoney = Number(dealerUser?.money ?? 0)
|
||||
await updateShopDealerUser({
|
||||
id: dealerUser?.id,
|
||||
money: (currentMoney + freezeMoney).toFixed(2),
|
||||
freezeMoney: '0.00'
|
||||
})
|
||||
Taro.hideLoading()
|
||||
Taro.showToast({
|
||||
title: '更新成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
// 刷新数据
|
||||
refresh()
|
||||
} catch (error) {
|
||||
Taro.hideLoading()
|
||||
Taro.showToast({
|
||||
title: '更新失败',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="p-4">
|
||||
@@ -108,7 +186,7 @@ const DealerIndex: React.FC = () => {
|
||||
<View className="text-sm" style={{
|
||||
color: 'rgba(255, 255, 255, 0.8)'
|
||||
}}>
|
||||
ID: {dealerUser.userId} | 推荐人: {dealerUser.refereeId || '无'}
|
||||
ID: {dealerUser.userId}
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-right hidden">
|
||||
@@ -129,26 +207,33 @@ const DealerIndex: React.FC = () => {
|
||||
{dealerUser && (
|
||||
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
|
||||
<View className="mb-4">
|
||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||
<Text className="font-semibold text-gray-800">资金统计</Text>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-3">
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.available
|
||||
}}>
|
||||
}} onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
{formatMoney(dealerUser.money)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
background: businessGradients.money.frozen
|
||||
}}>
|
||||
<View
|
||||
className="text-center p-3 rounded-lg flex flex-col"
|
||||
style={{
|
||||
background: businessGradients.money.frozen,
|
||||
opacity: Number(dealerUser.freezeMoney ?? 0) > 0 ? 1 : 0.8
|
||||
}}
|
||||
onClick={handleFreezeMoneyClick}
|
||||
>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
{formatMoney(dealerUser.freezeMoney)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>
|
||||
待使用
|
||||
</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.total
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
@@ -217,7 +302,7 @@ const DealerIndex: React.FC = () => {
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
||||
<Grid.Item text={'申请提现'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Purse color="#10b981" size="20"/>
|
||||
@@ -225,7 +310,7 @@ const DealerIndex: React.FC = () => {
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
|
||||
<Grid.Item text={'我的团队'} onClick={() => navigateToPage('/dealer/team/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<People color="#8b5cf6" size="20"/>
|
||||
@@ -233,7 +318,7 @@ const DealerIndex: React.FC = () => {
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
|
||||
<Grid.Item text={'实名认证'} onClick={() => navigateToPage('/user/userVerify/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Dongdong color="#f59e0b" size="20"/>
|
||||
@@ -286,6 +371,13 @@ const DealerIndex: React.FC = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 待使用明细弹窗 */}
|
||||
<FreezeMoneyModal
|
||||
visible={freezeMoneyModalVisible}
|
||||
amount={dealerUser?.freezeMoney || '0'}
|
||||
onClose={handleCloseFreezeMoneyModal}
|
||||
/>
|
||||
|
||||
{/* 底部安全区域 */}
|
||||
<View className="h-20"></View>
|
||||
</View>
|
||||
|
||||
@@ -24,7 +24,8 @@ const DealerOrders: React.FC = () => {
|
||||
|
||||
// 获取订单数据
|
||||
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||
if (!dealerUser?.userId) return
|
||||
// 需要当前登录用户ID(用于 resourceId 参数)
|
||||
if (!dealerUser || !dealerUser.userId) return
|
||||
|
||||
try {
|
||||
if (isRefresh) {
|
||||
@@ -37,14 +38,17 @@ const DealerOrders: React.FC = () => {
|
||||
|
||||
const result = await pageShopDealerOrder({
|
||||
page,
|
||||
limit: 10
|
||||
limit: 10,
|
||||
// 后端需要 resourceId=当前登录用户ID 才能正确过滤分销订单
|
||||
resourceId: dealerUser.userId
|
||||
})
|
||||
|
||||
if (result?.list) {
|
||||
const newOrders = result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
// 优先使用接口返回的订单号;没有则降级展示 orderId
|
||||
orderNo: order.orderNo ?? (order.orderId != null ? String(order.orderId) : undefined),
|
||||
customerName: `${order.nickname}${order.userId}`,
|
||||
userCommission: order.firstMoney || '0.00'
|
||||
}))
|
||||
|
||||
@@ -90,44 +94,53 @@ const DealerOrders: React.FC = () => {
|
||||
}
|
||||
}, [fetchOrders])
|
||||
|
||||
const getStatusText = (isSettled?: number, isInvalid?: number) => {
|
||||
const getStatusText = (isSettled?: number, isInvalid?: number, isUnfreeze?: number, orderStatus?: number) => {
|
||||
if (orderStatus === 2 || orderStatus === 5 || orderStatus === 6) return '已取消'
|
||||
if (isInvalid === 1) return '已失效'
|
||||
if (isUnfreeze === 1) return '已解冻'
|
||||
if (isSettled === 1) return '已结算'
|
||||
return '待结算'
|
||||
}
|
||||
|
||||
const getStatusColor = (isSettled?: number, isInvalid?: number) => {
|
||||
const getStatusColor = (isSettled?: number, isInvalid?: number, isUnfreeze?: number, orderStatus?: number) => {
|
||||
if (orderStatus === 2 || orderStatus === 5 || orderStatus === 6) return 'default'
|
||||
if (isInvalid === 1) return 'danger'
|
||||
if (isSettled === 1) return 'success'
|
||||
if (isUnfreeze === 1) return 'success'
|
||||
if (isSettled === 1) return 'info'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
const handleGoCapital = () => {
|
||||
Taro.navigateTo({url: '/dealer/capital/index'})
|
||||
}
|
||||
|
||||
const renderOrderItem = (order: OrderWithDetails) => (
|
||||
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View
|
||||
key={order.id}
|
||||
className="bg-white rounded-lg p-4 mb-3 shadow-sm"
|
||||
onClick={handleGoCapital}
|
||||
>
|
||||
<View className="flex justify-between items-start mb-1">
|
||||
<Text className="font-semibold text-gray-800">
|
||||
订单号:{order.orderNo}
|
||||
订单号:{order.orderNo || '-'}
|
||||
</Text>
|
||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
||||
{getStatusText(order.isSettled, order.isInvalid)}
|
||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid, order.isUnfreeze,order.orderStatus)}>
|
||||
{getStatusText(order.isSettled, order.isInvalid, order.isUnfreeze,order.orderStatus)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center mb-1">
|
||||
<Text className="text-sm text-gray-400">
|
||||
订单金额:¥{order.orderPrice || '0.00'}
|
||||
</Text>
|
||||
<Text className="text-sm text-orange-500 font-semibold">
|
||||
我的佣金:¥{order.userCommission}
|
||||
</Text>
|
||||
</View>
|
||||
{/*<View className="flex justify-between items-center mb-1">*/}
|
||||
{/* <Text className="text-sm text-gray-400">*/}
|
||||
{/* 订单金额:¥{order.orderPrice || '0.00'}*/}
|
||||
{/* </Text>*/}
|
||||
{/*</View>*/}
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-sm text-gray-400">
|
||||
客户:{order.customerName}
|
||||
{order.createTime}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">
|
||||
{order.createTime}
|
||||
订单金额:¥{order.orderPrice || '0.00'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '推广二维码'
|
||||
navigationBarTitleText: '账户管理中心',
|
||||
// Enable "Share to friends" and "Share to Moments" (timeline) for this page.
|
||||
enableShareAppMessage: true,
|
||||
enableShareTimeline: true
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react'
|
||||
import {View, Text, Image} from '@tarojs/components'
|
||||
import {Button, Loading} from '@nutui/nutui-react-taro'
|
||||
import {Download, QrCode} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import Taro, {useShareAppMessage} from '@tarojs/taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {generateInviteCode} from '@/api/invite'
|
||||
// import type {InviteStats} from '@/api/invite'
|
||||
@@ -10,10 +10,44 @@ import {businessGradients} from '@/styles/gradients'
|
||||
|
||||
const DealerQrcode: React.FC = () => {
|
||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [codeLoading, setCodeLoading] = useState<boolean>(false)
|
||||
const [saving, setSaving] = useState<boolean>(false)
|
||||
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||
const {dealerUser} = useDealerUser()
|
||||
const {dealerUser, loading: dealerLoading, error, refresh} = useDealerUser()
|
||||
|
||||
// Enable "转发给朋友" + "分享到朋友圈" items in the share panel/menu.
|
||||
useEffect(() => {
|
||||
// Some clients require explicit call to show both share entries.
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
showShareItems: ['shareAppMessage', 'shareTimeline']
|
||||
}).catch(() => {})
|
||||
}, [])
|
||||
|
||||
// 转发给朋友(分享小程序链接)
|
||||
useShareAppMessage(() => {
|
||||
const inviterRaw = dealerUser?.userId ?? Taro.getStorageSync('UserId')
|
||||
const inviter = Number(inviterRaw)
|
||||
const hasInviter = Number.isFinite(inviter) && inviter > 0
|
||||
|
||||
const user = Taro.getStorageSync('User') || {}
|
||||
const nickname = (user && (user.nickname || user.realName || user.username)) || ''
|
||||
const title = hasInviter ? `${nickname || '我'}邀请你加入桂乐淘伙伴计划` : '桂乐淘伙伴计划'
|
||||
|
||||
return {
|
||||
title,
|
||||
path: hasInviter
|
||||
? `/pages/index/index?inviter=${inviter}&source=dealer_qrcode&t=${Date.now()}`
|
||||
: `/pages/index/index`,
|
||||
success: function () {
|
||||
Taro.showToast({title: '分享成功', icon: 'success', duration: 2000})
|
||||
},
|
||||
fail: function () {
|
||||
Taro.showToast({title: '分享失败', icon: 'none', duration: 2000})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 生成小程序码
|
||||
const generateMiniProgramCode = async () => {
|
||||
@@ -22,7 +56,7 @@ const DealerQrcode: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setCodeLoading(true)
|
||||
|
||||
// 生成邀请小程序码
|
||||
const codeUrl = await generateInviteCode(dealerUser.userId)
|
||||
@@ -40,7 +74,7 @@ const DealerQrcode: React.FC = () => {
|
||||
// 清空之前的二维码
|
||||
setMiniProgramCodeUrl('')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setCodeLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +101,66 @@ const DealerQrcode: React.FC = () => {
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
const isAlbumAuthError = (errMsg?: string) => {
|
||||
if (!errMsg) return false
|
||||
// WeChat uses variants like: "saveImageToPhotosAlbum:fail auth deny",
|
||||
// "saveImageToPhotosAlbum:fail auth denied", "authorize:fail auth deny"
|
||||
return (
|
||||
errMsg.includes('auth deny') ||
|
||||
errMsg.includes('auth denied') ||
|
||||
errMsg.includes('authorize') ||
|
||||
errMsg.includes('scope.writePhotosAlbum')
|
||||
)
|
||||
}
|
||||
|
||||
const ensureWriteAlbumPermission = async (): Promise<boolean> => {
|
||||
try {
|
||||
const setting = await Taro.getSetting()
|
||||
if (setting?.authSetting?.['scope.writePhotosAlbum']) return true
|
||||
|
||||
await Taro.authorize({scope: 'scope.writePhotosAlbum'})
|
||||
return true
|
||||
} catch (error: any) {
|
||||
const modal = await Taro.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册,请在设置中开启相册权限',
|
||||
confirmText: '去设置'
|
||||
})
|
||||
if (modal.confirm) {
|
||||
await Taro.openSetting()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const downloadImageToLocalPath = async (url: string): Promise<string> => {
|
||||
// saveImageToPhotosAlbum must receive a local temp path (e.g. `http://tmp/...` or `wxfile://...`).
|
||||
// Some environments may return a non-existing temp path from getImageInfo, so we verify.
|
||||
if (url.startsWith('http://tmp/') || url.startsWith('wxfile://')) {
|
||||
return url
|
||||
}
|
||||
|
||||
const token = Taro.getStorageSync('access_token')
|
||||
const tenantId = Taro.getStorageSync('TenantId')
|
||||
const header: Record<string, string> = {}
|
||||
if (token) header.Authorization = token
|
||||
if (tenantId) header.TenantId = tenantId
|
||||
|
||||
// 先下载到本地临时文件再保存到相册
|
||||
const res = await Taro.downloadFile({url, header})
|
||||
if (res.statusCode !== 200 || !res.tempFilePath) {
|
||||
throw new Error(`图片下载失败(${res.statusCode || 'unknown'})`)
|
||||
}
|
||||
|
||||
// Double-check file exists to avoid: saveImageToPhotosAlbum:fail no such file or directory
|
||||
try {
|
||||
await Taro.getFileInfo({filePath: res.tempFilePath})
|
||||
} catch (_) {
|
||||
throw new Error('图片临时文件不存在,请重试')
|
||||
}
|
||||
return res.tempFilePath
|
||||
}
|
||||
|
||||
// 保存小程序码到相册
|
||||
const saveMiniProgramCode = async () => {
|
||||
if (!miniProgramCodeUrl) {
|
||||
@@ -78,39 +172,64 @@ const DealerQrcode: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 先下载图片到本地
|
||||
const res = await Taro.downloadFile({
|
||||
url: miniProgramCodeUrl
|
||||
})
|
||||
if (saving) return
|
||||
setSaving(true)
|
||||
Taro.showLoading({title: '保存中...'})
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
// 保存到相册
|
||||
await Taro.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath
|
||||
})
|
||||
const hasPermission = await ensureWriteAlbumPermission()
|
||||
if (!hasPermission) return
|
||||
|
||||
let filePath = await downloadImageToLocalPath(miniProgramCodeUrl)
|
||||
try {
|
||||
await Taro.saveImageToPhotosAlbum({filePath})
|
||||
} catch (e: any) {
|
||||
const msg = e?.errMsg || e?.message || ''
|
||||
// Fallback: some devices/clients may fail to save directly from a temp path.
|
||||
if (
|
||||
msg.includes('no such file or directory') &&
|
||||
(filePath.startsWith('http://tmp/') || filePath.startsWith('wxfile://'))
|
||||
) {
|
||||
const saved = (await Taro.saveFile({tempFilePath: filePath})) as unknown as { savedFilePath?: string }
|
||||
if (saved?.savedFilePath) {
|
||||
filePath = saved.savedFilePath
|
||||
}
|
||||
await Taro.saveImageToPhotosAlbum({filePath})
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errMsg?.includes('auth deny')) {
|
||||
Taro.showModal({
|
||||
const errMsg = error?.errMsg || error?.message
|
||||
if (errMsg?.includes('cancel')) {
|
||||
Taro.showToast({title: '已取消', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
if (isAlbumAuthError(errMsg)) {
|
||||
const modal = await Taro.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.openSetting()
|
||||
}
|
||||
}
|
||||
confirmText: '去设置'
|
||||
})
|
||||
if (modal.confirm) {
|
||||
await Taro.openSetting()
|
||||
}
|
||||
} else {
|
||||
Taro.showToast({
|
||||
// Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
|
||||
await Taro.showModal({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
content: errMsg || '保存失败,请稍后重试',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +245,7 @@ const DealerQrcode: React.FC = () => {
|
||||
//
|
||||
// const inviteText = `🎉 邀请您加入我的团队!
|
||||
//
|
||||
// 扫描小程序码或搜索"时里院子市集"小程序,即可享受优质商品和服务!
|
||||
// 扫描小程序码或搜索"桂乐淘"小程序,即可享受优质商品和服务!
|
||||
//
|
||||
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
||||
// 🎁 新用户专享优惠等你来拿
|
||||
@@ -162,7 +281,7 @@ const DealerQrcode: React.FC = () => {
|
||||
// })
|
||||
// }
|
||||
|
||||
if (!dealerUser) {
|
||||
if (dealerLoading) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading/>
|
||||
@@ -171,6 +290,33 @@ const DealerQrcode: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
|
||||
<Text className="text-gray-800 font-semibold">加载失败</Text>
|
||||
<Text className="text-gray-500 text-sm mt-2">{error}</Text>
|
||||
<Button className="mt-6" type="primary" onClick={refresh}>重试</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 未成为分销商时给出明确引导,避免一直停留在“加载中”
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
|
||||
<Text className="text-gray-800 font-semibold">你还不是分销商</Text>
|
||||
<Text className="text-gray-500 text-sm mt-2 text-center">申请成为分销商后即可生成分享码</Text>
|
||||
<Button
|
||||
className="mt-6"
|
||||
type="primary"
|
||||
onClick={() => Taro.navigateTo({url: '/dealer/apply/add'})}
|
||||
>
|
||||
去申请
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 头部卡片 */}
|
||||
@@ -185,9 +331,9 @@ const DealerQrcode: React.FC = () => {
|
||||
}}></View>
|
||||
|
||||
<View className="relative z-10 flex flex-col">
|
||||
<Text className="text-2xl font-bold mb-2 text-white">我的邀请小程序码</Text>
|
||||
<Text className="text-2xl font-bold mb-2 text-white">我的分享码</Text>
|
||||
<Text className="text-white text-opacity-80">
|
||||
分享小程序码邀请好友,获得丰厚佣金奖励
|
||||
与好友“共享福利 一起省、一起赚”
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -196,7 +342,7 @@ const DealerQrcode: React.FC = () => {
|
||||
{/* 小程序码展示区 */}
|
||||
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
||||
<View className="text-center">
|
||||
{loading ? (
|
||||
{codeLoading ? (
|
||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">生成中...</Text>
|
||||
@@ -239,10 +385,10 @@ const DealerQrcode: React.FC = () => {
|
||||
)}
|
||||
|
||||
<View className="text-lg font-semibold text-gray-800 mb-2">
|
||||
扫码加入我的团队
|
||||
桂乐淘伙伴计划
|
||||
</View>
|
||||
<View className="text-sm text-gray-500 mb-4">
|
||||
好友扫描小程序码即可直接进入小程序并建立邀请关系
|
||||
自购省 | 分享赚 | 好友惠
|
||||
</View>
|
||||
|
||||
|
||||
@@ -258,34 +404,12 @@ const DealerQrcode: React.FC = () => {
|
||||
block
|
||||
icon={<Download/>}
|
||||
onClick={saveMiniProgramCode}
|
||||
disabled={!miniProgramCodeUrl || loading}
|
||||
disabled={!miniProgramCodeUrl || codeLoading || saving}
|
||||
>
|
||||
保存小程序码到相册
|
||||
</Button>
|
||||
</View>
|
||||
{/*<View className={'my-2 bg-white'}>*/}
|
||||
{/* <Button*/}
|
||||
{/* size="large"*/}
|
||||
{/* block*/}
|
||||
{/* icon={<Copy/>}*/}
|
||||
{/* onClick={copyInviteInfo}*/}
|
||||
{/* disabled={!dealerUser?.userId || loading}*/}
|
||||
{/* >*/}
|
||||
{/* 复制邀请信息*/}
|
||||
{/* </Button>*/}
|
||||
{/*</View>*/}
|
||||
{/*<View className={'my-2 bg-white'}>*/}
|
||||
{/* <Button*/}
|
||||
{/* size="large"*/}
|
||||
{/* block*/}
|
||||
{/* fill="outline"*/}
|
||||
{/* icon={<Share/>}*/}
|
||||
{/* onClick={shareMiniProgramCode}*/}
|
||||
{/* disabled={!dealerUser?.userId || loading}*/}
|
||||
{/* >*/}
|
||||
{/* 分享给好友*/}
|
||||
{/* </Button>*/}
|
||||
{/*</View>*/}
|
||||
|
||||
</View>
|
||||
|
||||
{/* 推广说明 */}
|
||||
|
||||
@@ -325,7 +325,7 @@ const DealerTeam: React.FC = () => {
|
||||
</View>
|
||||
{/* 显示手机号(仅本级可见) */}
|
||||
{showPhone && member.phone && (
|
||||
<Text className="text-sm text-gray-500" onClick={(e) => {
|
||||
<Text className="text-sm text-gray-500 hidden" onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
makePhoneCall(member.phone || '');
|
||||
}}>
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react'
|
||||
import DealerWithdraw from '../index'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import * as withdrawAPI from '@/api/shop/shopDealerWithdraw'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/hooks/useDealerUser')
|
||||
jest.mock('@/api/shop/shopDealerWithdraw')
|
||||
jest.mock('@tarojs/taro', () => ({
|
||||
showToast: jest.fn(),
|
||||
getStorageSync: jest.fn(() => 123),
|
||||
}))
|
||||
|
||||
const mockUseDealerUser = useDealerUser as jest.MockedFunction<typeof useDealerUser>
|
||||
const mockAddShopDealerWithdraw = withdrawAPI.addShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.addShopDealerWithdraw>
|
||||
const mockPageShopDealerWithdraw = withdrawAPI.pageShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.pageShopDealerWithdraw>
|
||||
|
||||
describe('DealerWithdraw', () => {
|
||||
const mockDealerUser = {
|
||||
userId: 123,
|
||||
money: '10000.00',
|
||||
realName: '测试用户',
|
||||
mobile: '13800138000'
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseDealerUser.mockReturnValue({
|
||||
dealerUser: mockDealerUser,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: jest.fn()
|
||||
})
|
||||
|
||||
mockPageShopDealerWithdraw.mockResolvedValue({
|
||||
list: [],
|
||||
count: 0
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('应该正确显示可提现余额', () => {
|
||||
const { getByText } = render(<DealerWithdraw />)
|
||||
expect(getByText('10000.00')).toBeInTheDocument()
|
||||
expect(getByText('可提现余额')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('应该验证最低提现金额', async () => {
|
||||
mockAddShopDealerWithdraw.mockResolvedValue('success')
|
||||
|
||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||
|
||||
// 输入低于最低金额的数值
|
||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||
fireEvent.change(amountInput, { target: { value: '50' } })
|
||||
|
||||
// 选择提现方式
|
||||
const wechatRadio = getByText('微信钱包')
|
||||
fireEvent.click(wechatRadio)
|
||||
|
||||
// 提交表单
|
||||
const submitButton = getByText('申请提现')
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||
title: '最低提现金额为100元',
|
||||
icon: 'error'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('应该验证提现金额不超过可用余额', async () => {
|
||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||
|
||||
// 输入超过可用余额的金额
|
||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||
fireEvent.change(amountInput, { target: { value: '20000' } })
|
||||
|
||||
// 选择提现方式
|
||||
const wechatRadio = getByText('微信钱包')
|
||||
fireEvent.click(wechatRadio)
|
||||
|
||||
// 提交表单
|
||||
const submitButton = getByText('申请提现')
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||
title: '提现金额超过可用余额',
|
||||
icon: 'error'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('应该验证支付宝账户信息完整性', async () => {
|
||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||
|
||||
// 输入有效金额
|
||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||
fireEvent.change(amountInput, { target: { value: '1000' } })
|
||||
|
||||
// 选择支付宝提现
|
||||
const alipayRadio = getByText('支付宝')
|
||||
fireEvent.click(alipayRadio)
|
||||
|
||||
// 只填写账号,不填写姓名
|
||||
const accountInput = getByPlaceholderText('请输入支付宝账号')
|
||||
fireEvent.change(accountInput, { target: { value: 'test@alipay.com' } })
|
||||
|
||||
// 提交表单
|
||||
const submitButton = getByText('申请提现')
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||
title: '请填写完整的支付宝信息',
|
||||
icon: 'error'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('应该成功提交微信提现申请', async () => {
|
||||
mockAddShopDealerWithdraw.mockResolvedValue('success')
|
||||
|
||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||
|
||||
// 输入有效金额
|
||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||
fireEvent.change(amountInput, { target: { value: '1000' } })
|
||||
|
||||
// 选择微信提现
|
||||
const wechatRadio = getByText('微信钱包')
|
||||
fireEvent.click(wechatRadio)
|
||||
|
||||
// 提交表单
|
||||
const submitButton = getByText('申请提现')
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddShopDealerWithdraw).toHaveBeenCalledWith({
|
||||
userId: 123,
|
||||
money: '1000',
|
||||
payType: 10,
|
||||
applyStatus: 10,
|
||||
platform: 'MiniProgram'
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||
title: '提现申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('快捷金额按钮应该正常工作', () => {
|
||||
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
|
||||
|
||||
// 点击快捷金额按钮
|
||||
const quickAmountButton = getByText('500')
|
||||
fireEvent.click(quickAmountButton)
|
||||
|
||||
// 验证金额输入框的值
|
||||
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
|
||||
expect(amountInput.value).toBe('500')
|
||||
})
|
||||
|
||||
test('全部按钮应该设置为可用余额', () => {
|
||||
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
|
||||
|
||||
// 点击全部按钮
|
||||
const allButton = getByText('全部')
|
||||
fireEvent.click(allButton)
|
||||
|
||||
// 验证金额输入框的值
|
||||
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
|
||||
expect(amountInput.value).toBe('10000.00')
|
||||
})
|
||||
})
|
||||
@@ -1,13 +1,11 @@
|
||||
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {
|
||||
Cell,
|
||||
Space,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
CellGroup,
|
||||
Radio,
|
||||
Tabs,
|
||||
Tag,
|
||||
Empty,
|
||||
@@ -18,32 +16,109 @@ import {Wallet} from '@nutui/icons-react-taro'
|
||||
import {businessGradients} from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
|
||||
import {myUserVerify} from '@/api/system/userVerify'
|
||||
import {goTo} from '@/utils/navigation'
|
||||
import {
|
||||
pageShopDealerWithdraw,
|
||||
addShopDealerWithdraw,
|
||||
receiveShopDealerWithdraw,
|
||||
receiveSuccessShopDealerWithdraw
|
||||
} from '@/api/shop/shopDealerWithdraw'
|
||||
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
|
||||
|
||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||
accountDisplay?: string
|
||||
// Backend may include these fields for WeChat "confirm receipt" flow after approval.
|
||||
package_info?: string
|
||||
packageInfo?: string
|
||||
package?: string
|
||||
}
|
||||
|
||||
const extractPackageInfo = (result: unknown): string | null => {
|
||||
if (typeof result === 'string') return result
|
||||
if (!result || typeof result !== 'object') return null
|
||||
const r = result as any
|
||||
return (
|
||||
r.package_info ??
|
||||
r.packageInfo ??
|
||||
r.package ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
const canRequestMerchantTransferConfirm = (): boolean => {
|
||||
try {
|
||||
if (typeof (Taro as any).getEnv === 'function' && (Taro as any).ENV_TYPE) {
|
||||
const env = (Taro as any).getEnv()
|
||||
if (env !== (Taro as any).ENV_TYPE.WEAPP) return false
|
||||
}
|
||||
|
||||
const api =
|
||||
(globalThis as any).wx?.requestMerchantTransfer ||
|
||||
(Taro as any).requestMerchantTransfer
|
||||
|
||||
return typeof api === 'function'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const requestMerchantTransferConfirm = (packageInfo: string): Promise<any> => {
|
||||
if (!canRequestMerchantTransferConfirm()) {
|
||||
return Promise.reject(new Error('请在微信小程序内完成收款确认'))
|
||||
}
|
||||
|
||||
// Backend may wrap/format base64 with newlines; WeChat API requires a clean string.
|
||||
const cleanPackageInfo = String(packageInfo).replace(/\s+/g, '')
|
||||
|
||||
const api =
|
||||
(globalThis as any).wx?.requestMerchantTransfer ||
|
||||
(Taro as any).requestMerchantTransfer
|
||||
|
||||
if (typeof api !== 'function') {
|
||||
return Promise.reject(new Error('当前环境不支持商家转账收款确认(缺少 requestMerchantTransfer)'))
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
api({
|
||||
// WeChat API uses `package`, backend returns `package_info`.
|
||||
package: cleanPackageInfo,
|
||||
mchId: '1737910695',
|
||||
appId: 'wxad831ba00ad6a026',
|
||||
success: (res: any) => resolve(res),
|
||||
fail: (err: any) => reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Some backends may return money fields as number; keep internal usage always as string.
|
||||
const normalizeMoneyString = (money: unknown) => {
|
||||
if (money === null || money === undefined || money === '') return '0.00'
|
||||
return typeof money === 'string' ? money : String(money)
|
||||
}
|
||||
|
||||
const DealerWithdraw: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string | number>('0')
|
||||
const [selectedAccount, setSelectedAccount] = useState('')
|
||||
const [activeTab, setActiveTab] = useState<string>('0')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||
const [claimingId, setClaimingId] = useState<number | null>(null)
|
||||
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
|
||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
const {dealerUser} = useDealerUser()
|
||||
const [verifyStatus, setVerifyStatus] = useState<'unknown' | 'verified' | 'unverified' | 'pending' | 'rejected'>('unknown')
|
||||
const [verifyStatusText, setVerifyStatusText] = useState<string>('')
|
||||
|
||||
// Tab 切换处理函数
|
||||
const handleTabChange = (value: string | number) => {
|
||||
console.log('Tab切换到:', value)
|
||||
setActiveTab(value)
|
||||
const next = String(value)
|
||||
setActiveTab(next)
|
||||
|
||||
// 如果切换到提现记录页面,刷新数据
|
||||
if (String(value) === '1') {
|
||||
if (next === '1') {
|
||||
fetchWithdrawRecords()
|
||||
}
|
||||
}
|
||||
@@ -52,7 +127,7 @@ const DealerWithdraw: React.FC = () => {
|
||||
const fetchBalance = useCallback(async () => {
|
||||
console.log(dealerUser, 'dealerUser...')
|
||||
try {
|
||||
setAvailableAmount(dealerUser?.money || '0.00')
|
||||
setAvailableAmount(normalizeMoneyString(dealerUser?.money))
|
||||
} catch (error) {
|
||||
console.error('获取余额失败:', error)
|
||||
}
|
||||
@@ -115,12 +190,63 @@ const DealerWithdraw: React.FC = () => {
|
||||
}
|
||||
}, [fetchBalance, fetchWithdrawRecords])
|
||||
|
||||
// 判断实名认证状态:提现前必须完成实名认证(已通过)
|
||||
const fetchVerifyStatus = useCallback(async () => {
|
||||
// Fast path: some pages store this flag after login.
|
||||
if (String(Taro.getStorageSync('Certification')) === '1') {
|
||||
setVerifyStatus('verified')
|
||||
setVerifyStatusText('已实名认证')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await myUserVerify({})
|
||||
if (!r) {
|
||||
setVerifyStatus('unverified')
|
||||
setVerifyStatusText('未实名认证')
|
||||
return
|
||||
}
|
||||
|
||||
const s = Number((r as any).status)
|
||||
const st = String((r as any).statusText || '')
|
||||
|
||||
// Common convention in this project: 0审核中/待审核, 1已通过, 2已驳回
|
||||
if (s === 1) {
|
||||
setVerifyStatus('verified')
|
||||
setVerifyStatusText(st || '已实名认证')
|
||||
return
|
||||
}
|
||||
if (s === 0) {
|
||||
setVerifyStatus('pending')
|
||||
setVerifyStatusText(st || '审核中')
|
||||
return
|
||||
}
|
||||
if (s === 2) {
|
||||
setVerifyStatus('rejected')
|
||||
setVerifyStatusText(st || '已驳回')
|
||||
return
|
||||
}
|
||||
|
||||
setVerifyStatus('unverified')
|
||||
setVerifyStatusText(st || '未实名认证')
|
||||
} catch (e) {
|
||||
console.warn('获取实名认证状态失败,将按未认证处理:', e)
|
||||
setVerifyStatus('unverified')
|
||||
setVerifyStatusText('未实名认证')
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!dealerUser?.userId) return
|
||||
fetchVerifyStatus().then()
|
||||
}, [dealerUser?.userId, fetchVerifyStatus])
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 40:
|
||||
return '已到账'
|
||||
case 20:
|
||||
return '审核通过'
|
||||
return '待领取'
|
||||
case 10:
|
||||
return '待审核'
|
||||
case 30:
|
||||
@@ -135,7 +261,7 @@ const DealerWithdraw: React.FC = () => {
|
||||
case 40:
|
||||
return 'success'
|
||||
case 20:
|
||||
return 'success'
|
||||
return 'info'
|
||||
case 10:
|
||||
return 'warning'
|
||||
case 30:
|
||||
@@ -154,17 +280,17 @@ const DealerWithdraw: React.FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!values.accountType) {
|
||||
if (verifyStatus !== 'verified') {
|
||||
Taro.showToast({
|
||||
title: '请选择提现方式',
|
||||
icon: 'error'
|
||||
title: '请先完成实名认证',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证提现金额
|
||||
const amount = parseFloat(values.amount)
|
||||
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
||||
const amount = parseFloat(String(values.amount))
|
||||
const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, ''))
|
||||
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
Taro.showToast({
|
||||
@@ -175,72 +301,48 @@ const DealerWithdraw: React.FC = () => {
|
||||
}
|
||||
|
||||
if (amount < 100) {
|
||||
// Taro.showToast({
|
||||
// title: '最低提现金额为100元',
|
||||
// icon: 'error'
|
||||
// })
|
||||
// return
|
||||
}
|
||||
|
||||
if (amount > 200) {
|
||||
Taro.showToast({
|
||||
title: '最低提现金额为100元',
|
||||
icon: 'error'
|
||||
title: '单笔最多提现200元',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (amount > available) {
|
||||
Taro.showToast({
|
||||
title: '提现金额超过可用余额',
|
||||
icon: 'error'
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证账户信息
|
||||
if (values.accountType === 'alipay') {
|
||||
if (!values.account || !values.accountName) {
|
||||
Taro.showToast({
|
||||
title: '请填写完整的支付宝信息',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
} else if (values.accountType === 'bank') {
|
||||
if (!values.account || !values.accountName || !values.bankName) {
|
||||
Taro.showToast({
|
||||
title: '请填写完整的银行卡信息',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true)
|
||||
|
||||
const withdrawData: ShopDealerWithdraw = {
|
||||
userId: dealerUser.userId,
|
||||
money: values.amount,
|
||||
payType: values.accountType === 'wechat' ? 10 :
|
||||
values.accountType === 'alipay' ? 20 : 30,
|
||||
applyStatus: 10, // 待审核
|
||||
// Only support WeChat wallet withdrawals.
|
||||
payType: 10,
|
||||
platform: 'MiniProgram'
|
||||
}
|
||||
|
||||
// 根据提现方式设置账户信息
|
||||
if (values.accountType === 'alipay') {
|
||||
withdrawData.alipayAccount = values.account
|
||||
withdrawData.alipayName = values.accountName
|
||||
} else if (values.accountType === 'bank') {
|
||||
withdrawData.bankCard = values.account
|
||||
withdrawData.bankAccount = values.accountName
|
||||
withdrawData.bankName = values.bankName || '银行卡'
|
||||
}
|
||||
|
||||
// Security flow:
|
||||
// 1) user submits => applyStatus=10 (待审核)
|
||||
// 2) backend审核通过 => applyStatus=20 (待领取)
|
||||
// 3) user goes to records to "领取" => applyStatus=40 (已到账)
|
||||
await addShopDealerWithdraw(withdrawData)
|
||||
|
||||
Taro.showToast({
|
||||
title: '提现申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
Taro.showToast({title: '提现申请已提交,等待审核', icon: 'success'})
|
||||
|
||||
// 重置表单
|
||||
formRef.current?.resetFields()
|
||||
setSelectedAccount('')
|
||||
|
||||
// 刷新数据
|
||||
await handleRefresh()
|
||||
@@ -259,24 +361,103 @@ const DealerWithdraw: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const quickAmounts = ['100', '300', '500', '1000']
|
||||
const handleClaim = async (record: WithdrawRecordWithDetails) => {
|
||||
if (!record?.id) {
|
||||
Taro.showToast({title: '记录不存在', icon: 'error'})
|
||||
return
|
||||
}
|
||||
|
||||
if (record.applyStatus !== 20) {
|
||||
Taro.showToast({title: '当前状态不可领取', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
if (record.payType !== 10) {
|
||||
Taro.showToast({title: '仅支持微信提现领取', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
if (claimingId !== null) return
|
||||
|
||||
try {
|
||||
setClaimingId(record.id)
|
||||
|
||||
if (!canRequestMerchantTransferConfirm()) {
|
||||
throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作')
|
||||
}
|
||||
|
||||
const receiveResult = await receiveShopDealerWithdraw(record.id)
|
||||
const packageInfo = extractPackageInfo(receiveResult)
|
||||
if (!packageInfo) {
|
||||
throw new Error('后台未返回 package_info,无法领取,请联系管理员')
|
||||
}
|
||||
|
||||
try {
|
||||
await requestMerchantTransferConfirm(packageInfo)
|
||||
} catch (e: any) {
|
||||
const msg = String(e?.errMsg || e?.message || '')
|
||||
if (/cancel/i.test(msg)) {
|
||||
Taro.showToast({title: '已取消领取', icon: 'none'})
|
||||
return
|
||||
}
|
||||
throw new Error(msg || '领取失败,请稍后重试')
|
||||
}
|
||||
|
||||
try {
|
||||
await receiveSuccessShopDealerWithdraw(record.id)
|
||||
Taro.showToast({title: '领取成功', icon: 'success'})
|
||||
} catch (e: any) {
|
||||
console.warn('领取成功,但状态同步失败:', e)
|
||||
Taro.showToast({title: '已收款,状态更新失败,请稍后刷新', icon: 'none'})
|
||||
} finally {
|
||||
await handleRefresh()
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('领取失败:', e)
|
||||
Taro.showToast({title: e?.message || '领取失败', icon: 'error'})
|
||||
} finally {
|
||||
setClaimingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
const quickAmounts = ['50', '100', '200']
|
||||
|
||||
const setQuickAmount = (amount: string) => {
|
||||
formRef.current?.setFieldsValue({amount})
|
||||
}
|
||||
|
||||
const setAllAmount = () => {
|
||||
formRef.current?.setFieldsValue({amount: availableAmount.replace(/,/g, '')})
|
||||
formRef.current?.setFieldsValue({amount: normalizeMoneyString(availableAmount).replace(/,/g, '')})
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (money?: string) => {
|
||||
if (!money) return '0.00'
|
||||
return parseFloat(money).toFixed(2)
|
||||
const formatMoney = (money?: unknown) => {
|
||||
const n = parseFloat(normalizeMoneyString(money).replace(/,/g, ''))
|
||||
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||||
}
|
||||
|
||||
const goVerify = () => {
|
||||
goTo('/user/userVerify/index')
|
||||
}
|
||||
|
||||
const renderWithdrawForm = () => (
|
||||
<View>
|
||||
{(verifyStatus === 'unverified' || verifyStatus === 'pending' || verifyStatus === 'rejected') && (
|
||||
<View className="rounded-lg bg-white px-4 py-3 mb-4 mx-4">
|
||||
<View className="flex items-center justify-between">
|
||||
<View className="flex flex-col">
|
||||
<Text className="text-sm text-red-500">提现前请先完成实名认证</Text>
|
||||
{verifyStatusText ? (
|
||||
<Text className="text-xs text-gray-500 mt-1">当前状态:{verifyStatusText}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<Text className="text-sm text-blue-600" onClick={goVerify}>
|
||||
去认证
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 余额卡片 */}
|
||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
@@ -303,7 +484,7 @@ const DealerWithdraw: React.FC = () => {
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
|
||||
}}>
|
||||
<Text className="text-white text-opacity-80 text-xs">
|
||||
最低提现金额:¥100 | 手续费:免费
|
||||
手续费:免费
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -314,18 +495,10 @@ const DealerWithdraw: React.FC = () => {
|
||||
labelPosition="top"
|
||||
>
|
||||
<CellGroup>
|
||||
<Form.Item name="amount" label="提现金额" required>
|
||||
<Form.Item name="amount" label="提现金额">
|
||||
<Input
|
||||
placeholder="请输入提现金额"
|
||||
type="number"
|
||||
onChange={(value) => {
|
||||
// 实时验证提现金额
|
||||
const amount = parseFloat(value)
|
||||
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
||||
if (!isNaN(amount) && amount > available) {
|
||||
// 可以在这里添加实时提示,但不阻止输入
|
||||
}
|
||||
}}
|
||||
type="digit"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -353,54 +526,15 @@ const DealerWithdraw: React.FC = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Form.Item name="accountType" label="提现方式" required>
|
||||
<Radio.Group value={selectedAccount} onChange={() => setSelectedAccount}>
|
||||
<Cell.Group>
|
||||
<Cell>
|
||||
<Radio value="wechat">微信钱包</Radio>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Radio value="alipay">支付宝</Radio>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Radio value="bank">银行卡</Radio>
|
||||
</Cell>
|
||||
</Cell.Group>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{selectedAccount === 'alipay' && (
|
||||
<>
|
||||
<Form.Item name="account" label="支付宝账号" required>
|
||||
<Input placeholder="请输入支付宝账号"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="支付宝姓名" required>
|
||||
<Input placeholder="请输入支付宝实名姓名"/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedAccount === 'bank' && (
|
||||
<>
|
||||
<Form.Item name="bankName" label="开户银行" required>
|
||||
<Input placeholder="请输入开户银行名称"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="account" label="银行卡号" required>
|
||||
<Input placeholder="请输入银行卡号"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="开户姓名" required>
|
||||
<Input placeholder="请输入银行卡开户姓名"/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedAccount === 'wechat' && (
|
||||
<View className="px-4 py-2">
|
||||
<Text className="text-sm text-gray-500">
|
||||
微信钱包提现将直接转入您的微信零钱
|
||||
<Text className="text-sm text-red-500">
|
||||
注意事项:
|
||||
1. 提取佣金必须完成实名认证。
|
||||
2. 佣金非自动到账,在您提取佣金申请通过后,请手动到我的申请记录点击领取。
|
||||
3. 桂乐淘温馨提示,请您依法依规申报所得,缴税相关税费。
|
||||
4. 单笔提现最高金额为200。
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</CellGroup>
|
||||
|
||||
<View className="mt-6 px-4">
|
||||
@@ -409,7 +543,7 @@ const DealerWithdraw: React.FC = () => {
|
||||
type="primary"
|
||||
nativeType="submit"
|
||||
loading={submitting}
|
||||
disabled={submitting || !selectedAccount}
|
||||
disabled={submitting || verifyStatus !== 'verified'}
|
||||
>
|
||||
{submitting ? '提交中...' : '申请提现'}
|
||||
</Button>
|
||||
@@ -440,20 +574,36 @@ const DealerWithdraw: React.FC = () => {
|
||||
<Text className="font-semibold text-gray-800 mb-1">
|
||||
提现金额:¥{record.money}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
提现账户:{record.accountDisplay}
|
||||
</Text>
|
||||
{/*<Text className="text-sm text-gray-500">*/}
|
||||
{/* 提现账户:{record.accountDisplay}*/}
|
||||
{/*</Text>*/}
|
||||
</Space>
|
||||
<Tag type={getStatusColor(record.applyStatus)}>
|
||||
<Tag background="#999999" type={getStatusColor(record.applyStatus)} plain>
|
||||
{getStatusText(record.applyStatus)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
|
||||
{record.applyStatus === 20 && record.payType === 10 && (
|
||||
<View className="flex mb-5 justify-center">
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
loading={claimingId === record.id}
|
||||
disabled={claimingId !== null}
|
||||
onClick={() => handleClaim(record)}
|
||||
>
|
||||
立即领取
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<View className="text-xs text-gray-400">
|
||||
<Text>申请时间:{record.createTime}</Text>
|
||||
<Text>创建时间:{record.createTime}</Text>
|
||||
{record.auditTime && (
|
||||
<Text className="block mt-1">
|
||||
审核时间:{new Date(record.auditTime).toLocaleString()}
|
||||
审核时间:{record.auditTime}
|
||||
</Text>
|
||||
)}
|
||||
{record.rejectReason && (
|
||||
@@ -463,6 +613,8 @@ const DealerWithdraw: React.FC = () => {
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Empty description="暂无提现记录"/>
|
||||
@@ -485,13 +637,12 @@ const DealerWithdraw: React.FC = () => {
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<Tabs.TabPane title="申请提现" value="0">
|
||||
{renderWithdrawForm()}
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="提现记录" value="1">
|
||||
{renderWithdrawRecords()}
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
{activeTab === '0' ? renderWithdrawForm() : renderWithdrawRecords()}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useState, useEffect, useCallback} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import {getShopDealerUser} from '@/api/shop/shopDealerUser'
|
||||
import type {ShopDealerUser} from '@/api/shop/shopDealerUser/model'
|
||||
|
||||
@@ -22,17 +22,20 @@ export interface UseDealerUserReturn {
|
||||
*/
|
||||
export const useDealerUser = (): UseDealerUserReturn => {
|
||||
const [dealerUser, setDealerUser] = useState<ShopDealerUser | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const rawUserId = Taro.getStorageSync('UserId')
|
||||
const userId = Number(rawUserId)
|
||||
const hasUser = Number.isFinite(userId) && userId > 0
|
||||
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
// If user is logged in, start in loading state to avoid "click too fast" mis-routing.
|
||||
const [loading, setLoading] = useState<boolean>(hasUser)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// 获取经销商用户数据
|
||||
const fetchDealerData = useCallback(async () => {
|
||||
|
||||
if (!userId) {
|
||||
console.log('🔍 用户未登录,提前返回')
|
||||
if (!hasUser) {
|
||||
setDealerUser(null)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -55,7 +58,7 @@ export const useDealerUser = (): UseDealerUserReturn => {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [userId])
|
||||
}, [hasUser, userId])
|
||||
|
||||
// 刷新数据
|
||||
const refresh = useCallback(async () => {
|
||||
@@ -64,13 +67,31 @@ export const useDealerUser = (): UseDealerUserReturn => {
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
console.log('🔍 调用 fetchDealerData')
|
||||
if (hasUser) {
|
||||
fetchDealerData()
|
||||
} else {
|
||||
console.log('🔍 用户ID不存在,不调用 fetchDealerData')
|
||||
setDealerUser(null)
|
||||
setError(null)
|
||||
setLoading(false)
|
||||
}
|
||||
}, [fetchDealerData, userId])
|
||||
}, [fetchDealerData, hasUser])
|
||||
|
||||
// 页面返回/切换到前台时刷新一次,避免“注册成为经销商后,页面不更新”
|
||||
useDidShow(() => {
|
||||
fetchDealerData()
|
||||
})
|
||||
|
||||
// 允许业务侧通过事件主动触发刷新(例如:注册成功后触发)
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
fetchDealerData()
|
||||
}
|
||||
// 事件名尽量语义化;后续可在注册成功处 trigger
|
||||
Taro.eventCenter.on('dealerUser:changed', handler)
|
||||
return () => {
|
||||
Taro.eventCenter.off('dealerUser:changed', handler)
|
||||
}
|
||||
}, [fetchDealerData])
|
||||
|
||||
return {
|
||||
dealerUser,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { UserOrderStats } from '@/api/user';
|
||||
import { getUserOrderStats, UserOrderStats } from '@/api/user';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {pageShopOrder} from "@/api/shop/shopOrder";
|
||||
|
||||
/**
|
||||
* 订单统计Hook
|
||||
@@ -31,20 +30,17 @@ export const useOrderStats = () => {
|
||||
if(!Taro.getStorageSync('UserId')){
|
||||
return false;
|
||||
}
|
||||
// TODO 读取订单数量
|
||||
const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0})
|
||||
const paid = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 1})
|
||||
const shipped = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 3})
|
||||
const completed = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 5})
|
||||
const refund = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 6})
|
||||
const total = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId')})
|
||||
|
||||
// 聚合接口:一次请求返回各状态数量(后台按用户做了缓存)
|
||||
const stats = await getUserOrderStats();
|
||||
|
||||
setOrderStats({
|
||||
pending: pending?.count || 0,
|
||||
paid: paid?.count || 0,
|
||||
shipped: shipped?.count || 0,
|
||||
completed: completed?.count || 0,
|
||||
refund: refund?.count || 0,
|
||||
total: total?.count || 0
|
||||
pending: stats?.pending || 0,
|
||||
paid: stats?.paid || 0,
|
||||
shipped: stats?.shipped || 0,
|
||||
completed: stats?.completed || 0,
|
||||
refund: stats?.refund || 0,
|
||||
total: stats?.total || 0
|
||||
})
|
||||
|
||||
if (showToast) {
|
||||
|
||||
@@ -13,19 +13,30 @@ export interface CountdownTime {
|
||||
totalMinutes: number; // 总剩余分钟数
|
||||
}
|
||||
|
||||
export interface UsePaymentCountdownParams {
|
||||
/** 订单创建时间(用于兼容:当 expirationTime 缺失时按 createTime + timeoutHours 计算) */
|
||||
createTime?: string;
|
||||
/** 订单过期时间(推荐直接传后端返回的 expirationTime) */
|
||||
expirationTime?: string;
|
||||
/** 支付状态 */
|
||||
payStatus?: boolean;
|
||||
/** 是否实时更新(详情页用true,列表页用false) */
|
||||
realTime?: boolean;
|
||||
/** 超时小时数,默认24小时(仅在 expirationTime 缺失时生效) */
|
||||
timeoutHours?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付倒计时Hook
|
||||
* @param createTime 订单创建时间
|
||||
* @param payStatus 支付状态
|
||||
* @param realTime 是否实时更新(详情页用true,列表页用false)
|
||||
* @param timeoutHours 超时小时数,默认24小时
|
||||
* 优先使用 expirationTime;当 expirationTime 缺失时回退到 createTime + timeoutHours。
|
||||
*/
|
||||
export const usePaymentCountdown = (
|
||||
createTime?: string,
|
||||
payStatus?: boolean,
|
||||
realTime: boolean = false,
|
||||
timeoutHours: number = 24
|
||||
): CountdownTime => {
|
||||
export const usePaymentCountdown = ({
|
||||
createTime,
|
||||
expirationTime,
|
||||
payStatus,
|
||||
realTime = false,
|
||||
timeoutHours = 24
|
||||
}: UsePaymentCountdownParams): CountdownTime => {
|
||||
const [timeLeft, setTimeLeft] = useState<CountdownTime>({
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
@@ -37,7 +48,7 @@ export const usePaymentCountdown = (
|
||||
// 计算剩余时间的函数
|
||||
const calculateTimeLeft = useMemo(() => {
|
||||
return (): CountdownTime => {
|
||||
if (!createTime || payStatus) {
|
||||
if (payStatus || (!expirationTime && !createTime)) {
|
||||
return {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
@@ -47,8 +58,27 @@ export const usePaymentCountdown = (
|
||||
};
|
||||
}
|
||||
|
||||
const createTimeObj = dayjs(createTime);
|
||||
const expireTime = createTimeObj.add(timeoutHours, 'hour');
|
||||
// 优先使用后端过期时间;如果无法解析,再回退到 createTime + timeoutHours
|
||||
const expireTimeFromExpiration = expirationTime ? dayjs(expirationTime) : null;
|
||||
const expireTimeFromCreate =
|
||||
createTime ? dayjs(createTime).add(timeoutHours, 'hour') : null;
|
||||
const expireTime =
|
||||
expireTimeFromExpiration?.isValid()
|
||||
? expireTimeFromExpiration
|
||||
: expireTimeFromCreate?.isValid()
|
||||
? expireTimeFromCreate
|
||||
: null;
|
||||
|
||||
if (!expireTime) {
|
||||
return {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: true,
|
||||
totalMinutes: 0
|
||||
};
|
||||
}
|
||||
|
||||
const now = dayjs();
|
||||
const diff = expireTime.diff(now);
|
||||
|
||||
@@ -76,10 +106,10 @@ export const usePaymentCountdown = (
|
||||
totalMinutes
|
||||
};
|
||||
};
|
||||
}, [createTime, payStatus, timeoutHours]);
|
||||
}, [createTime, expirationTime, payStatus, timeoutHours]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!createTime || payStatus) {
|
||||
if (payStatus || (!expirationTime && !createTime)) {
|
||||
setTimeLeft({
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
@@ -111,7 +141,7 @@ export const usePaymentCountdown = (
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [createTime, payStatus, realTime, calculateTimeLeft]);
|
||||
}, [createTime, expirationTime, payStatus, realTime, calculateTimeLeft]);
|
||||
|
||||
return timeLeft;
|
||||
};
|
||||
|
||||
277
src/hooks/useRiderNotification.ts
Normal file
277
src/hooks/useRiderNotification.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* 配送员订单通知 Hook
|
||||
* 功能:
|
||||
* 1. 轮询获取待配送订单数量
|
||||
* 2. 新订单时播放提示音
|
||||
* 3. 支持声音开关设置
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { pageGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||
|
||||
// 轮询间隔(毫秒)- 30秒
|
||||
const POLL_INTERVAL = 30000
|
||||
|
||||
// 提示音 URL(使用微信官方提示音)
|
||||
const NOTIFICATION_SOUND_URL = 'https://res.wx.qq.com/wechatApp/pkg/notice/notice.mp3'
|
||||
|
||||
interface RiderNotificationState {
|
||||
/** 待配送订单数量 */
|
||||
pendingCount: number
|
||||
/** 所有配送中订单数量 */
|
||||
totalCount: number
|
||||
/** 是否正在轮询 */
|
||||
isPolling: boolean
|
||||
/** 最后更新的订单ID */
|
||||
lastOrderId: number | null
|
||||
/** 声音是否开启 */
|
||||
soundEnabled: boolean
|
||||
}
|
||||
|
||||
interface UseRiderNotificationReturn extends RiderNotificationState {
|
||||
/** 开始轮询 */
|
||||
startPolling: () => void
|
||||
/** 停止轮询 */
|
||||
stopPolling: () => void
|
||||
/** 刷新数据 */
|
||||
refresh: () => Promise<void>
|
||||
/** 切换声音开关 */
|
||||
toggleSound: () => void
|
||||
/** 手动播放提示音 */
|
||||
playSound: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 配送员通知 Hook
|
||||
* @param autoStart 是否自动开始轮询,默认 true
|
||||
*/
|
||||
export const useRiderNotification = (autoStart = true): UseRiderNotificationReturn => {
|
||||
const [state, setState] = useState<RiderNotificationState>({
|
||||
pendingCount: 0,
|
||||
totalCount: 0,
|
||||
isPolling: false,
|
||||
lastOrderId: null,
|
||||
soundEnabled: true
|
||||
})
|
||||
|
||||
const pollTimerRef = useRef<number | null>(null)
|
||||
const audioContextRef = useRef<any>(null)
|
||||
const lastCountRef = useRef(0)
|
||||
const isMountedRef = useRef(true)
|
||||
|
||||
// 获取配送员ID
|
||||
const getRiderId = useCallback(() => {
|
||||
const riderId = Taro.getStorageSync('UserId')
|
||||
return Number(riderId) || undefined
|
||||
}, [])
|
||||
|
||||
// 从本地存储加载声音设置
|
||||
const loadSoundSetting = useCallback(() => {
|
||||
try {
|
||||
const enabled = Taro.getStorageSync('rider_sound_enabled')
|
||||
return enabled !== '0' // 默认开启
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 保存声音设置到本地存储
|
||||
const saveSoundSetting = useCallback((enabled: boolean) => {
|
||||
try {
|
||||
Taro.setStorageSync('rider_sound_enabled', enabled ? '1' : '0')
|
||||
} catch (e) {
|
||||
console.error('保存声音设置失败', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 播放提示音
|
||||
const playSound = useCallback(() => {
|
||||
if (!state.soundEnabled) return
|
||||
|
||||
try {
|
||||
// 停止之前的音频
|
||||
if (audioContextRef.current) {
|
||||
audioContextRef.current.stop()
|
||||
audioContextRef.current.destroy()
|
||||
}
|
||||
|
||||
// 创建新的音频实例
|
||||
const audioContext = Taro.createInnerAudioContext()
|
||||
audioContextRef.current = audioContext
|
||||
|
||||
audioContext.src = NOTIFICATION_SOUND_URL
|
||||
audioContext.volume = 0.8
|
||||
|
||||
audioContext.play()
|
||||
audioContext.onPlay(() => {
|
||||
console.log('📢 新订单提示音播放中')
|
||||
})
|
||||
|
||||
audioContext.onError((err: any) => {
|
||||
console.error('提示音播放失败', err)
|
||||
// 播放失败时尝试使用本地提示音
|
||||
tryFallbackSound()
|
||||
})
|
||||
|
||||
audioContext.onEnded(() => {
|
||||
audioContext.destroy()
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('播放提示音异常', e)
|
||||
}
|
||||
}, [state.soundEnabled])
|
||||
|
||||
// 备用提示音方案(使用微信震动)
|
||||
const tryFallbackSound = useCallback(() => {
|
||||
try {
|
||||
// 震动提示
|
||||
Taro.vibrateShort({ type: 'heavy' })
|
||||
} catch (e) {
|
||||
console.error('震动提示失败', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 获取未读订单数量
|
||||
const fetchUnreadCount = useCallback(async () => {
|
||||
const riderId = getRiderId()
|
||||
if (!riderId) return
|
||||
|
||||
try {
|
||||
const res = await pageGltTicketOrder({
|
||||
riderId,
|
||||
deliveryStatus: 10, // 待配送状态
|
||||
page: 1,
|
||||
limit: 1
|
||||
} as any)
|
||||
|
||||
const pendingCount = res?.count || 0
|
||||
const currentLastId = res?.list?.[0]?.id
|
||||
|
||||
// 如果数量增加,说明有新订单,播放提示音
|
||||
if (pendingCount > lastCountRef.current && lastCountRef.current > 0) {
|
||||
playSound()
|
||||
}
|
||||
|
||||
lastCountRef.current = pendingCount
|
||||
|
||||
if (isMountedRef.current) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
pendingCount,
|
||||
lastOrderId: currentLastId || prev.lastOrderId
|
||||
}))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取未读订单失败', e)
|
||||
}
|
||||
}, [getRiderId, playSound])
|
||||
|
||||
// 开始轮询
|
||||
const startPolling = useCallback(() => {
|
||||
if (pollTimerRef.current) return
|
||||
|
||||
// 立即执行一次
|
||||
fetchUnreadCount()
|
||||
|
||||
// 设置定时器
|
||||
pollTimerRef.current = setInterval(() => {
|
||||
fetchUnreadCount()
|
||||
}, POLL_INTERVAL)
|
||||
|
||||
setState(prev => ({ ...prev, isPolling: true }))
|
||||
}, [fetchUnreadCount])
|
||||
|
||||
// 停止轮询
|
||||
const stopPolling = useCallback(() => {
|
||||
if (pollTimerRef.current) {
|
||||
clearInterval(pollTimerRef.current)
|
||||
pollTimerRef.current = null
|
||||
}
|
||||
|
||||
// 停止音频
|
||||
if (audioContextRef.current) {
|
||||
audioContextRef.current.stop()
|
||||
audioContextRef.current.destroy()
|
||||
audioContextRef.current = null
|
||||
}
|
||||
|
||||
setState(prev => ({ ...prev, isPolling: false }))
|
||||
}, [])
|
||||
|
||||
// 刷新数据
|
||||
const refresh = useCallback(async () => {
|
||||
lastCountRef.current = 0 // 重置计数,强制刷新
|
||||
await fetchUnreadCount()
|
||||
}, [fetchUnreadCount])
|
||||
|
||||
// 切换声音开关
|
||||
const toggleSound = useCallback(() => {
|
||||
setState(prev => {
|
||||
const newEnabled = !prev.soundEnabled
|
||||
saveSoundSetting(newEnabled)
|
||||
return { ...prev, soundEnabled: newEnabled }
|
||||
})
|
||||
}, [saveSoundSetting])
|
||||
|
||||
// 初始化
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
|
||||
// 加载声音设置
|
||||
const soundEnabled = loadSoundSetting()
|
||||
setState(prev => ({ ...prev, soundEnabled }))
|
||||
|
||||
// 自动开始轮询
|
||||
if (autoStart) {
|
||||
startPolling()
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
stopPolling()
|
||||
}
|
||||
}, [autoStart, loadSoundSetting, startPolling, stopPolling])
|
||||
|
||||
// 监听页面显示/隐藏
|
||||
useEffect(() => {
|
||||
const onShow = () => {
|
||||
if (!state.isPolling) {
|
||||
startPolling()
|
||||
}
|
||||
}
|
||||
|
||||
const onHide = () => {
|
||||
// 页面隐藏时停止轮询节省电量
|
||||
// stopPolling()
|
||||
}
|
||||
|
||||
// 微信小程序监听
|
||||
if (typeof Taro.onAppShow === 'function') {
|
||||
Taro.onAppShow(onShow)
|
||||
}
|
||||
if (typeof Taro.onAppHide === 'function') {
|
||||
Taro.onAppHide(onHide)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (typeof Taro.offAppShow === 'function') {
|
||||
Taro.offAppShow(onShow)
|
||||
}
|
||||
if (typeof Taro.offAppHide === 'function') {
|
||||
Taro.offAppHide(onHide)
|
||||
}
|
||||
}
|
||||
}, [state.isPolling, startPolling])
|
||||
|
||||
return {
|
||||
...state,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
refresh,
|
||||
toggleSound,
|
||||
playSound
|
||||
}
|
||||
}
|
||||
|
||||
export default useRiderNotification
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { gradientThemes, GradientTheme, gradientUtils } from '@/styles/gradients'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { gradientThemes, type GradientTheme, gradientUtils } from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface UseThemeReturn {
|
||||
@@ -14,28 +14,42 @@ export interface UseThemeReturn {
|
||||
* 提供主题切换和状态管理功能
|
||||
*/
|
||||
export const useTheme = (): UseThemeReturn => {
|
||||
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(gradientThemes[0])
|
||||
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(true)
|
||||
|
||||
// 获取当前主题
|
||||
const getCurrentTheme = (): GradientTheme => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
|
||||
if (savedTheme === 'auto') {
|
||||
// 自动主题:根据用户ID生成
|
||||
const userId = Taro.getStorageSync('userId') || '1'
|
||||
return gradientUtils.getThemeByUserId(userId)
|
||||
} else {
|
||||
// 手动选择的主题
|
||||
return gradientThemes.find(t => t.name === savedTheme) || gradientThemes[0]
|
||||
const getSavedThemeName = useCallback((): string => {
|
||||
try {
|
||||
return Taro.getStorageSync('user_theme') || 'nature'
|
||||
} catch {
|
||||
return 'nature'
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getStoredUserId = useCallback((): number => {
|
||||
try {
|
||||
const raw = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId')
|
||||
const asNumber = typeof raw === 'number' ? raw : parseInt(String(raw || '1'), 10)
|
||||
return Number.isFinite(asNumber) ? asNumber : 1
|
||||
} catch {
|
||||
return 1
|
||||
}
|
||||
}, [])
|
||||
|
||||
const resolveTheme = useCallback(
|
||||
(themeName: string): GradientTheme => {
|
||||
if (themeName === 'auto') {
|
||||
return gradientUtils.getThemeByUserId(getStoredUserId())
|
||||
}
|
||||
return gradientThemes.find(t => t.name === themeName) || gradientUtils.getThemeByName('nature') || gradientThemes[0]
|
||||
},
|
||||
[getStoredUserId]
|
||||
)
|
||||
|
||||
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(() => getSavedThemeName() === 'auto')
|
||||
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(() => resolveTheme(getSavedThemeName()))
|
||||
|
||||
// 初始化主题
|
||||
useEffect(() => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
const savedTheme = getSavedThemeName()
|
||||
setIsAutoTheme(savedTheme === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
setCurrentTheme(resolveTheme(savedTheme))
|
||||
}, [])
|
||||
|
||||
// 设置主题
|
||||
@@ -43,7 +57,7 @@ export const useTheme = (): UseThemeReturn => {
|
||||
try {
|
||||
Taro.setStorageSync('user_theme', themeName)
|
||||
setIsAutoTheme(themeName === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
setCurrentTheme(resolveTheme(themeName))
|
||||
} catch (error) {
|
||||
console.error('保存主题失败:', error)
|
||||
}
|
||||
@@ -51,7 +65,7 @@ export const useTheme = (): UseThemeReturn => {
|
||||
|
||||
// 刷新主题(用于自动主题模式下用户信息变更时)
|
||||
const refreshTheme = () => {
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
setCurrentTheme(resolveTheme(getSavedThemeName()))
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
parseQRContent
|
||||
} from '@/api/passport/qr-login';
|
||||
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
||||
import { getGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket';
|
||||
import { useUser } from "@/hooks/useUser";
|
||||
import { isValidJSON } from "@/utils/jsonUtils";
|
||||
import dayjs from 'dayjs';
|
||||
@@ -29,6 +30,15 @@ export enum ScanType {
|
||||
UNKNOWN = 'unknown' // 未知类型
|
||||
}
|
||||
|
||||
type VerificationBusinessType = 'gift' | 'ticket';
|
||||
|
||||
interface TicketVerificationPayload {
|
||||
userTicketId: number;
|
||||
qty?: number;
|
||||
userId?: number;
|
||||
t?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码结果
|
||||
*/
|
||||
@@ -73,7 +83,11 @@ export function useUnifiedQRScan() {
|
||||
// 1. 检查是否为JSON格式(核销二维码)
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
// Allow plaintext (non-encrypted) ticket verification payload for debugging/internal use.
|
||||
if (json.userTicketId) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
}
|
||||
@@ -130,35 +144,79 @@ export function useUnifiedQRScan() {
|
||||
throw new Error('您没有核销权限');
|
||||
}
|
||||
|
||||
let code = '';
|
||||
let businessType: VerificationBusinessType = 'gift';
|
||||
let decryptedOrRaw = '';
|
||||
|
||||
// 判断是否为加密的JSON格式
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
// 解密获取核销码
|
||||
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
|
||||
businessType = json.businessType;
|
||||
// 解密获取核销内容
|
||||
const decryptedData = await decryptQrData({
|
||||
token: json.token,
|
||||
encryptedData: json.data
|
||||
});
|
||||
|
||||
if (decryptedData) {
|
||||
code = decryptedData.toString();
|
||||
decryptedOrRaw = decryptedData.toString();
|
||||
} else {
|
||||
throw new Error('解密失败');
|
||||
}
|
||||
} else if (json.userTicketId) {
|
||||
businessType = 'ticket';
|
||||
decryptedOrRaw = scanResult.trim();
|
||||
}
|
||||
} else {
|
||||
// 直接使用扫码结果作为核销码
|
||||
code = scanResult.trim();
|
||||
// 直接使用扫码结果作为核销内容
|
||||
decryptedOrRaw = scanResult.trim();
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
if (!decryptedOrRaw) {
|
||||
throw new Error('无法获取有效的核销码');
|
||||
}
|
||||
|
||||
// 验证核销码
|
||||
const gift = await getShopGiftByCode(code);
|
||||
if (businessType === 'ticket') {
|
||||
if (!isValidJSON(decryptedOrRaw)) {
|
||||
throw new Error('水票核销信息格式错误');
|
||||
}
|
||||
const payload = JSON.parse(decryptedOrRaw) as TicketVerificationPayload;
|
||||
const userTicketId = Number(payload.userTicketId);
|
||||
const qty = Math.max(1, Number(payload.qty || 1));
|
||||
if (!Number.isFinite(userTicketId) || userTicketId <= 0) {
|
||||
throw new Error('水票核销信息无效');
|
||||
}
|
||||
|
||||
const ticket = await getGltUserTicket(userTicketId);
|
||||
if (!ticket) throw new Error('水票不存在');
|
||||
if (ticket.status === 1) throw new Error('该水票已冻结');
|
||||
const available = Number(ticket.availableQty || 0);
|
||||
const used = Number(ticket.usedQty || 0);
|
||||
if (available < qty) throw new Error('水票可用次数不足');
|
||||
|
||||
await updateGltUserTicket({
|
||||
...ticket,
|
||||
availableQty: available - qty,
|
||||
usedQty: used + qty
|
||||
});
|
||||
|
||||
return {
|
||||
type: ScanType.VERIFICATION,
|
||||
data: {
|
||||
businessType: 'ticket',
|
||||
ticket: {
|
||||
...ticket,
|
||||
availableQty: available - qty,
|
||||
usedQty: used + qty
|
||||
},
|
||||
qty
|
||||
},
|
||||
message: `核销成功(已使用${qty}次)`
|
||||
};
|
||||
}
|
||||
|
||||
// 验证礼品卡核销码
|
||||
const gift = await getShopGiftByCode(decryptedOrRaw);
|
||||
|
||||
if (!gift) {
|
||||
throw new Error('核销码无效');
|
||||
@@ -187,7 +245,7 @@ export function useUnifiedQRScan() {
|
||||
|
||||
return {
|
||||
type: ScanType.VERIFICATION,
|
||||
data: gift,
|
||||
data: { businessType: 'gift', gift },
|
||||
message: '核销成功'
|
||||
};
|
||||
}, [isAdmin]);
|
||||
@@ -213,7 +271,14 @@ export function useUnifiedQRScan() {
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || '扫码失败'));
|
||||
const msg = (err as any)?.errMsg || '';
|
||||
// `scanCode:fail cancel` is a user-driven cancel; don't treat it as an error toast.
|
||||
if (typeof msg === 'string' && msg.toLowerCase().includes('cancel')) {
|
||||
cancelRef.current = true;
|
||||
reject(new Error('取消扫码'));
|
||||
return;
|
||||
}
|
||||
reject(new Error(msg || '扫码失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -265,6 +330,11 @@ export function useUnifiedQRScan() {
|
||||
return result;
|
||||
|
||||
} catch (err: any) {
|
||||
// User cancelled scanning (e.g. `scanCode:fail cancel`).
|
||||
if (cancelRef.current) {
|
||||
reset();
|
||||
return null;
|
||||
}
|
||||
if (!cancelRef.current) {
|
||||
setState(UnifiedScanState.ERROR);
|
||||
const errorMessage = err.message || '处理失败';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { User } from '@/api/system/user/model';
|
||||
import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout';
|
||||
import { getUserInfo, updateUserInfo, loginByOpenId, getWxOpenId } from '@/api/layout';
|
||||
import { TenantId } from '@/config/app';
|
||||
import {getStoredInviteParams, handleInviteRelation} from '@/utils/invite';
|
||||
import { handleInviteRelation } from '@/utils/invite';
|
||||
|
||||
// 用户Hook
|
||||
export const useUser = () => {
|
||||
@@ -27,6 +27,24 @@ export const useUser = () => {
|
||||
setUser(data.user);
|
||||
setIsLoggedIn(true);
|
||||
|
||||
// 自动登录成功后,补齐 openid(JSAPI 微信支付必需)
|
||||
// 防止后续支付时报"下单账号与支付账号不一致"
|
||||
if (!data.user?.openid) {
|
||||
try {
|
||||
const freshCode = await new Promise<string | undefined>((resolve, reject) => {
|
||||
Taro.login({
|
||||
success: (r) => resolve(r.code as string),
|
||||
fail: () => resolve(undefined),
|
||||
});
|
||||
});
|
||||
if (freshCode) {
|
||||
await getWxOpenId({ code: freshCode });
|
||||
}
|
||||
} catch (_e) {
|
||||
console.warn('自动登录后绑定 openid 失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理邀请关系
|
||||
if (data.user?.userId) {
|
||||
try {
|
||||
@@ -44,15 +62,10 @@ export const useUser = () => {
|
||||
reject(new Error('自动登录失败'));
|
||||
}
|
||||
}).catch(_ => {
|
||||
// 首次注册,跳转到邀请注册页面
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const inviteParams = getStoredInviteParams()
|
||||
if (currentPage?.route !== 'dealer/apply/add' && inviteParams?.inviter) {
|
||||
return Taro.navigateTo({
|
||||
url: '/dealer/apply/add'
|
||||
});
|
||||
}
|
||||
// 登录失败(通常是新用户尚未注册/未绑定手机号等)。
|
||||
// 这里不做任何“自动跳转”,避免用户点击「我的」时被强制带到分销/申请页,体验割裂。
|
||||
// 需要登录的页面请使用 utils/auth 的 ensureLoggedIn / goToRegister 做显式跳转。
|
||||
reject(new Error('autoLoginByOpenId failed'));
|
||||
});
|
||||
},
|
||||
fail: reject
|
||||
@@ -60,7 +73,11 @@ export const useUser = () => {
|
||||
});
|
||||
return res;
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
// 新用户首次进入、未绑定手机号等场景属于“预期失败”,避免刷屏报错。
|
||||
if (msg !== 'autoLoginByOpenId failed') {
|
||||
console.error('自动登录失败:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -280,11 +297,14 @@ export const useUser = () => {
|
||||
|
||||
// 检查用户是否是管理员
|
||||
const isAdmin = () => {
|
||||
return user?.isAdmin === true;
|
||||
// Some backends use `1/0` (or `1/2`) instead of boolean.
|
||||
const v: any = (user as any)?.isAdmin;
|
||||
return v === true || v === 1 || v === '1';
|
||||
};
|
||||
|
||||
const isSuperAdmin = () => {
|
||||
return user?.isSuperAdmin === true;
|
||||
const v: any = (user as any)?.isSuperAdmin;
|
||||
return v === true || v === 1 || v === '1';
|
||||
};
|
||||
|
||||
// 获取用户余额
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {pageShopUserCoupon} from "@/api/shop/shopUserCoupon";
|
||||
import {pageShopGift} from "@/api/shop/shopGift";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {getUserInfo} from "@/api/layout";
|
||||
import { getUserCardStats } from '@/api/user'
|
||||
|
||||
interface UserData {
|
||||
balance: number
|
||||
balance: string
|
||||
points: number
|
||||
coupons: number
|
||||
giftCards: number
|
||||
@@ -24,7 +22,7 @@ interface UseUserDataReturn {
|
||||
loading: boolean
|
||||
error: string | null
|
||||
refresh: () => Promise<void>
|
||||
updateBalance: (newBalance: number) => void
|
||||
updateBalance: (newBalance: string) => void
|
||||
updatePoints: (newPoints: number) => void
|
||||
}
|
||||
|
||||
@@ -43,18 +41,14 @@ export const useUserData = (): UseUserDataReturn => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 并发请求所有数据
|
||||
const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
|
||||
getUserInfo(),
|
||||
pageShopUserCoupon({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0}),
|
||||
pageShopGift({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0})
|
||||
])
|
||||
// 聚合接口:一次请求返回余额/积分/优惠券/礼品卡统计(后端可按用户做缓存)
|
||||
const stats = await getUserCardStats()
|
||||
|
||||
const newData: UserData = {
|
||||
balance: userDataRes?.balance || 0.00,
|
||||
points: userDataRes?.points || 0,
|
||||
coupons: couponsRes?.count || 0,
|
||||
giftCards: giftCardsRes?.count || 0,
|
||||
balance: stats?.balance || '0.00',
|
||||
points: stats?.points || 0,
|
||||
coupons: stats?.coupons || 0,
|
||||
giftCards: stats?.giftCards || 0,
|
||||
orders: {
|
||||
pending: 0,
|
||||
paid: 0,
|
||||
@@ -78,7 +72,7 @@ export const useUserData = (): UseUserDataReturn => {
|
||||
}, [fetchUserData])
|
||||
|
||||
// 更新余额(本地更新,避免频繁请求)
|
||||
const updateBalance = useCallback((newBalance: number) => {
|
||||
const updateBalance = useCallback((newBalance: string) => {
|
||||
setData(prev => prev ? { ...prev, balance: newBalance } : null)
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
Divider,
|
||||
ConfigProvider
|
||||
} from '@nutui/nutui-react-taro';
|
||||
import {ArrowLeft, Del} from '@nutui/icons-react-taro';
|
||||
import {Del} from '@nutui/icons-react-taro';
|
||||
import {View} from '@tarojs/components';
|
||||
import {CartItem, useCart} from "@/hooks/useCart";
|
||||
import './cart.scss';
|
||||
import { ensureLoggedIn } from '@/utils/auth'
|
||||
|
||||
function Cart() {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
|
||||
@@ -41,7 +42,7 @@ function Cart() {
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: '购物车 - 时里院子市集',
|
||||
title: '购物车 - 桂乐淘',
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
},
|
||||
@@ -150,6 +151,9 @@ function Cart() {
|
||||
// 将选中的商品信息存储到本地,供结算页面使用
|
||||
Taro.setStorageSync('checkout_items', JSON.stringify(selectedCartItems));
|
||||
|
||||
// 未登录则引导去注册/登录;登录后回到购物车结算页
|
||||
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
|
||||
|
||||
// 跳转到购物车结算页面
|
||||
Taro.navigateTo({
|
||||
url: '/shop/orderConfirmCart/index'
|
||||
@@ -171,7 +175,6 @@ function Cart() {
|
||||
<NavBar
|
||||
fixed={true}
|
||||
style={{marginTop: `${statusBarHeight}px`}}
|
||||
left={<ArrowLeft onClick={() => Taro.navigateBack()}/>}
|
||||
right={
|
||||
cartItems.length > 0 && (
|
||||
<Button
|
||||
@@ -227,7 +230,6 @@ function Cart() {
|
||||
<NavBar
|
||||
fixed={true}
|
||||
style={{marginTop: `${statusBarHeight}px`}}
|
||||
left={<ArrowLeft onClick={() => Taro.navigateBack()}/>}
|
||||
right={
|
||||
cartItems.length > 0 && (
|
||||
<Button
|
||||
|
||||
@@ -49,7 +49,7 @@ function Category() {
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: `${nav?.categoryName}_时里院子市集`,
|
||||
title: `${nav?.categoryName}_桂乐淘`,
|
||||
path: `/shop/category/index?id=${categoryId}`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
|
||||
@@ -6,52 +6,81 @@ import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getCmsAdByCode} from "@/api/cms/cmsAd";
|
||||
import navTo from "@/utils/common";
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
type AdImage = {
|
||||
url?: string
|
||||
path?: string
|
||||
title?: string
|
||||
// Compatible keys (some backends use different fields)
|
||||
src?: string
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
function normalizeAdImages(ad?: CmsAd): AdImage[] {
|
||||
const list = ad?.imageList
|
||||
if (Array.isArray(list) && list.length) return list as AdImage[]
|
||||
|
||||
// Some APIs only return `images` as a JSON string.
|
||||
const raw = ad?.images
|
||||
if (!raw) return []
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
return Array.isArray(parsed) ? (parsed as AdImage[]) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function toNumberPx(input: unknown, fallback: number) {
|
||||
const n = typeof input === 'number' ? input : Number.parseInt(String(input ?? ''), 10)
|
||||
return Number.isFinite(n) ? n : fallback
|
||||
}
|
||||
|
||||
const MyPage = () => {
|
||||
const [carouselData, setCarouselData] = useState<CmsAd>()
|
||||
const [hotToday, setHotToday] = useState<CmsAd>()
|
||||
const [item, setItem] = useState<CmsArticle>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
// const [disableSwiper, setDisableSwiper] = useState(false)
|
||||
|
||||
const CACHE_KEY = 'home_banner_mp-ad'
|
||||
|
||||
// 用于记录触摸开始位置
|
||||
// const touchStartRef = useRef({x: 0, y: 0})
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
const loadData = async (opts?: {silent?: boolean}) => {
|
||||
if (!opts?.silent) setLoading(true)
|
||||
try {
|
||||
setLoading(true)
|
||||
// 轮播图
|
||||
const flash = await getCmsAdByCode('flash')
|
||||
// 今日热卖
|
||||
const hotToday = await getCmsAdByCode('hot_today')
|
||||
// 时里动态
|
||||
const news = await pageCmsArticle({limit:1,recommend:1})
|
||||
// 赋值
|
||||
if(flash){
|
||||
// 只阻塞 banner 自己的数据;其他数据预热不应影响首屏展示速度
|
||||
const flash = await getCmsAdByCode('mp-ad')
|
||||
setCarouselData(flash)
|
||||
}
|
||||
if(hotToday){
|
||||
setHotToday(hotToday)
|
||||
}
|
||||
if(news && news.list.length > 0){
|
||||
setItem(news.list[0])
|
||||
}
|
||||
void Taro.setStorage({ key: CACHE_KEY, data: flash }).catch(() => {})
|
||||
} catch (error) {
|
||||
console.error('Banner数据加载失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
if (!opts?.silent) setLoading(false)
|
||||
}
|
||||
|
||||
// 后台预热(不阻塞 banner 渲染)
|
||||
void getCmsAdByCode('hot_today').catch(() => {})
|
||||
void pageCmsArticle({ limit: 1, recommend: 1 }).catch(() => {})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
const cached = Taro.getStorageSync(CACHE_KEY) as CmsAd | undefined
|
||||
// 有缓存则先渲染缓存,避免首屏等待;再静默刷新
|
||||
if (cached && normalizeAdImages(cached).length) {
|
||||
setCarouselData(cached)
|
||||
setLoading(false)
|
||||
void loadData({ silent: true })
|
||||
return
|
||||
}
|
||||
void loadData()
|
||||
}, [])
|
||||
|
||||
// 轮播图高度,默认300px
|
||||
const carouselHeight = carouselData?.height || 300;
|
||||
const carouselHeight = toNumberPx(carouselData?.height, 300)
|
||||
const carouselImages = normalizeAdImages(carouselData)
|
||||
|
||||
// 骨架屏组件
|
||||
const BannerSkeleton = () => (
|
||||
@@ -100,12 +129,6 @@ const MyPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex p-2 justify-between" style={{height: `${carouselHeight}px`}}>
|
||||
{/* 左侧轮播图区域 */}
|
||||
<View
|
||||
style={{width: '50%', height: '100%'}}
|
||||
className="banner-swiper-container"
|
||||
>
|
||||
<Swiper
|
||||
defaultValue={0}
|
||||
height={carouselHeight}
|
||||
@@ -120,15 +143,19 @@ const MyPage = () => {
|
||||
direction="horizontal"
|
||||
className="custom-swiper"
|
||||
>
|
||||
{carouselData && carouselData?.imageList?.map((img, index) => (
|
||||
{carouselImages.map((img, index) => {
|
||||
const src = img.url || img.src || img.imageUrl
|
||||
if (!src) return null
|
||||
return (
|
||||
<Swiper.Item key={index} style={{ touchAction: 'pan-x pan-y' }}>
|
||||
<Image
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={img.url}
|
||||
src={src}
|
||||
mode={'scaleToFill'}
|
||||
onClick={() => navTo(`${img.path}`)}
|
||||
lazyLoad={false}
|
||||
onClick={() => (img.path ? navTo(`${img.path}`) : undefined)}
|
||||
// 首张图优先加载,其余按需懒加载,避免并发图片请求拖慢首屏可见
|
||||
lazyLoad={index !== 0}
|
||||
style={{
|
||||
height: `${carouselHeight}px`,
|
||||
borderRadius: '4px',
|
||||
@@ -136,58 +163,9 @@ const MyPage = () => {
|
||||
}}
|
||||
/>
|
||||
</Swiper.Item>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</Swiper>
|
||||
</View>
|
||||
|
||||
{/* 右侧上下图片区域 - 从API获取数据 */}
|
||||
<View className="flex flex-col" style={{width: '50%', height: '100%'}}>
|
||||
{/* 上层图片 - 使用今日热卖素材 */}
|
||||
<View className={'ml-2 bg-white rounded-lg shadow-sm'}>
|
||||
<View className={'px-3 my-2 font-bold text-sm'}>今日热卖</View>
|
||||
<View className={'px-3 flex'} style={{
|
||||
height: '110px'
|
||||
}}>
|
||||
{
|
||||
hotToday?.imageList?.map(item => (
|
||||
<View className={'item flex flex-col mr-1'} key={item.url}>
|
||||
<Image
|
||||
width={70}
|
||||
height={70}
|
||||
src={item.url}
|
||||
mode={'scaleToFill'}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
onClick={() => navTo('/shop/category/index?id=4424')}
|
||||
/>
|
||||
<View className={'text-xs py-2 text-orange-600 whitespace-nowrap'}>{item.title || '到手价¥9.9'}</View>
|
||||
</View>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 下层图片 - 使用社区拼团素材 */}
|
||||
<View className={'ml-2 bg-white rounded-lg mt-3 shadow-sm'}>
|
||||
<View className={'px-3 my-2 font-bold text-sm'}>走进社区</View>
|
||||
<View className={'rounded-lg px-3 pb-3'}>
|
||||
<Image
|
||||
width={'100%'}
|
||||
height={94}
|
||||
src={item?.image}
|
||||
mode={'scaleToFill'}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
onClick={() => navTo('cms/detail/index?id=' + item?.articleId)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,3 @@
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 吸顶状态下的样式 */
|
||||
.nutui-sticky--fixed {
|
||||
.header-bg {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,151 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro';
|
||||
import {Button, Space, Sticky} from '@nutui/nutui-react-taro'
|
||||
import {TriangleDown} from '@nutui/icons-react-taro'
|
||||
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
|
||||
import {Button, Popup, Cell, CellGroup} from '@nutui/nutui-react-taro'
|
||||
// import {TriangleDown} from '@nutui/icons-react-taro'
|
||||
import { NavBar} from '@nutui/nutui-react-taro'
|
||||
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {TenantId, TenantName} from "@/config/app";
|
||||
import {getOrganization} from "@/api/system/organization";
|
||||
import {myUserVerify} from "@/api/system/userVerify";
|
||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
||||
// import { useShopInfo } from '@/hooks/useShopInfo';
|
||||
import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite";
|
||||
import {View,Text} from '@tarojs/components'
|
||||
import MySearch from "./MySearch";
|
||||
import './Header.scss';
|
||||
import {User} from "@/api/system/user/model";
|
||||
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
||||
import type {ShopStore} from "@/api/shop/shopStore/model";
|
||||
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
||||
|
||||
const Header = (_: any) => {
|
||||
// 使用新的useShopInfo Hook
|
||||
const {
|
||||
getWebsiteName,
|
||||
getWebsiteLogo
|
||||
} = useShopInfo();
|
||||
// const {
|
||||
// getWebsiteLogo
|
||||
// } = useShopInfo();
|
||||
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(true)
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||
const [userInfo] = useState<User>()
|
||||
|
||||
// 门店选择:用于首页展示“最近门店”,并在下单时写入订单 storeId
|
||||
const [storePopupVisible, setStorePopupVisible] = useState(false)
|
||||
const [stores, setStores] = useState<ShopStore[]>([])
|
||||
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
|
||||
const [userLocation, setUserLocation] = useState<{lng: number; lat: number} | null>(null)
|
||||
|
||||
const getTenantName = () => {
|
||||
return userInfo?.tenantName || TenantName
|
||||
}
|
||||
|
||||
const parseStoreCoords = (s: ShopStore): {lng: number; lat: number} | null => {
|
||||
const raw = (s.lngAndLat || s.location || '').trim()
|
||||
if (!raw) return null
|
||||
|
||||
const parts = raw.split(/[,\s]+/).filter(Boolean)
|
||||
if (parts.length < 2) return null
|
||||
|
||||
const a = parseFloat(parts[0])
|
||||
const b = parseFloat(parts[1])
|
||||
if (Number.isNaN(a) || Number.isNaN(b)) return null
|
||||
|
||||
// 常见格式是 "lng,lat";这里做一个简单兜底(经度范围更宽)
|
||||
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90
|
||||
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180
|
||||
if (looksLikeLngLat) return {lng: a, lat: b}
|
||||
if (looksLikeLatLng) return {lng: b, lat: a}
|
||||
return null
|
||||
}
|
||||
|
||||
const distanceMeters = (a: {lng: number; lat: number}, b: {lng: number; lat: number}) => {
|
||||
const toRad = (x: number) => (x * Math.PI) / 180
|
||||
const R = 6371000 // meters
|
||||
const dLat = toRad(b.lat - a.lat)
|
||||
const dLng = toRad(b.lng - a.lng)
|
||||
const lat1 = toRad(a.lat)
|
||||
const lat2 = toRad(b.lat)
|
||||
const sin1 = Math.sin(dLat / 2)
|
||||
const sin2 = Math.sin(dLng / 2)
|
||||
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2
|
||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)))
|
||||
}
|
||||
|
||||
const formatDistance = (meters?: number) => {
|
||||
if (meters === undefined || Number.isNaN(meters)) return ''
|
||||
if (meters < 1000) return `${Math.round(meters)}m`
|
||||
return `${(meters / 1000).toFixed(1)}km`
|
||||
}
|
||||
|
||||
const getStoreDistance = (s: ShopStore) => {
|
||||
if (!userLocation) return undefined
|
||||
const coords = parseStoreCoords(s)
|
||||
if (!coords) return undefined
|
||||
return distanceMeters(userLocation, coords)
|
||||
}
|
||||
|
||||
const initStoreSelection = async () => {
|
||||
// 先读取本地已选门店,避免页面首屏抖动
|
||||
const stored = getSelectedStoreFromStorage()
|
||||
if (stored?.id) {
|
||||
setSelectedStore(stored)
|
||||
}
|
||||
|
||||
// 拉取门店列表(失败时允许用户手动重试/继续使用本地门店)
|
||||
let list: ShopStore[] = []
|
||||
try {
|
||||
list = await listShopStore()
|
||||
} catch (e) {
|
||||
console.error('获取门店列表失败:', e)
|
||||
list = []
|
||||
}
|
||||
const usable = (list || []).filter(s => s?.isDelete !== 1)
|
||||
setStores(usable)
|
||||
|
||||
// 尝试获取定位,用于计算最近门店
|
||||
let loc: {lng: number; lat: number} | null = null
|
||||
try {
|
||||
const r = await Taro.getLocation({type: 'gcj02'})
|
||||
loc = {lng: r.longitude, lat: r.latitude}
|
||||
} catch (e) {
|
||||
// 不强制定位授权;无定位时仍允许用户手动选择
|
||||
console.warn('获取定位失败,将不显示最近门店距离:', e)
|
||||
}
|
||||
setUserLocation(loc)
|
||||
|
||||
const ensureStoreDetail = async (s: ShopStore) => {
|
||||
if (!s?.id) return s
|
||||
// 如果后端已经返回默认仓库等字段,就不额外请求
|
||||
if (s.warehouseId) return s
|
||||
try {
|
||||
const full = await getShopStore(s.id)
|
||||
return full || s
|
||||
} catch (_e) {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// 若用户没有选过门店,则自动选择最近门店(或第一个)
|
||||
const alreadySelected = stored?.id
|
||||
if (alreadySelected || usable.length === 0) return
|
||||
|
||||
let autoPick: ShopStore | undefined
|
||||
if (loc) {
|
||||
autoPick = [...usable]
|
||||
.map(s => {
|
||||
const coords = parseStoreCoords(s)
|
||||
const d = coords ? distanceMeters(loc, coords) : undefined
|
||||
return {s, d}
|
||||
})
|
||||
.sort((x, y) => (x.d ?? Number.POSITIVE_INFINITY) - (y.d ?? Number.POSITIVE_INFINITY))[0]?.s
|
||||
} else {
|
||||
autoPick = usable[0]
|
||||
}
|
||||
|
||||
if (autoPick?.id) {
|
||||
const full = await ensureStoreDetail(autoPick)
|
||||
setSelectedStore(full)
|
||||
saveSelectedStoreToStorage(full)
|
||||
}
|
||||
}
|
||||
|
||||
const reload = async () => {
|
||||
Taro.getSystemInfo({
|
||||
@@ -109,7 +232,7 @@ const Header = (_: any) => {
|
||||
success: function () {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code,
|
||||
@@ -167,12 +290,6 @@ const Header = (_: any) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 处理粘性布局状态变化
|
||||
const onStickyChange = (isSticky: boolean) => {
|
||||
setStickyStatus(isSticky)
|
||||
console.log('Header 粘性状态:', isSticky ? '已固定' : '取消固定')
|
||||
}
|
||||
|
||||
// 获取小程序系统信息
|
||||
// const getSystemInfo = () => {
|
||||
// const systemInfo = Taro.getSystemInfoSync()
|
||||
@@ -182,26 +299,21 @@ const Header = (_: any) => {
|
||||
|
||||
useEffect(() => {
|
||||
reload().then()
|
||||
initStoreSelection().then()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sticky
|
||||
threshold={0}
|
||||
onChange={onStickyChange}
|
||||
<View
|
||||
className={'header-bg'}
|
||||
style={{
|
||||
zIndex: 1000,
|
||||
backgroundColor: stickyStatus ? '#03605c' : 'transparent',
|
||||
transition: 'background-color 0.3s ease',
|
||||
height: '180px',
|
||||
paddingBottom: '12px',
|
||||
}}
|
||||
>
|
||||
<View className={'header-bg'} style={{
|
||||
height: !stickyStatus ? '180px' : `${(statusBarHeight || 0) + 44}px`,
|
||||
paddingBottom: !stickyStatus ? '12px' : '0px'
|
||||
}}>
|
||||
{/* 只在非吸顶状态下显示搜索框 */}
|
||||
{!stickyStatus && <MySearch statusBarHeight={statusBarHeight} />}
|
||||
<MySearch statusBarHeight={statusBarHeight} />
|
||||
</View>
|
||||
|
||||
<NavBar
|
||||
style={{
|
||||
marginTop: `${statusBarHeight}px`,
|
||||
@@ -210,33 +322,96 @@ const Header = (_: any) => {
|
||||
}}
|
||||
onBackClick={() => {
|
||||
}}
|
||||
left={
|
||||
// left={
|
||||
// <View
|
||||
// style={{display: 'flex', alignItems: 'center', gap: '8px'}}
|
||||
// onClick={() => setStorePopupVisible(true)}
|
||||
// >
|
||||
// <Avatar
|
||||
// size="22"
|
||||
// src={getWebsiteLogo()}
|
||||
// />
|
||||
// <Text className={'text-white'}>
|
||||
// {selectedStore?.name || '请选择门店'}
|
||||
// </Text>
|
||||
// <TriangleDown className={'text-white'} size={9}/>
|
||||
// </View>
|
||||
// }
|
||||
right={
|
||||
!IsLogin ? (
|
||||
<View style={{display: 'flex', alignItems: 'center'}}>
|
||||
<Button style={{color: '#ffffff'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
|
||||
<Space>
|
||||
<Avatar
|
||||
size="22"
|
||||
src={getWebsiteLogo()}
|
||||
/>
|
||||
<Text style={{color: '#ffffff'}}>{getWebsiteName()}</Text>
|
||||
<TriangleDown size={9} className={'text-white'}/>
|
||||
</Space>
|
||||
<Button
|
||||
size="small"
|
||||
fill="none"
|
||||
style={{color: '#ffffff'}}
|
||||
open-type="getPhoneNumber"
|
||||
onGetPhoneNumber={handleGetPhoneNumber}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
|
||||
<Avatar
|
||||
size="22"
|
||||
src={getWebsiteLogo()}
|
||||
/>
|
||||
<Text className={'text-white'}>{getWebsiteName()}</Text>
|
||||
<TriangleDown className={'text-white'} size={9}/>
|
||||
</View>
|
||||
)}>
|
||||
{/*<QRLoginButton />*/}
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<Text className={'text-white'}>{getTenantName()}</Text>
|
||||
</NavBar>
|
||||
</Sticky>
|
||||
|
||||
<Popup
|
||||
visible={storePopupVisible}
|
||||
position="bottom"
|
||||
style={{height: '70vh'}}
|
||||
onClose={() => setStorePopupVisible(false)}
|
||||
>
|
||||
<View className="p-4">
|
||||
<View className="flex justify-between items-center mb-3">
|
||||
<Text className="text-base font-medium">选择门店</Text>
|
||||
<Text
|
||||
className="text-sm text-gray-500"
|
||||
onClick={() => setStorePopupVisible(false)}
|
||||
>
|
||||
关闭
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-xs text-gray-500 mb-2">
|
||||
{userLocation ? '已获取定位,按距离排序' : '未获取定位,可手动选择门店'}
|
||||
</View>
|
||||
|
||||
<CellGroup>
|
||||
{[...stores]
|
||||
.sort((a, b) => (getStoreDistance(a) ?? Number.POSITIVE_INFINITY) - (getStoreDistance(b) ?? Number.POSITIVE_INFINITY))
|
||||
.map((s) => {
|
||||
const d = getStoreDistance(s)
|
||||
const isActive = !!selectedStore?.id && selectedStore.id === s.id
|
||||
return (
|
||||
<Cell
|
||||
key={s.id}
|
||||
title={
|
||||
<View className="flex items-center justify-between">
|
||||
<Text className={isActive ? 'text-green-600' : ''}>{s.name || `门店${s.id}`}</Text>
|
||||
{d !== undefined && <Text className="text-xs text-gray-500">{formatDistance(d)}</Text>}
|
||||
</View>
|
||||
}
|
||||
description={s.address || ''}
|
||||
onClick={async () => {
|
||||
let storeToSave = s
|
||||
if (s?.id) {
|
||||
try {
|
||||
const full = await getShopStore(s.id)
|
||||
if (full) storeToSave = full
|
||||
} catch (_e) {
|
||||
// keep base item
|
||||
}
|
||||
}
|
||||
setSelectedStore(storeToSave)
|
||||
saveSelectedStoreToStorage(storeToSave)
|
||||
setStorePopupVisible(false)
|
||||
Taro.showToast({title: '门店已切换', icon: 'success'})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</CellGroup>
|
||||
</View>
|
||||
</Popup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const Login = (props: LoginProps) => {
|
||||
success: function () {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code,
|
||||
|
||||
@@ -30,7 +30,7 @@ function MySearch(props: any) {
|
||||
|
||||
|
||||
return (
|
||||
<div className={'z-50 left-0 w-full'}>
|
||||
<div className={'z-50 left-0 w-full hidden'}>
|
||||
<div className={'px-2'}>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -4,6 +4,376 @@ page {
|
||||
background: linear-gradient(to bottom, #e9fff2, #ffffff);
|
||||
}
|
||||
|
||||
.home-page {
|
||||
padding: 24rpx 24rpx calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.home-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 28rpx;
|
||||
background: linear-gradient(180deg, #bfefff 0%, #eafaff 40%, #fff7ec 100%);
|
||||
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.home-hero__bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(360rpx 240rpx at 18% 16%, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)),
|
||||
radial-gradient(320rpx 220rpx at 84% 18%, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0)),
|
||||
linear-gradient(180deg, rgba(0, 207, 255, 0.12), rgba(0, 0, 0, 0));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.home-hero__content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
padding: 26rpx 24rpx 28rpx;
|
||||
min-height: 320rpx;
|
||||
}
|
||||
|
||||
.home-hero__left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.home-hero__topRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.home-hero__brand {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 214, 84, 0.92);
|
||||
color: #2a2a2a;
|
||||
font-weight: 700;
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-hero__brandText {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-hero__tag {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 10rpx 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(90deg, #22d64a 0%, #7df4b0 100%);
|
||||
box-shadow: 0 14rpx 24rpx rgba(36, 202, 148, 0.22);
|
||||
}
|
||||
|
||||
.home-hero__tagText {
|
||||
font-size: 56rpx;
|
||||
font-weight: 900;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-hero__date {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.home-hero__dateText {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.home-hero__headline {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.home-hero__headlineText {
|
||||
display: block;
|
||||
font-size: 42rpx;
|
||||
font-weight: 900;
|
||||
color: #0b0b0b;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.home-hero__right {
|
||||
width: 200rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.home-hero__bottle {
|
||||
position: relative;
|
||||
width: 190rpx;
|
||||
height: 250rpx;
|
||||
border-radius: 28rpx;
|
||||
background:
|
||||
radial-gradient(240rpx 360rpx at 60% 30%, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.18)),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.1));
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.65);
|
||||
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.home-hero__bottleCap {
|
||||
position: absolute;
|
||||
top: 14rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 88rpx;
|
||||
height: 26rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(180deg, #d7e6f3, #b0cadd);
|
||||
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.home-hero__bottleLabel {
|
||||
position: absolute;
|
||||
left: 18rpx;
|
||||
right: 18rpx;
|
||||
bottom: 30rpx;
|
||||
padding: 12rpx 12rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(90deg, rgba(0, 150, 255, 0.18), rgba(0, 255, 210, 0.18));
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.home-hero__bottleLabelText {
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: rgba(0, 80, 140, 0.95);
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ticket-card {
|
||||
margin-top: 18rpx;
|
||||
border-radius: 22rpx;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.ticket-card__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 18rpx 20rpx;
|
||||
background: linear-gradient(90deg, #22d64a 0%, #2fa560 100%);
|
||||
}
|
||||
|
||||
.ticket-card__title {
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.ticket-card__count {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.ticket-card__countNum {
|
||||
color: #ffffff;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.ticket-card__body {
|
||||
padding: 20rpx 10rpx 22rpx;
|
||||
}
|
||||
|
||||
.shortcut-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.shortcut-grid__item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.shortcut-grid__icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
color: #20c26a;
|
||||
border: 2rpx solid rgba(32, 194, 106, 0.35);
|
||||
}
|
||||
|
||||
.shortcut-grid__text {
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.home-tabs {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.home-tabs__inner {
|
||||
display: flex;
|
||||
gap: 18rpx;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
.home-tabs__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.home-tabs__item--active {
|
||||
background: rgba(32, 194, 106, 0.16);
|
||||
}
|
||||
|
||||
.home-tabs__itemText {
|
||||
font-size: 28rpx;
|
||||
color: #2a2a2a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.home-tabs__item--active .home-tabs__itemText {
|
||||
color: #16b65a;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.goods-grid {
|
||||
margin-top: 18rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.goods-card {
|
||||
border-radius: 22rpx;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.goods-card__imgWrap {
|
||||
padding: 18rpx 18rpx 0;
|
||||
}
|
||||
|
||||
.goods-card__img {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
.goods-card__body {
|
||||
padding: 18rpx 18rpx 20rpx;
|
||||
}
|
||||
|
||||
.goods-card__title {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #1c1c1c;
|
||||
min-height: 72rpx;
|
||||
}
|
||||
|
||||
.goods-card__meta {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.goods-card__sold {
|
||||
font-size: 22rpx;
|
||||
color: #9a9a9a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.goods-card__price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
color: #27c86b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.goods-card__priceUnit {
|
||||
font-size: 22rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.goods-card__priceValue {
|
||||
font-size: 36rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.goods-card__actions {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.goods-card__btn {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.goods-card__btn--ghost {
|
||||
border: 2rpx solid rgba(32, 194, 106, 0.7);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.goods-card__btn--primary {
|
||||
background: linear-gradient(90deg, #24d34c 0%, #6df09a 100%);
|
||||
}
|
||||
|
||||
.goods-card__btnText {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #18b85a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.goods-card__btnText--primary {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.buy-btn{
|
||||
height: 70px;
|
||||
background: linear-gradient(to bottom, #1cd98a, #24ca94);
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import Header from './Header';
|
||||
import BestSellers from './BestSellers';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {useShareAppMessage} from "@tarojs/taro"
|
||||
import {useEffect, useState} from "react";
|
||||
import {getShopInfo} from "@/api/layout";
|
||||
import Menu from "./Menu";
|
||||
import Banner from "./Banner";
|
||||
import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite";
|
||||
// import Header from './Header'
|
||||
import Banner from './Banner'
|
||||
import Taro, { useDidShow, useShareAppMessage } from '@tarojs/taro'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { Cart, Gift, Ticket, Agenda, ArrowRight } from '@nutui/icons-react-taro'
|
||||
import { getShopInfo } from '@/api/layout'
|
||||
import { checkAndHandleInviteRelation, hasPendingInvite } from '@/utils/invite'
|
||||
import { pageShopGoods } from '@/api/shop/shopGoods'
|
||||
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'
|
||||
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
|
||||
import { ensureLoggedIn } from '@/utils/auth'
|
||||
import './index.scss'
|
||||
// import navTo from "@/utils/common";
|
||||
|
||||
function Home() {
|
||||
// 吸顶状态
|
||||
// const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||
// Tabs粘性状态
|
||||
const [_, setTabsStickyStatus] = useState<boolean>(false)
|
||||
const [activeTabKey, setActiveTabKey] = useState('recommend')
|
||||
const [goodsList, setGoodsList] = useState<ShopGoods[]>([])
|
||||
const [ticketTotal, setTicketTotal] = useState(0)
|
||||
|
||||
useShareAppMessage(() => {
|
||||
// 获取当前用户ID,用于生成邀请链接
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
const user = Taro.getStorageSync('User') || {};
|
||||
const nickname =
|
||||
(user && (user.nickname || user.realName || user.username)) || '';
|
||||
|
||||
return {
|
||||
title: '🏠 首页 🏠',
|
||||
title: (nickname || '') + '超值推荐',
|
||||
path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`,
|
||||
success: function () {
|
||||
console.log('首页分享成功');
|
||||
@@ -85,14 +91,30 @@ function Home() {
|
||||
// }
|
||||
|
||||
// 处理Tabs粘性状态变化
|
||||
const handleTabsStickyChange = (isSticky: boolean) => {
|
||||
setTabsStickyStatus(isSticky)
|
||||
}
|
||||
// const handleTabsStickyChange = (isSticky: boolean) => {}
|
||||
|
||||
const reload = () => {
|
||||
|
||||
const token = Taro.getStorageSync('access_token')
|
||||
const userIdRaw = Taro.getStorageSync('UserId')
|
||||
const userId = Number(userIdRaw)
|
||||
const hasUserId = Number.isFinite(userId) && userId > 0
|
||||
if (!token && !hasUserId) {
|
||||
setTicketTotal(0)
|
||||
return
|
||||
}
|
||||
getMyGltUserTicketTotal(hasUserId ? userId : undefined)
|
||||
.then((total) => setTicketTotal(typeof total === 'number' ? total : 0))
|
||||
.catch((err) => {
|
||||
console.error('首页水票总数加载失败:', err)
|
||||
setTicketTotal(0)
|
||||
})
|
||||
};
|
||||
|
||||
// 回到首页/首次进入时都刷新一次(避免依赖 scope.userInfo 导致不触发 reload)
|
||||
useDidShow(() => {
|
||||
reload()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// 获取站点信息
|
||||
getShopInfo().then(() => {
|
||||
@@ -135,7 +157,6 @@ function Home() {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户已经授权过,可以直接获取用户信息
|
||||
console.log('用户已经授权过,可以直接获取用户信息')
|
||||
reload();
|
||||
} else {
|
||||
// 用户未授权,需要弹出授权窗口
|
||||
console.log('用户未授权,需要弹出授权窗口')
|
||||
@@ -147,21 +168,211 @@ function Home() {
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
const avatar = res.userInfo.avatarUrl;
|
||||
// Keep WeChat display name in storage so share title can use it.
|
||||
console.log(avatar, 'avatarUrl')
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const tabs = useMemo<
|
||||
Array<{ key: string; title: string; params: Partial<ShopGoodsParam> }>
|
||||
>(
|
||||
() => [
|
||||
{ key: 'recommend', title: '推荐', params: { recommend: 1 } },
|
||||
{ key: '4476', title: '桶装水', params: { categoryId: 4476 } },
|
||||
{ key: '4556', title: '水票套餐', params: { categoryId: 4556 } },
|
||||
// { key: '4557', title: '购机套餐', params: { categoryId: 4557 } },
|
||||
// { key: '4477', title: '饮水设备', params: { categoryId: 4477 } },
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const tab = tabs.find((t) => t.key === activeTabKey) || tabs[0]
|
||||
if (!tab) return
|
||||
|
||||
pageShopGoods({ ...tab.params, status: 0 })
|
||||
.then((res) => setGoodsList((res?.list || []).filter((g) => g?.status === 0)))
|
||||
.catch((err) => {
|
||||
console.error('首页商品列表加载失败:', err)
|
||||
setGoodsList([])
|
||||
})
|
||||
}, [activeTabKey, tabs])
|
||||
|
||||
const shortcuts = useMemo<
|
||||
Array<{ key: string; title: string; icon: ReactNode; onClick: () => void }>
|
||||
>(
|
||||
() => [
|
||||
{
|
||||
key: 'ticket',
|
||||
title: '我的水票',
|
||||
icon: <Ticket size={30} />,
|
||||
onClick: () => {
|
||||
if (!ensureLoggedIn('/user/ticket/index')) return
|
||||
Taro.navigateTo({ url: '/user/ticket/index' })
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'order',
|
||||
title: '立即送水',
|
||||
icon: <Cart size={30} />,
|
||||
onClick: () => {
|
||||
if (!ensureLoggedIn('/user/ticket/use')) return
|
||||
Taro.navigateTo({ url: '/user/ticket/use' })
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'order',
|
||||
title: '送水订单',
|
||||
icon: <Agenda size={30} />,
|
||||
onClick: () => {
|
||||
const url = '/user/ticket/index?tab=order'
|
||||
if (!ensureLoggedIn(url)) return
|
||||
Taro.navigateTo({ url })
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'invite',
|
||||
title: '邀请有礼',
|
||||
icon: <Gift size={30} />,
|
||||
onClick: () => {
|
||||
if (!ensureLoggedIn('/dealer/qrcode/index')) return
|
||||
Taro.navigateTo({ url: '/dealer/qrcode/index' })
|
||||
},
|
||||
},
|
||||
// {
|
||||
// key: 'coupon',
|
||||
// title: '领券中心',
|
||||
// icon: <Coupon size={30} />,
|
||||
// onClick: () => Taro.navigateTo({ url: '/coupon/index' }),
|
||||
// },
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const visibleGoods = useMemo(() => {
|
||||
// 先按效果图展示两列卡片,数据不够时也保持布局稳定
|
||||
const list = goodsList || []
|
||||
if (list.length <= 6) return list
|
||||
return list.slice(0, 6)
|
||||
}, [goodsList])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Header区域 - 现在由Header组件内部处理吸顶逻辑 */}
|
||||
<Header />
|
||||
{/* Header区域 */}
|
||||
{/*<Header />*/}
|
||||
|
||||
<div className={'flex flex-col mt-12'}>
|
||||
<Menu/>
|
||||
<Banner/>
|
||||
<BestSellers onStickyChange={handleTabsStickyChange}/>
|
||||
</div>
|
||||
<View className="home-page">
|
||||
{/* 顶部活动主视觉:使用 Banner 组件 */}
|
||||
<Banner />
|
||||
|
||||
{/* 电子水票 */}
|
||||
<View className="ticket-card">
|
||||
<View className="ticket-card__head">
|
||||
<Text className="ticket-card__title">电子水票</Text>
|
||||
<Text className="ticket-card__count">
|
||||
您还有 <Text className="ticket-card__countNum">{ticketTotal}</Text> 张水票
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="ticket-card__body">
|
||||
<View className="shortcut-grid">
|
||||
{shortcuts.map((item) => (
|
||||
<View
|
||||
key={item.key}
|
||||
className="shortcut-grid__item"
|
||||
onClick={item.onClick}
|
||||
>
|
||||
<View className="shortcut-grid__icon">{item.icon}</View>
|
||||
<Text className="shortcut-grid__text">{item.title}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="ticket-card" onClick={() => Taro.navigateTo({ url: `/shop/category/index?id=4560` })}>
|
||||
<View className="ticket-card__head">
|
||||
<Text className="ticket-card__title">政企采购专区</Text>
|
||||
<ArrowRight className={'text-gray-50'} size={16} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="ticket-card" onClick={() => Taro.navigateTo({ url: `/shop/category/index?id=4556` })}>
|
||||
<View className="ticket-card__head">
|
||||
<Text className="ticket-card__title">桂乐淘·福利惊爆区</Text>
|
||||
<ArrowRight className={'text-gray-50'} size={16} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/*分类Tabs*/}
|
||||
<ScrollView className="home-tabs" scrollX enableFlex>
|
||||
<View className="home-tabs__inner">
|
||||
{tabs.map((tab) => {
|
||||
const active = tab.key === activeTabKey
|
||||
return (
|
||||
<View
|
||||
key={tab.key}
|
||||
className={`home-tabs__item ${active ? 'home-tabs__item--active' : ''}`}
|
||||
onClick={() => setActiveTabKey(tab.key)}
|
||||
>
|
||||
<Text className="home-tabs__itemText">{tab.title}</Text>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</ScrollView>
|
||||
{/* 商品列表 */}
|
||||
<View className="goods-grid">
|
||||
{visibleGoods.map((item) => (
|
||||
<View key={item.goodsId} className="goods-card">
|
||||
<View className="goods-card__imgWrap">
|
||||
<Image
|
||||
className="goods-card__img"
|
||||
src={item.image || ''}
|
||||
mode="aspectFill"
|
||||
lazyLoad={false}
|
||||
onClick={() =>
|
||||
Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="goods-card__body">
|
||||
<Text className="goods-card__title">{item.name}</Text>
|
||||
<View className="goods-card__meta">
|
||||
<Text className="goods-card__sold">已购:{item.sales || 0}人</Text>
|
||||
<View className="goods-card__price">
|
||||
<Text className="goods-card__priceUnit">¥</Text>
|
||||
<Text className="goods-card__priceValue">{item.buyingPrice}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="goods-card__actions">
|
||||
{/*<View*/}
|
||||
{/* className="goods-card__btn goods-card__btn--ghost"*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* if (!ensureLoggedIn('/shop/orderConfirm/index?goodsId=10074')) return*/}
|
||||
{/* Taro.navigateTo({ url: '/shop/orderConfirm/index?goodsId=10074' })*/}
|
||||
{/* }}*/}
|
||||
{/*>*/}
|
||||
{/* <Text className="goods-card__btnText">买水票更优惠</Text>*/}
|
||||
{/*</View>*/}
|
||||
<View
|
||||
className="goods-card__btn goods-card__btn--primary"
|
||||
onClick={() =>
|
||||
Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
|
||||
}
|
||||
>
|
||||
<Text className="goods-card__btnText goods-card__btnText--primary">立即购买</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ import {useUser} from '@/hooks/useUser'
|
||||
import {useDealerUser} from "@/hooks/useDealerUser";
|
||||
import {useThemeStyles} from "@/hooks/useTheme";
|
||||
import { useConfig } from "@/hooks/useConfig"; // 使用新的自定义Hook
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const IsDealer = () => {
|
||||
const themeStyles = useThemeStyles();
|
||||
const { config } = useConfig(); // 使用新的Hook
|
||||
const {isSuperAdmin} = useUser();
|
||||
const {dealerUser} = useDealerUser()
|
||||
const {dealerUser, loading: dealerLoading} = useDealerUser()
|
||||
|
||||
/**
|
||||
* 管理中心
|
||||
@@ -51,7 +52,7 @@ const IsDealer = () => {
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Reward className={'text-orange-100 '} size={16}/>
|
||||
<Text style={{fontSize: '16px'}}
|
||||
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '入驻申请'}</Text>
|
||||
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '账户管理中心'}</Text>
|
||||
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
|
||||
</View>
|
||||
}
|
||||
@@ -75,12 +76,18 @@ const IsDealer = () => {
|
||||
title={
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Reward className={'text-orange-100 '} size={16}/>
|
||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '开通VIP'}</Text>
|
||||
<Text className={'text-white opacity-80 pl-3'}>{config?.vipComments || '享优惠'}</Text>
|
||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '账户管理中心'}</Text>
|
||||
<Text className={'text-white opacity-80 pl-3'}>{config?.vipComments || ''}</Text>
|
||||
</View>
|
||||
}
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => navTo('/dealer/apply/add', true)}
|
||||
onClick={() => {
|
||||
if (dealerLoading) {
|
||||
Taro.showToast({ title: '正在加载信息,请稍等...', icon: 'none' })
|
||||
return
|
||||
}
|
||||
navTo('/dealer/apply/add', true)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
|
||||
@@ -8,33 +8,110 @@ import navTo from "@/utils/common";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import {useUserData} from "@/hooks/useUserData";
|
||||
import {getStoredInviteParams} from "@/utils/invite";
|
||||
import {checkAndHandleInviteRelation, getStoredInviteParams, hasPendingInvite} from "@/utils/invite";
|
||||
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
||||
import {useThemeStyles} from "@/hooks/useTheme";
|
||||
import {getRootDomain} from "@/utils/domain";
|
||||
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
|
||||
import { saveStorageByLoginUser } from '@/utils/server'
|
||||
import { getUserLevelName, getUserLevelConfig } from '@/utils/userLevel'
|
||||
|
||||
const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
const {data, refresh} = useUserData()
|
||||
const {getDisplayName, getRoleName} = useUser();
|
||||
const {loadUserFromStorage} = useUser();
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||
const [userInfo, setUserInfo] = useState<User>()
|
||||
const [ticketTotal, setTicketTotal] = useState<number>(0)
|
||||
|
||||
const themeStyles = useThemeStyles();
|
||||
const canShowScanButton = (() => {
|
||||
const v: any = (userInfo as any)?.isAdmin
|
||||
return v === true || v === 1 || v === '1'
|
||||
})()
|
||||
|
||||
const getDisplayName = () => {
|
||||
if (!userInfo) return IsLogin ? '用户' : '点击登录'
|
||||
return userInfo.nickname || (userInfo as any).realName || (userInfo as any).username || (IsLogin ? '用户' : '点击登录')
|
||||
}
|
||||
|
||||
// 角色名称:优先使用 dealerLevel 显示四种分级,否则取用户 roles 数组的第一个角色名称
|
||||
const getRoleName = () => {
|
||||
const dealerLevel = (userInfo as any)?.dealerLevel
|
||||
if (dealerLevel !== undefined && dealerLevel !== null) {
|
||||
return getUserLevelName(dealerLevel)
|
||||
}
|
||||
return userInfo?.roles?.[0]?.roleName || userInfo?.roleName || '注册用户'
|
||||
}
|
||||
|
||||
// 获取用户等级配置(用于自定义样式)
|
||||
const getRoleLevelConfig = () => {
|
||||
const dealerLevel = (userInfo as any)?.dealerLevel
|
||||
if (dealerLevel !== undefined && dealerLevel !== null) {
|
||||
return getUserLevelConfig(dealerLevel)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const roleLevelConfig = getRoleLevelConfig()
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
const reloadStats = async (showToast = false) => {
|
||||
await refresh()
|
||||
reloadTicketTotal()
|
||||
if (showToast) {
|
||||
Taro.showToast({
|
||||
title: '刷新成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const syncUserToStorage = (u: User) => {
|
||||
// Keep storage up-to-date for other places that read user info synchronously.
|
||||
Taro.setStorageSync('User', u)
|
||||
if (u?.userId) Taro.setStorageSync('UserId', u.userId)
|
||||
if (u?.nickname) Taro.setStorageSync('WxNickName', u.nickname)
|
||||
}
|
||||
|
||||
const reloadUserInfo = async () => {
|
||||
try {
|
||||
const u = await getUserInfo()
|
||||
if (u) {
|
||||
setUserInfo(u)
|
||||
setIsLogin(true)
|
||||
syncUserToStorage(u)
|
||||
// Refresh this hook instance's state from storage (defensive).
|
||||
await loadUserFromStorage()
|
||||
|
||||
// 获取openId(不阻塞 UI 刷新)
|
||||
if (!u.openid) {
|
||||
Taro.login({
|
||||
success: (res) => {
|
||||
getWxOpenId({code: res.code}).catch(() => {})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Not logged in / token expired: keep UI in "not login" state.
|
||||
// Other error handling is done in request interceptor / callers.
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh
|
||||
handleRefresh: async () => {
|
||||
await reloadUserInfo()
|
||||
await reloadStats(true)
|
||||
},
|
||||
reloadStats,
|
||||
reloadUserInfo
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
// 独立于用户信息授权:只要有登录 token,就可以拉取水票总数
|
||||
reloadTicketTotal()
|
||||
|
||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||
Taro.getSetting({
|
||||
success: (res) => {
|
||||
@@ -51,6 +128,23 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const reloadTicketTotal = () => {
|
||||
const token = Taro.getStorageSync('access_token')
|
||||
const userIdRaw = Taro.getStorageSync('UserId')
|
||||
const userId = Number(userIdRaw)
|
||||
const hasUserId = Number.isFinite(userId) && userId > 0
|
||||
if (!token && !hasUserId) {
|
||||
setTicketTotal(0)
|
||||
return
|
||||
}
|
||||
getMyGltUserTicketTotal(hasUserId ? userId : undefined)
|
||||
.then((total) => setTicketTotal(typeof total === 'number' ? total : 0))
|
||||
.catch((err) => {
|
||||
console.error('个人中心水票总数加载失败:', err)
|
||||
setTicketTotal(0)
|
||||
})
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
@@ -60,25 +154,15 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
nickname: res.userInfo.nickName,
|
||||
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
||||
})
|
||||
getUserInfo().then((data) => {
|
||||
if (data) {
|
||||
setUserInfo(data)
|
||||
setIsLogin(true);
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
|
||||
// 获取openId
|
||||
if (!data.openid) {
|
||||
Taro.login({
|
||||
success: (res) => {
|
||||
getWxOpenId({code: res.code}).then(() => {
|
||||
reloadUserInfo()
|
||||
.then(() => {
|
||||
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
|
||||
refresh().then()
|
||||
reloadTicketTotal()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
.catch(() => {
|
||||
console.log('未登录')
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -133,7 +217,7 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
success: function () {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code,
|
||||
@@ -158,10 +242,19 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
return false;
|
||||
}
|
||||
// 登录成功
|
||||
Taro.setStorageSync('access_token', res.data.data.access_token)
|
||||
Taro.setStorageSync('UserId', res.data.data.user.userId)
|
||||
saveStorageByLoginUser(res.data.data.access_token, res.data.data.user)
|
||||
setUserInfo(res.data.data.user)
|
||||
setIsLogin(true)
|
||||
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
|
||||
refresh().then()
|
||||
reloadTicketTotal()
|
||||
|
||||
// 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定
|
||||
if (hasPendingInvite()) {
|
||||
checkAndHandleInviteRelation().catch((e) => {
|
||||
console.error('个人中心登录后处理邀请关系失败:', e)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -189,7 +282,24 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
/>
|
||||
<View className={'flex flex-col'}>
|
||||
<Text style={{color: '#ffffff'}}>{getDisplayName() || '点击登录'}</Text>
|
||||
<View><Tag type="success">{getRoleName()}</Tag></View>
|
||||
{getRootDomain() && (
|
||||
<View>
|
||||
{roleLevelConfig ? (
|
||||
<Tag
|
||||
type={roleLevelConfig.tagType as any}
|
||||
style={{
|
||||
backgroundColor: roleLevelConfig.bgColor,
|
||||
color: roleLevelConfig.textColor,
|
||||
borderColor: roleLevelConfig.borderColor,
|
||||
}}
|
||||
>
|
||||
{getRoleName()}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag type="success">{getRoleName()}</Tag>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -209,11 +319,12 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
{/*统一扫码入口 - 仅管理员可见*/}
|
||||
{canShowScanButton && (
|
||||
<Space style={{
|
||||
marginTop: '30px',
|
||||
marginRight: '10px'
|
||||
}}>
|
||||
{/*统一扫码入口 - 支持登录和核销*/}
|
||||
<UnifiedQRButton
|
||||
text="扫一扫"
|
||||
size="small"
|
||||
@@ -221,10 +332,27 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
console.log('统一扫码成功:', result);
|
||||
// 根据扫码类型给出不同的提示
|
||||
if (result.type === 'verification') {
|
||||
// 核销成功,可以显示更多信息或跳转到详情页
|
||||
const businessType = result?.data?.businessType;
|
||||
if (businessType === 'gift' && result?.data?.gift) {
|
||||
const gift = result.data.gift;
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}`
|
||||
content: `已成功核销:${gift.goodsName || gift.name || '礼品'},面值¥${gift.faceValue}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (businessType === 'ticket' && result?.data?.ticket) {
|
||||
const ticket = result.data.ticket;
|
||||
const qty = result.data.qty || 1;
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: `已成功核销:${ticket.templateName || '水票'},本次使用${qty}次,剩余可用${ticket.availableQty ?? 0}次`
|
||||
});
|
||||
return;
|
||||
}
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '已成功核销'
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -233,9 +361,20 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
</View>
|
||||
<View className={'py-2'}>
|
||||
<View className={'flex justify-around mt-1'}>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/ticket/index', true)}>
|
||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>水票</Text>
|
||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{ticketTotal}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/coupon/index', true)}>
|
||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>优惠券</Text>
|
||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/wallet/wallet', true)}>
|
||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>余额</Text>
|
||||
@@ -245,16 +384,6 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>积分</Text>
|
||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.points || 0}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/coupon/index', true)}>
|
||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>优惠券</Text>
|
||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/gift/index', true)}>
|
||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>礼品卡</Text>
|
||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.giftCards || 0}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -55,7 +55,7 @@ const UserCell = () => {
|
||||
title={
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Location size={16}/>
|
||||
<Text className={'pl-3 text-sm'}>收货地址</Text>
|
||||
<Text className={'pl-3 text-sm'}>配送地址</Text>
|
||||
</View>
|
||||
}
|
||||
align="center"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {loginBySms} from "@/api/passport/login";
|
||||
import {useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View,Text} from '@tarojs/components'
|
||||
import {Popup} from '@nutui/nutui-react-taro'
|
||||
import {UserParam} from "@/api/system/user/model";
|
||||
import {Button} from '@nutui/nutui-react-taro'
|
||||
import {Button, Image} from '@nutui/nutui-react-taro'
|
||||
import {Form, Input} from '@nutui/nutui-react-taro'
|
||||
import {Copyright, Version} from "@/config/app";
|
||||
import {Copyright} from "@/config/app";
|
||||
const UserFooter = () => {
|
||||
const [openLoginByPhone, setOpenLoginByPhone] = useState(false)
|
||||
const [clickNum, setClickNum] = useState<number>(0)
|
||||
@@ -46,10 +47,14 @@ const UserFooter = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
|
||||
<div className={'text-xs text-gray-400 py-1'}>当前版本:{Version}</div>
|
||||
<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>
|
||||
</div>
|
||||
<View className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
|
||||
{/*<View className={'text-xs text-gray-400 py-1'}>当前版本:{Version}</View>*/}
|
||||
{/*<View className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</View>*/}
|
||||
<View className={'text-xs text-gray-400 py-1 flex justify-center items-center gap-2'}>
|
||||
<Image src={'https://oss.wsdns.cn/20260412/7d03ec2a05964c3e926c4eac12ee5835.png'} mode={'aspectFit'} width={20} height={20} />
|
||||
<Text>{Copyright}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Popup
|
||||
style={{width: '350px', padding: '10px'}}
|
||||
@@ -65,7 +70,7 @@ const UserFooter = () => {
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitByPhone(values)}
|
||||
footer={
|
||||
<div
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
@@ -75,7 +80,7 @@ const UserFooter = () => {
|
||||
<Button nativeType="submit" block style={{backgroundColor: '#000000',color: '#ffffff'}}>
|
||||
提交
|
||||
</Button>
|
||||
</div>
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<Form.Item
|
||||
|
||||
@@ -11,13 +11,14 @@ import {
|
||||
People,
|
||||
// AfterSaleService,
|
||||
Logout,
|
||||
ShoppingAdd,
|
||||
Shop,
|
||||
Jdl,
|
||||
Service
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
|
||||
const UserCell = () => {
|
||||
const {logoutUser} = useUser();
|
||||
const {logoutUser, hasRole} = useUser();
|
||||
|
||||
const onLogout = () => {
|
||||
Taro.showModal({
|
||||
@@ -38,7 +39,7 @@ const UserCell = () => {
|
||||
return (
|
||||
<>
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl">
|
||||
<View className="font-semibold text-gray-800 pt-4 pl-4">我的服务</View>
|
||||
<View className="font-semibold text-gray-800 pt-4 pl-4">桂乐淘服务中心</View>
|
||||
<ConfigProvider>
|
||||
<Grid
|
||||
columns={4}
|
||||
@@ -49,10 +50,49 @@ const UserCell = () => {
|
||||
border: 'none'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Grid.Item text="企业采购" onClick={() => navTo('/user/poster/poster', true)}>
|
||||
|
||||
{hasRole('store') && (
|
||||
<Grid.Item text="门店中心" onClick={() => navTo('/store/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<ShoppingAdd color="#3b82f6" size="20"/>
|
||||
<Shop color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
)}
|
||||
|
||||
{hasRole('rider') && (
|
||||
<Grid.Item text="配送中心" onClick={() => navTo('/rider/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Jdl color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
)}
|
||||
|
||||
{(hasRole('staff') || hasRole('admin')) && (
|
||||
<Grid.Item text="门店订单" onClick={() => navTo('/user/store/orders/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Shop color="#f59e0b" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
)}
|
||||
|
||||
<Grid.Item text="配送地址" onClick={() => navTo('/user/address/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Location color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Ask className={'text-cyan-500'} size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
@@ -71,14 +111,6 @@ const UserCell = () => {
|
||||
</Button>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text="收货地址" onClick={() => navTo('/user/address/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Location color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'实名认证'} onClick={() => navTo('/user/userVerify/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
@@ -111,13 +143,6 @@ const UserCell = () => {
|
||||
{/* </View>*/}
|
||||
{/*</Grid.Item>*/}
|
||||
|
||||
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Ask className={'text-cyan-500'} size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>
|
||||
<View className="text-center">
|
||||
@@ -189,4 +214,3 @@ const UserCell = () => {
|
||||
)
|
||||
}
|
||||
export default UserCell
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ function UserOrder() {
|
||||
}}
|
||||
>
|
||||
<View className={'title-bar flex justify-between pt-2'}>
|
||||
<Text className={'title font-medium px-4'}>我的订单</Text>
|
||||
<Text className={'title font-medium px-4'}>商城订单</Text>
|
||||
<View
|
||||
className={'more flex items-center px-2'}
|
||||
onClick={() => navTo('/user/order/order', true)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user