Compare commits

17 Commits

Author SHA1 Message Date
f532b3923d fix(shop): 始终显示商品详情销量,避免销量隐藏
- 修改商品详情页销量展示,去除销量大于0的判断
- 销量字段不存在或为0时统一显示为0,提升信息一致性
- 列表页仍保持销量为0时不显示的原有逻辑
- 适配后端新增销量累加方式配置,支持显示和回滚逻辑调整
2026-08-01 21:19:06 +08:00
f1c0349f64 feat(api): 新增 cms-api API 域名支持并切换幻灯片广告接口域名
- 配置文件 env.js 中新增 CMS_API_URL,三个环境均统一配置
- app.ts 导出 CmsBaseUrl,用于 cms-api 相关请求
- cmsAd 模块接口调用统一改为通过 CmsBaseUrl 拼接完整 cms-api 路径
- 幻灯片广告相关接口走独立 cms-api 域名,保证数据隔离
- 更新工作备忘录,补充 API 域名配置与幻灯片广告调用说明
2026-07-27 12:42:27 +08:00
0cc0d089e9 feat(store): 在订单列表页新增下单时间展示
- 订单卡片头部左侧新增「下单时间:YYYY-MM-DD HH:mm」显示,位于订单号下方
- 状态标签右侧对齐方式由垂直居中调整为顶端对齐,适配两行文本
- 新增辅助函数 formatOrderTime 兼容 ISO 和空格分隔时间格式,仅保留到分钟
- 复用后台返回的 ShopOrder.createTime 字段,无需后端改动,保证时间数据准确同步
2026-07-23 02:03:50 +08:00
c6e022f815 feat(store): 门店订单页新增付款凭证展示
- 在订单详情买家备注下方添加付款凭证展示区域
- 付款凭证区域背景为绿色(bg-green-50),仅在有凭证时显示
- 支持点击付款凭证图片预览大图
- 修复之前只展示送达凭证未展示付款凭证的问题
2026-07-17 17:08:01 +08:00
0d0c4b327a feat(store): 恢复门店端独立改价按钮和弹窗
- 去除确认收款弹窗中的“实付金额”输入框
- 改价功能从合并回独立弹窗及按钮
- 新增改价相关状态和函数处理逻辑
- 线下付款未付款状态显示“改价”与“确认收款”按钮
- 改价弹窗包含订单信息、新金额输入及改价备注
- 改价按钮样式调整为橙色背景白字
- 改价提交时更新订单金额及备注并刷新列表
2026-07-17 16:50:18 +08:00
7ff921d973 refactor(store): 合并改价到确认收款弹窗
- 去除独立的“改价”按钮与修改金额弹窗相关状态和逻辑
- 在确认收款弹窗内新增“实付金额”输入框,预填当前值支持修改
- 取消改价时单独输入原因,仅保留收款备注说明
- 提交确认收款时,先调用接口更新改价信息,再调用确认收款接口
- 调整订单操作类型及按钮展示,移除改价相关项
- 更新界面样式及交互,支持合并改价与收款流程
- 更新业务状态说明,调整发货及付款状态文案逻辑
2026-07-17 16:39:34 +08:00
69862bfc0c feat(order): 拆分“确认完成”为“送达”和“确认完成”按钮
- 新增送达操作,已发货且无送达照片时显示,可上传送达凭证
- 确认完成操作仅限已付款订单,标记为已完成,不需上传凭证
- 取消deliveryStatus=30表示已送达的用法,避免混淆“部分发货”状态
- 统一按钮显示逻辑,支持线下付款场景下同时显示确认收款/送达按钮
- 调整订单状态文案,新增“已送达待收款”与“待确认完成”状态描述
- 优化订单列表及详情中付款和物流状态的判断,去除对deliveryStatus=30的依赖
- 确认证据上传界面调整,确认完成操作不再显示上传凭证区域
2026-07-17 16:27:09 +08:00
184763a15c feat(store): 新增订单送达操作功能
- 新增操作类型 'deliver',用于上传送达凭证照片并标记为已收货
- 在操作列表中增加“送达”按钮,仅在订单已发货状态显示
- 送达操作不改变付款状态和订单完成状态
- 送达操作要求上传凭证照片,否则提示用户上传
- 确认完成操作仅在订单已付款时可用
- 调整凭证上传提示文案,针对送达操作显示专属提示
- 更新订单状态逻辑,送达时设置配送状态为“已收货”,完成时标记订单为已完成
2026-07-17 16:00:40 +08:00
147802dbe9 fix(order): 统一线下付款已发货未付款状态显示
- 用户端订单卡的已发货待收款状态调整,线下付款且未付款状态显示红色标识
- 用户端订单详情新增该状态的红色展示和提示信息
- 门店端订单列表状态文案和颜色同步更新,保持与用户端一致
- 门店端订单操作按钮逻辑调整,未付款已发货订单显示“确认完成”按钮
- 解决线下付款先发货后付款订单的状态显示和按钮操作不一致问题
2026-07-17 15:51:22 +08:00
37cea1b6d0 fix(order): 修正未付款已发货订单状态显示错误
- 订单列表卡片 OrderCard 中将已发货/已收货状态判断提前,覆盖未付款状态显示
- 订单详情页 detail.tsx 调整 getOrderDisplayStatus 和 getOrderPhase 函数,优先显示物流状态
- 确保未付款但已发货订单状态正确显示为“待收货”
- 改动不影响正常付款和发货流程,仅修正异常状态显示问题
2026-07-17 15:28:57 +08:00
65bcd07ca7 fix(orders): 优化线下付款订单状态和发货操作逻辑
- 调整确认收款操作描述为“确认已收到线下付款”
- 增加线下付款已发货待收款状态显示处理
- 支持线下付款订单先发货后确认收款的发货操作
- 修改发货按钮显示条件,提升线下付款订单兼容性
2026-07-17 15:10:20 +08:00
bd3589af61 feat(store-center): 在门店中心添加扫码登录PC后台功能
- 门店中心右上角新增扫码登录按钮,使用base64 SVG图标兼容小程序Image组件渲染
- 扫码后通过解析二维码内容调用confirmWechatQRLogin接口完成PC端登录确认
- 支持扫码取消静默处理,扫码失败则弹窗提示错误信息
- 优化门店中心页面布局,调整扫码按钮绝对定位及样式
- 优化了用户管理页面的小型样式,包括按钮和文本间距调整
- 调整用户页面部分Badge显示逻辑,排除tabIndex为4的项不显示角标
2026-07-17 12:38:27 +08:00
21b8403fab feat(order): 支持商家送货的配送方式展示
- 新增配送方式映射,支持快递配送、自提和商家送货三种类型
- 修改判断逻辑,将商家送货归类为快递配送类型以便正确渲染
- 根据配送方式不同显示相应图标,商家送货显示摩托车图标
- 商家送货时添加配送门店信息展示
- 订单详情增加配送方式字段的显示项
2026-07-17 12:10:38 +08:00
955b156399 refactor(user-profile): 调整个人信息页面布局优化结构
- 调整用户ID位置至头像之上以强化信息层次
- 统一并优化代码格式排版,提升代码可读性
- 合并重复的用户ID展示,避免冗余
- 保持功能一致,未改动交互逻辑
- 优化注释及空白行,代码风格更规范
2026-07-17 11:42:04 +08:00
ceba1cabec fix(store): 修复店员列表拉取逻辑,保证数据正确性
- 获取当前登录店员的 storeId,避免使用无效 storeId 查询
- 根据 storeId 拉取本门店店员列表,修复空列表问题
- 加载失败时提示“加载店员失败”
- 提示门店信息未就绪时的用户反馈
- 默认选中当前登录用户对应的店员项
2026-07-17 11:36:15 +08:00
01f0638a37 fix(store): 修复获取店员列表时缺少店铺ID参数问题
- 移除调用 listShopStoreUser 时的 storeId 参数
- 解决因参数传递导致的加载店员失败问题
- 保持加载和错误提示逻辑不变
2026-07-17 11:24:06 +08:00
197722adeb fix(orders): 修复确认完成按钮显示逻辑
- 调整确认完成按钮的注释缩进,提升代码可读性
- 保持确认完成按钮仅在已付款状态下显示
- 未改变按钮显示的实际逻辑行为
2026-07-17 04:01:38 +08:00
17 changed files with 1952 additions and 1643 deletions

View File

@@ -94,3 +94,62 @@
### 验证
- `npx taro build --type weapp` 构建成功
## 门店中心右上角扫码登录 PC 后台src/pages/store/center/index.tsx
- **需求**:门店中心顶部绿色渐变区右上角加一个二维码扫码图标,点击后调起微信扫码,扫码结果解析出 token 后调 `confirmWechatQRLogin` 确认 PC 端登录
- **参考**`/Users/gxwebsoft/VUE/template-10584/src/pages/user/components/UserCard.tsx` 中的 `UnifiedQRButton` 组件
- **实现**
- 复用已有 API`parseQRContent` + `confirmWechatQRLogin``@/api/passport/qr-login`),与 `src/components/QRLoginScanner.tsx` 同款
- 新增 `handleScanLogin` callback`Taro.scanCode``parseQRContent` → 校验 token/userId → `confirmWechatQRLogin` → 成功弹窗
- 右上角按钮用 `absolute top-3 right-4 z-20` 定位,半透明白色圆形背景 + base64 SVG 图标
- 小程序不支持 `<svg>` 标签,用 `Image` 组件 + `data:image/svg+xml;base64,...` data URI 渲染图标
- 用户取消扫码errMsg 含 'cancel')静默处理
## 修复未付款已发货订单状态显示错误
- **问题**订单列表卡片OrderCard和详情页detail.tsx状态判断时 `!payStatus`(待付款)优先于 `deliveryStatus` 判断,导致"未付款但已发货"(payStatus=false + deliveryStatus=20) 的订单显示"待付款"而非"待收货"
- **修改文件**
- `src/components/common/OrderCard/index.tsx``getCardStatus``deliveryStatus===20/30` 判断提前到 `!payStatus` 之前
- `src/pages/order/detail.tsx``getOrderDisplayStatus``getOrderPhase` 两个函数都做同样调整,物流状态优先于付款状态
- **影响范围**:仅影响"未付款但已发货"的异常场景,正常流程不受影响
### 状态描述对齐已发货待收款payType=9 + deliveryStatus=20 + 未付款)
- **需求**:线下付款(payType=9)已发货但未确认收款的订单,用户端应与门店端一致显示"已发货待收款"(红色),而非"待收货"(蓝色)
- **修改文件**
- `src/components/common/OrderCard/index.tsx``getCardStatus``deliveryStatus===20` 之前加 `!payStatus && payType===9 && deliveryStatus===20` → "已发货待收款"(红色)
- `src/pages/order/detail.tsx``getOrderDisplayStatus` 在物流状态判断前加 `isOffline && !payStatus && (deliveryStatus===20||30)` → "已发货待收款"红色subtitle: 商品已发货,请尽快转账付款)
- `src/pages/store/orders/index.tsx``getStatusText` 恢复 `payType===9 && deliveryStatus===20` → "已发货待收款"`getStatusColor` 对应红色
- **最终统一**:三端状态文案完全对齐
| 场景 | 用户端列表 | 用户端详情 | 门店端 |
|------|-----------|-----------|--------|
| 线下付款+已发货+未付款 | 已发货待收款(红) | 已发货待收款(红) | 已发货待收款(红) |
| 其他已发货 | 待收货(蓝) | 待收货(蓝) | 待收货(蓝) |
### 门店端:拆分"确认完成"为"送达"和"确认完成"两个操作
- **新增操作类型** `deliver`送达deliveryStatus=20已发货且无 sendEndImg 时显示,上传送达照片后按钮自动隐藏(用 sendEndImg 判断,不改变 deliveryStatus
- **修改** `complete`(确认完成):仅已付款才可操作,只标记 orderStatus=1已完成不需要上传凭证
- ⚠️ `deliveryStatus=30` 是"部分发货"含义,不能用来表示"已送达",所有相关判断已清除
- **按钮显示逻辑**
| 场景 | 送达 | 确认完成 | 确认收款 |
|------|:---:|:---:|:---:|
| 线下付款+未付款+已发货(无送达照片) | ✅ | ❌ | ✅ |
| 线下付款+未付款+已发货(有送达照片) | ❌ | ❌ | ✅ |
| 已付款+已发货(无送达照片) | ✅ | ✅ | ❌ |
| 已付款+已发货(有送达照片) | ❌ | ✅ | ❌ |
- **deliveryStatus 含义**10=待发货, 20=已发货, 30=部分发货(未使用)
- **状态文案**deliveryStatus=20+未付款+线下付款 → "已发货待收款"deliveryStatus=20+已付款 → "待收货"(蓝)
### 门店端:改价功能合并到确认收款弹窗 → 又改回来了
- 先去掉了独立的"改价"按钮和修改金额弹窗(`editPrice` OpType、`showEditPriceModal``editReason` 等)
- 在确认收款弹窗中增加了"实付金额"输入框
- **2026-07-17 下午**:用户要求改回来,恢复独立的改价按钮和弹窗
- OpType 加回 `editPrice`
- `getOrderActions` 中线下付款未付款时显示「改价」+「确认收款」两个按钮
- 恢复 `showEditPriceModal`/`editPriceOrder`/`editPriceValue`/`editPriceRemarks`/`editingPrice` 状态
- 恢复 `openEditPriceModal`/`closeEditPriceModal`/`submitEditPrice` 函数
- 恢复独立改价弹窗(订单信息 + 新金额输入 + 改价原因备注)
- 确认收款弹窗去掉「实付金额」输入框,`submitOperation` 中 confirmPay 分支去掉改价逻辑
- 改价按钮样式:橙色背景白字 `bg-orange-500`
### 门店订单页:付款凭证(paymentVoucher)未显示
- **问题**`renderOrderCard` 中只展示了 `sendEndImg`(配送/送达凭证),没有展示 `paymentVoucher`(付款凭证)。后端 `ShopOrder.paymentVoucher` 字段已正确保存和返回
- **修复**:在买家备注下方、送达凭证上方新增付款凭证展示区域(绿色背景 `bg-green-50`),仅当 `order.paymentVoucher` 有值时渲染,支持点击预览大图

View File

@@ -0,0 +1,7 @@
# 2026-07-23 工作日志
## 门店订单列表补充「下单时间」
- 文件:`src/pages/store/orders/index.tsx`
- 在订单卡片头部左侧新增「下单时间YYYY-MM-DD HH:mm」位于订单号下方状态标签仍在右侧`items-center``items-start` 适配两行)。
- 数据字段:复用 `ShopOrder.createTime`(创建时间=下单时间,接口本来就有返回,列表排序也用它,无需改后端)。
- 新增辅助函数 `formatOrderTime(raw)`:兼容 `2026-07-23T02:00:18``2026-07-23 02:00:18` 两种格式,仅保留到分钟。

View File

@@ -0,0 +1,29 @@
# 2026-08-01 工作记录
## 销量累加方式后台可配置功能
- 用户需求商品详情页缺销量字段实际是销量为0时隐藏、下单后销量不累计希望后台可配置「销量累加方式」默认支付累加、可选下单累加。
- 确认项1) 销量为0展示「销量0」2) 用户已加 `shop_order.sales_accumulated` 字段3) 取消订单回滚销量4) 退款不扣减销量本次预留结构不实现5) 列表页不显示已售0。
### 后端改动guilixu-java
- `ShopGoodsMapper.addSaleCount`:改为 `GREATEST(IFNULL(sales,0)+saleCount, 0)`,支持负数回滚且不为负。
- `ShopOrder` 实体新增 `salesAccumulated` 字段Integer用户已加表字段
- `ShopSettingService` 新增 `getSalesAccumulateType()`默认10=支付累加20=下单累加),读取 order 分类下 `salesAccumulateType`
- `ShopOrderServiceImpl.updateGoodsSales(order, rollback)`:改造支持幂等(按 salesAccumulated 标记)+ 回滚delta 取负)。`updateSingleGoodsSales` 接收 delta。
- `handlePaymentSuccess`:仅当配置=10 时才累加销量。
- 新增公开方法 `accumulateSalesOnOrderCreated(order)`(配置=20时下单累加`handlePaidSuccess(order)`(复用 handlePaymentSuccess供非微信支付路径触发`rollbackSalesOnCancel(order)`
- `OrderBusinessService.createOrder`:保存订单商品后调 `accumulateSalesOnOrderCreated`;货到付款、余额支付分支补调 `handlePaidSuccess`(修复此前非微信支付不累加销量的 bug
- `ShopOrderServiceImpl.confirmOfflinePayment`:确认线下收款成功后调 `handlePaymentSuccess`(覆盖线下付款场景)。
- `OrderCancelServiceImpl.cancelOrder`:已累加销量时调 `rollbackSalesOnCancel` 回滚。
### 前端改动xinlong-shop-taro
- `pages/shop/product-detail.tsx`:销量展示去掉 `>0` 判断,改为 `{product.sales || 0}` 始终显示。列表页(首页/分类/卡片保持原逻辑销量为0不显示
### 后台管理guilixu-admin
- `views/shop/shopSetting/components/order.vue`新增「销量累加方式」单选10支付/20下单默认值10load 解析 salesAccumulateType。保存复用 batchSaveShopSetting。
### 配置初始化
- 新增 `guilixu-java/sql/sales_accumulate_config.sql`可选代码默认已是10并补充 `UPDATE shop_goods SET sales=0 WHERE sales IS NULL`
- 注意:数据库字段名需与实体 `salesAccumulated` 对应(下划线 `sales_accumulated`MyBatis-Plus 默认映射)。
### 编译
- `mvn -o compile` 通过exit 0

View File

@@ -6,6 +6,8 @@
- 后端 API 前缀:`/shop/``/api/`
- **后端项目路径:`/Users/gxwebsoft/JAVA/guilixu-java`**(⚠️ 不要找 paopao-java 或 websopy-java那是旧的
- **后端 Maven 编译命令**`cd /Users/gxwebsoft/JAVA/guilixu-java && "/Applications/IntelliJ IDEA Ultimate.app/Contents/plugins/maven/lib/maven3/bin/mvn" compile`guilixu 的 mvnw 损坏,用 IntelliJ 自带的 mvn
- **API 域名配置**`config/env.js` 三环境一致):`API_BASE_URL=https://shop-api.websoft.top/api``SERVER_API_URL=https://server.websoft.top/api``CMS_API_URL=https://cms-api.websoft.top/api``config/app.ts` 导出 `BaseUrl/ServerBaseUrl/CmsBaseUrl`
- **幻灯片广告(轮播图)调用已切到 cms-api**`src/api/cms/cmsAd/index.ts` 的读接口(`listCmsAd`/`getCmsAd`/`getCmsAdByCode`/`pageCmsAd`)通过 `cmsUrl()` 拼接 `CmsBaseUrl`,首页轮播 `listCmsAd({ adType: 'banner', status: 0 })``https://cms-api.websoft.top/api/cms/cms-ad`;写接口仍走 shop-api
- 门店店员身份判断:通过 `getMyClerk()` API (`/shop/shop-store-user/my`) 返回值判断
- VIP/分销商申请记录表:`ShopDealerApply`,状态码 10待审核/20通过/30驳回

View File

@@ -0,0 +1,55 @@
2026年7月17日 下午改动问题总结
================================================
1. 门店订单页 — 付款凭证(paymentVoucher)未显示
文件: src/pages/store/orders/index.tsx
问题: renderOrderCard 中只渲染了送达凭证(sendEndImg),遗漏了付款凭证(paymentVoucher)
修复: 在买家备注下方、送达凭证上方新增付款凭证展示区(绿色背景),支持点击预览大图
2. 门店订单页 — 改价功能来回调整
文件: src/pages/store/orders/index.tsx
问题: 先把独立「改价」按钮和弹窗合并到「确认收款」弹窗中
修复: 下午用户要求改回来恢复独立的改价按钮橙色bg-orange-500和弹窗确认收款弹窗去掉实付金额输入框
3. 门店订单页 — 拆分"确认完成"为"送达"和"确认完成"
文件: src/pages/store/orders/index.tsx
问题: 新增deliver送达操作已发货但无送达照片时显示上传照片后按钮自动隐藏
修复: complete确认完成仅已付款可操作只标记orderStatus=1。清除误用deliveryStatus=30表示"已送达"的逻辑
4. 状态文案对齐 — "已发货待收款"
文件: src/pages/store/orders/index.tsx, src/components/common/OrderCard/index.tsx, src/pages/order/detail.tsx
问题: 线下付款(payType=9)+已发货+未付款 → 三端统一显示"已发货待收款"(红色)
修复: 其他已发货 → "待收货"(蓝色)
5. 修复未付款已发货订单状态显示错误
文件: src/components/common/OrderCard/index.tsx, src/pages/order/detail.tsx
问题: !payStatus判断优先于deliveryStatus导致"未付款但已发货"显示为"待付款"
修复: 物流状态判断提前到付款状态之前
6. 门店中心 — 右上角扫码登录PC后台
文件: src/pages/store/center/index.tsx
问题: 顶部渐变区右上角新增扫码图标,点击调起微信扫码 → 解析token → confirmWechatQRLogin确认PC端登录
7. 订单详情页 — "过期时间"改为"送达时间"
文件: src/pages/order/detail.tsx
问题: 信息区「过期时间」(expirationTime)改为「送达时间」(deliveryTime)
8. 新用户注册后首次加购购物车不刷新
文件: src/passport/login.tsx, src/passport/register.tsx, src/passport/sms-login.tsx, src/pages/shop/cart.tsx等
问题: 登录/注册后只存了storage未同步UserContext → isLoggedIn仍为false → 购物车不刷新
修复: 登录/注册后调loginUser()/syncFromStorage()同步React状态
9. 收货地址编辑不加载旧数据
文件: src/pages/user/address-edit.tsx, src/api/shop/shopUserAddress/index.ts
问题: API路径PUT /shop/shop-user-address/{id} → PUT /shop/shop-user-address后端无/{id}路径导致404
修复: setFormData改为merge模式防止null覆盖
10. 地图选择首次打开列表不显示
文件: src/pages/user/address-edit.tsx
问题: 鸿蒙手机首次打开chooseLocation时POI列表空白
修复: 先getLocation获取当前位置传入地图确保首次打开就在用户位置附近
11. 订单详情页金额明细0隐藏问题
文件: src/pages/order/detail.tsx
问题: {order.reducePrice && Number(order.reducePrice) > 0}当reducePrice为0时React渲染了文本"0"
修复: 改为{Number(order.reducePrice || 0) > 0 && (...)}

View File

@@ -1,7 +1,8 @@
import { API_BASE_URL, SERVER_API_URL } from './env'
import { API_BASE_URL, SERVER_API_URL, CMS_API_URL } from './env'
export const TenantId = '10611'
export const TenantName = '鑫龙家电'
export const BaseUrl = API_BASE_URL
export const ServerBaseUrl = SERVER_API_URL
export const CmsBaseUrl = CMS_API_URL
export const Version = 'v1.0.0'

View File

@@ -4,18 +4,21 @@ export const ENV_CONFIG = {
development: {
API_BASE_URL: 'https://shop-api.websoft.top/api',
SERVER_API_URL: 'https://server.websoft.top/api',
CMS_API_URL: 'https://cms-api.websoft.top/api',
APP_NAME: '鑫龙家电',
DEBUG: 'true',
},
test: {
API_BASE_URL: 'https://shop-api.websoft.top/api',
SERVER_API_URL: 'https://server.websoft.top/api',
CMS_API_URL: 'https://cms-api.websoft.top/api',
APP_NAME: '鑫龙家电',
DEBUG: 'true',
},
production: {
API_BASE_URL: 'https://shop-api.websoft.top/api',
SERVER_API_URL: 'https://server.websoft.top/api',
CMS_API_URL: 'https://cms-api.websoft.top/api',
APP_NAME: '鑫龙家电',
DEBUG: 'false',
},
@@ -25,4 +28,4 @@ export function getEnvConfig() {
return ENV_CONFIG[CURRENT_ENV] || ENV_CONFIG.development
}
export const { API_BASE_URL, SERVER_API_URL, APP_NAME, DEBUG } = getEnvConfig()
export const { API_BASE_URL, SERVER_API_URL, CMS_API_URL, APP_NAME, DEBUG } = getEnvConfig()

View File

@@ -1,6 +1,10 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsAd, CmsAdParam } from './model';
import { CmsBaseUrl } from '@/config/app';
// 幻灯片/广告位等 CMS 内容走独立的 cms-api 域名
const cmsUrl = (path: string) => `${CmsBaseUrl}${path}`;
/**
@@ -8,7 +12,7 @@ import type { CmsAd, CmsAdParam } from './model';
*/
export async function pageCmsAd(params: CmsAdParam) {
const res = await request.get<ApiResult<PageResult<CmsAd>>>(
'/cms/cms-ad/page',
cmsUrl('/cms/cms-ad/page'),
params
);
if (res.code === 0) {
@@ -22,7 +26,7 @@ export async function pageCmsAd(params: CmsAdParam) {
*/
export async function listCmsAd(params?: CmsAdParam) {
const res = await request.get<ApiResult<CmsAd[]>>(
'/cms/cms-ad',
cmsUrl('/cms/cms-ad'),
params
);
if (res.code === 0 && res.data) {
@@ -93,7 +97,7 @@ export async function removeBatchCmsAd(data: (number | undefined)[]) {
*/
export async function getCmsAd(id: number) {
const res = await request.get<ApiResult<CmsAd>>(
'/cms/cms-ad/' + id
cmsUrl('/cms/cms-ad/' + id)
);
if (res.code === 0 && res.data) {
return res.data;
@@ -106,7 +110,7 @@ export async function getCmsAd(id: number) {
*/
export async function getCmsAdByCode(code: string) {
const res = await request.get<ApiResult<CmsAd>>(
'/cms/cms-ad/getByCode/' + code
cmsUrl('/cms/cms-ad/getByCode/' + code)
);
if (res.code === 0 && res.data) {
return res.data;

View File

@@ -25,12 +25,16 @@ const getCardStatus = (order: ShopOrder): { text: string; color: string } => {
// 终态orderStatus=1 必须最先判断,盖过 deliveryStatus/payStatus
if (orderStatus === 1) return { text: '已完成', color: '#0e932e' }
// 线下付款(9)已发货但未确认收款:显示"已发货待收款"(红色),与门店端一致
if (!payStatus && payType === 9 && deliveryStatus === 20) return { text: '待付款,已发货', color: '#ee0a24' }
// 物流状态优先于付款状态(已发货的订单,即使未付款也显示物流状态)
if (deliveryStatus === 20) return { text: '待收货', color: '#4b9cf5' }
// 货到付款(8):下单时后端已 setPayStatus(true),不会走到这里
// 线下付款(9):保持 payStatus=false待商家确认收款应显示"待付款"与 Tab 一致
if (!payStatus && !isCod) return { text: '待付款', color: '#ee0a24' }
if (deliveryStatus === 10) return { text: '待发货', color: '#4b9cf5' }
if (deliveryStatus === 20) return { text: '待收货', color: '#4b9cf5' }
if (deliveryStatus === 30) return { text: '已收货', color: '#0e932e' }
return { text: '已付款', color: '#0e932e' }
}

View File

@@ -41,6 +41,13 @@ const DELIVERY_STATUS_MAP: Record<number, string> = {
30: '部分发货',
}
// 配送方式映射: 0=快递配送, 1=无需发货/自提, 2=商家送货
const DELIVERY_TYPE_MAP: Record<number, string> = {
0: '快递配送',
1: '自提',
2: '商家送货',
}
/** 根据订单状态计算展示用的状态标签 */
const getOrderDisplayStatus = (order: ShopOrder): { title: string; bgColor: string; subtitle: string } => {
const { payStatus, deliveryStatus, orderStatus, payType } = order
@@ -64,7 +71,17 @@ const getOrderDisplayStatus = (order: ShopOrder): { title: string; bgColor: stri
return { bgColor: '#ee0a24', title: '退款被拒绝', subtitle: '退款申请已被拒绝' }
}
// 正常订单流程
// 线下付款已发货但未确认收款:显示"已发货待收款"(红色),与门店端一致
if (isOffline && !payStatus && deliveryStatus === 20) {
return { bgColor: '#ee0a24', title: '已发货待收款', subtitle: '商品已发货,请尽快转账付款' }
}
// 物流状态优先于付款状态
if (deliveryStatus === 20) {
return { bgColor: '#4b9cf5', title: '待收货', subtitle: '商品运输中,请注意查收' }
}
// 未发货时按付款状态显示
if (!payStatus && !isCod && !isOffline) {
return { bgColor: '#ff7d00', title: '待付款', subtitle: '请尽快完成支付' }
}
@@ -77,9 +94,6 @@ const getOrderDisplayStatus = (order: ShopOrder): { title: string; bgColor: stri
if (deliveryStatus === 10) {
return { bgColor: '#4b9cf5', title: '待发货', subtitle: '商家正在准备商品' }
}
if (deliveryStatus === 20 || deliveryStatus === 30) {
return { bgColor: '#4b9cf5', title: '待收货', subtitle: '商品运输中,请注意查收' }
}
if (orderStatus === OrderStatus.Completed) {
return { bgColor: '#0e932e', title: '已完成', subtitle: '交易已完成,感谢购买' }
}
@@ -104,12 +118,14 @@ const getOrderPhase = (order: ShopOrder): OrderPhase => {
return orderStatus === OrderStatus.Cancelled || orderStatus === OrderStatus.RefundSuccess ? 'cancelled' : 'refund'
}
// 物流状态优先于付款状态(已发货的订单,即使未付款也进入 shipped 阶段)
if (deliveryStatus === 20) return 'shipped'
// 线下付款且未确认收款:等待商家确认,不显示"立即支付"按钮
if (isOffline && !payStatus) return 'offline_pending'
// 货到付款订单payStatus=true 但还未实际付款,直接进入 unshipped 阶段(不需要"立即支付"按钮)
if (!payStatus && !isCod) return 'unpaid'
if (deliveryStatus === 10) return 'unshipped'
if (deliveryStatus === 20 || deliveryStatus === 30) return 'shipped'
if (orderStatus === OrderStatus.Completed) return 'completed'
return 'unshipped'
@@ -245,7 +261,8 @@ const OrderDetailPage: React.FC = () => {
const displayStatus = getOrderDisplayStatus(order)
const phase = getOrderPhase(order)
const isExpress = order.deliveryType === 0 || order.deliveryType === undefined
// deliveryType: 0=快递配送, 1=无需发货/自提, 2=商家送货
const isExpress = order.deliveryType === 0 || order.deliveryType === 2 || order.deliveryType === undefined
const goodsCount = order.orderGoods?.reduce((sum, g) => sum + (g.totalNum || 1), 0) || 0
return (
@@ -274,13 +291,16 @@ const OrderDetailPage: React.FC = () => {
{isExpress ? (
<View className='bg-white mx-3 mt-3 p-3 rounded-lg'>
<View className='flex items-start gap-2'>
<Text className='text-base'>📦</Text>
<Text className='text-base'>{order.deliveryType === 2 ? '🛵' : '📦'}</Text>
<View className='flex-1'>
<View className='flex gap-2 mb-1'>
<Text className='text-sm font-medium text-gray-800'>{order.realName || '未知'}</Text>
<Text className='text-sm text-gray-600'>{order.mobile || order.phone || '-'}</Text>
</View>
<Text className='text-xs text-gray-500'>{order.address || '未填写收货地址'}</Text>
{order.deliveryType === 2 && order.expressMerchantName && (
<Text className='text-xs text-gray-400 mt-1'>: {order.expressMerchantName}</Text>
)}
{(order.sendStartTime || order.sendEndTime) && (
<Text className='text-xs text-gray-400 mt-1'>
: {order.sendStartTime?.slice(0, 10)} ~ {order.sendEndTime?.slice(0, 10)}
@@ -389,6 +409,10 @@ const OrderDetailPage: React.FC = () => {
<Text className='text-xs text-gray-400'></Text>
<Text className='text-xs text-gray-600'>{DELIVERY_STATUS_MAP[order.deliveryStatus ?? 10]}</Text>
</View>
<View className='flex justify-between'>
<Text className='text-xs text-gray-400'></Text>
<Text className='text-xs text-gray-600'>{DELIVERY_TYPE_MAP[order.deliveryType ?? 0] || '未知'}</Text>
</View>
<View className='flex justify-between'>
<Text className='text-xs text-gray-400'></Text>
<Text className='text-xs text-gray-600'>{formatTime(order.createTime)}</Text>

View File

@@ -327,9 +327,7 @@ const ProductDetailPage: React.FC = () => {
</View>
)}
<View className='flex gap-2 mt-2'>
{Number(product.sales) > 0 && (
<Text className='text-xs text-gray-400'>: {product.sales}</Text>
)}
<Text className='text-xs text-gray-400'>: {product.sales || 0}</Text>
<Text className='text-xs text-gray-400'>: {product.stock || 0}</Text>
{product.unitName && (
<Text className='text-xs text-gray-400'>: {product.unitName}</Text>

View File

@@ -1,254 +1,306 @@
import React, { useState, useEffect, useCallback } from 'react'
import { View, Text, Image } from '@tarojs/components'
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text, Image} from '@tarojs/components'
import Taro from '@tarojs/taro'
import { getMyClerk } from '@/api/shop/shopStoreUser'
import { listShopDealerApply } from '@/api/shop/shopDealerApply'
import { pageShopOrder } from '@/api/shop/shopOrder'
import {getMyClerk} from '@/api/shop/shopStoreUser'
import {listShopDealerApply} from '@/api/shop/shopDealerApply'
import {pageShopOrder} from '@/api/shop/shopOrder'
import {confirmWechatQRLogin, parseQRContent} from '@/api/passport/qr-login'
import Badge from '@/components/common/Badge'
// 二维码扫码图标base64 SVG data URI兼容小程序 Image 组件)
const QR_SCAN_ICON = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMyA3VjVhMiAyIDAgMCAxIDItMmgyTTE3IDNoMmEyIDIgMCAwIDEgMiAydjJNMjEgMTd2MmEyIDIgMCAwIDEtMiAyaC0yTTcgMjFINWEyIDIgMCAwIDEtMi0ydi0yIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiAvPjxyZWN0IHg9IjciIHk9IjciIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiAvPjxyZWN0IHg9IjEzIiB5PSI3IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgLz48cmVjdCB4PSI3IiB5PSIxMyIgd2lkdGg9IjQiIGhlaWdodD0iNCIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIC8+PHBhdGggZD0iTTEzIDEzaDJ2Mk0xNyAxM3YyTTE3IDE3djJNMTMgMTd2Mk0xMyAxOWgyIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiAvPjwvc3ZnPg=="
definePageConfig({
navigationBarTitleText: '门店中心',
navigationBarTitleText: '门店中心',
})
// 功能卡片定义(未来新增功能只需在这里加一项)
const FEATURE_CARDS = [
{
key: 'goods',
icon: '🏷️',
title: '商品管理',
desc: '管理门店商品上下架和库存',
url: '/pages/store/goods/index',
color: '#0891b2',
bgColor: '#cffafe',
showBadge: false,
},
{
key: 'orders',
icon: '📦',
title: '订单管理',
desc: '查看和处理门店订单',
url: '/pages/store/orders/index',
color: '#15803d',
bgColor: '#dcfce7',
// 显示待处理订单数量角标
showBadge: true,
},
{
key: 'users',
icon: '👥',
title: '用户管理',
desc: '查看用户信息、禁用/启用用户',
url: '/pages/store/users/index',
color: '#2563eb',
bgColor: '#dbeafe',
showBadge: false,
},
{
key: 'vip-review',
icon: '👑',
title: 'VIP会员审核',
desc: '审核客户VIP会员申请',
url: '/pages/user/vip-review/index',
color: '#7c3aed',
bgColor: '#ede9fe',
showBadge: true,
},
{
key: 'goods',
icon: '🏷️',
title: '商品管理',
desc: '管理门店商品上下架和库存',
url: '/pages/store/goods/index',
color: '#0891b2',
bgColor: '#cffafe',
showBadge: false,
},
{
key: 'orders',
icon: '📦',
title: '订单管理',
desc: '查看和处理门店订单',
url: '/pages/store/orders/index',
color: '#15803d',
bgColor: '#dcfce7',
// 显示待处理订单数量角标
showBadge: true,
},
{
key: 'users',
icon: '👥',
title: '用户管理',
desc: '查看用户信息、禁用/启用用户',
url: '/pages/store/users/index',
color: '#2563eb',
bgColor: '#dbeafe',
showBadge: false,
},
{
key: 'vip-review',
icon: '👑',
title: 'VIP会员审核',
desc: '审核客户VIP会员申请',
url: '/pages/user/vip-review/index',
color: '#7c3aed',
bgColor: '#ede9fe',
showBadge: true,
},
]
// 订阅消息模板 ID
const SUBSCRIBE_TMPL_IDS = [
'RpZaVfxTuNxRmUxvinAwk6FHTpjV9yTznShdy8OHiTk', // 交易提醒:订单号、商品名称、联系人、联系电话、送货地址
'RpZaVfxTuNxRmUxvinAwk6FHTpjV9yTznShdy8OHiTk', // 交易提醒:订单号、商品名称、联系人、联系电话、送货地址
]
export default function StoreCenterPage() {
const [storeInfo, setStoreInfo] = useState<any>(null)
const [pendingVipCount, setPendingVipCount] = useState(0)
const [pendingOrderCount, setPendingOrderCount] = useState(0)
const [loading, setLoading] = useState(true)
const [storeInfo, setStoreInfo] = useState<any>(null)
const [pendingVipCount, setPendingVipCount] = useState(0)
const [pendingOrderCount, setPendingOrderCount] = useState(0)
const [loading, setLoading] = useState(true)
useEffect(() => {
// 验证店员身份
getMyClerk()
.then(data => {
if (!data) {
Taro.showToast({ title: '仅门店店员可访问', icon: 'none' })
setTimeout(() => Taro.navigateBack(), 1500)
return
useEffect(() => {
// 验证店员身份
getMyClerk()
.then(data => {
if (!data) {
Taro.showToast({title: '仅门店店员可访问', icon: 'none'})
setTimeout(() => Taro.navigateBack(), 1500)
return
}
setStoreInfo(data)
// 加载待审核数量
loadBadgeCounts()
})
.catch(() => {
Taro.showToast({title: '仅门店店员可访问', icon: 'none'})
setTimeout(() => Taro.navigateBack(), 1500)
})
.finally(() => setLoading(false))
}, [])
/** 加载各类待处理角标数量 */
const loadBadgeCounts = async () => {
try {
// VIP 待审核
const vipData = await listShopDealerApply({applyStatus: 10})
setPendingVipCount((vipData || []).length)
} catch { /* ignore */
}
setStoreInfo(data)
// 加载待审核数量
loadBadgeCounts()
})
.catch(() => {
Taro.showToast({ title: '仅门店店员可访问', icon: 'none' })
setTimeout(() => Taro.navigateBack(), 1500)
})
.finally(() => setLoading(false))
}, [])
/** 加载各类待处理角标数量 */
const loadBadgeCounts = async () => {
try {
// VIP 待审核
const vipData = await listShopDealerApply({ applyStatus: 10 })
setPendingVipCount((vipData || []).length)
} catch { /* ignore */ }
try {
// 待处理订单:未付款/待发货/待收货
const orderData = await pageShopOrder({ statusFilter: 1, page: 1, limit: 1 })
setPendingOrderCount(orderData?.total || 0)
} catch { /* ignore */ }
}
/** 获取卡片角标数字 */
const getBadgeCount = (key: string) => {
if (key === 'orders') return pendingOrderCount
if (key === 'vip-review') return pendingVipCount
return 0
}
/** 请求订阅消息授权 */
const handleSubscribe = useCallback(() => {
if (SUBSCRIBE_TMPL_IDS.length === 0) {
Taro.showToast({ title: '暂无可订阅的消息模板', icon: 'none' })
return
try {
// 待处理订单:未付款/待发货/待收货
const orderData = await pageShopOrder({statusFilter: 1, page: 1, limit: 1})
setPendingOrderCount(orderData?.total || 0)
} catch { /* ignore */
}
}
Taro.requestSubscribeMessage({
tmplIds: SUBSCRIBE_TMPL_IDS,
success: (res) => {
// res[templateId] === 'accept' 表示用户同意订阅
const accepted = SUBSCRIBE_TMPL_IDS.filter(id => res[id] === 'accept')
if (accepted.length > 0) {
Taro.showToast({ title: '订阅成功', icon: 'success' })
/** 获取卡片角标数字 */
const getBadgeCount = (key: string) => {
if (key === 'orders') return pendingOrderCount
if (key === 'vip-review') return pendingVipCount
return 0
}
/** 扫码登录 PC 后台 */
const handleScanLogin = useCallback(async () => {
try {
Taro.showLoading({title: '请扫码...', mask: true})
const scanRes = await Taro.scanCode({onlyFromCamera: false, scanType: ['qrCode']})
Taro.hideLoading()
const rawContent = scanRes.result || ''
const token = parseQRContent(rawContent)
const userId = Number(Taro.getStorageSync('UserId'))
if (!token) {
Taro.showModal({title: '提示', content: '未识别到有效的登录二维码', showCancel: false})
return
}
if (!userId) {
Taro.showModal({title: '提示', content: '当前用户未登录', showCancel: false})
return
}
Taro.showLoading({title: '正在确认登录...', mask: true})
await confirmWechatQRLogin(token, userId)
Taro.hideLoading()
Taro.showModal({title: '登录成功', content: 'PC 端后台登录已确认,请返回网页端查看', showCancel: false})
} catch (err: any) {
Taro.hideLoading()
if (err?.errMsg?.includes('cancel')) {
return // 用户取消扫码,静默处理
}
Taro.showModal({title: '登录失败', content: err?.message || '扫码登录失败,请重试', showCancel: false})
}
},
fail: (err: any) => {
console.error('订阅失败:', err)
// errCode 20001: 模板ID不存在或未绑定到当前小程序
if (err.errCode === 20001) {
Taro.showModal({
title: '订阅模板未配置',
content: '请联系管理员在微信公众平台选用「新订单通知」订阅消息模板',
showCancel: false,
})
} else if (err.errCode === 20004 || err.errCode === 20002) {
// 用户拒绝/关闭了主开关,静默处理
Taro.showToast({ title: '已取消订阅', icon: 'none' })
}, [])
/** 请求订阅消息授权 */
const handleSubscribe = useCallback(() => {
if (SUBSCRIBE_TMPL_IDS.length === 0) {
Taro.showToast({title: '暂无可订阅的消息模板', icon: 'none'})
return
}
},
})
}, [])
Taro.requestSubscribeMessage({
tmplIds: SUBSCRIBE_TMPL_IDS,
success: (res) => {
// res[templateId] === 'accept' 表示用户同意订阅
const accepted = SUBSCRIBE_TMPL_IDS.filter(id => res[id] === 'accept')
if (accepted.length > 0) {
Taro.showToast({title: '订阅成功', icon: 'success'})
}
},
fail: (err: any) => {
console.error('订阅失败:', err)
// errCode 20001: 模板ID不存在或未绑定到当前小程序
if (err.errCode === 20001) {
Taro.showModal({
title: '订阅模板未配置',
content: '请联系管理员在微信公众平台选用「新订单通知」订阅消息模板',
showCancel: false,
})
} else if (err.errCode === 20004 || err.errCode === 20002) {
// 用户拒绝/关闭了主开关,静默处理
Taro.showToast({title: '已取消订阅', icon: 'none'})
}
},
})
}, [])
if (loading) {
return (
<View className='min-h-full bg-gray-50 flex items-center justify-center'>
<Text className='text-gray-400'>...</Text>
</View>
)
}
if (!storeInfo) return null
return (
<View className='min-h-full bg-gray-50'>
{/* 顶部信息区:店员头像 + 门店名称 */}
<View
className='pt-8 pb-12 px-5 rounded-b-3xl relative overflow-hidden'
style={{ background: 'linear-gradient(135deg, #15803d 0%, #22c55e 60%, #4ade80 100%)' }}
>
<View className='absolute -top-10 -right-10 w-40 h-40 rounded-full opacity-10'
style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} />
<View className='absolute -bottom-6 -left-6 w-24 h-24 rounded-full opacity-15'
style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} />
<View className='relative z-10 flex items-center gap-4'>
{/* 店员头像 */}
<View className='w-16 h-16 rounded-full bg-white/20 border-2 border-white/40 overflow-hidden flex-shrink-0'>
{storeInfo.avatar ? (
<Image
className='w-full h-full'
src={storeInfo.avatar}
mode='aspectFill'
/>
) : (
<View className='w-full h-full flex items-center justify-center'>
<Text className='text-2xl'>👤</Text>
</View>
)}
</View>
{/* 门店信息 */}
<View className='flex-1 min-w-0'>
<Text className='text-white text-lg font-bold block truncate'>
{storeInfo.storeName || '门店中心'}
</Text>
<Text className='text-white text-opacity-80 text-sm mt-1 block truncate'>
{storeInfo.name} {storeInfo.phone ? `· ${storeInfo.phone}` : ''}
</Text>
</View>
</View>
</View>
{/* 功能卡片区 */}
<View className='mx-3 -mt-6 relative z-20'>
<View className='flex flex-col gap-3'>
{FEATURE_CARDS.map(card => (
<View
key={card.key}
className='bg-white rounded-xl p-4 flex items-center gap-4 active:opacity-80 relative'
onClick={() => Taro.navigateTo({ url: card.url })}
>
{/* 图标 */}
<View
className='w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0'
style={{ background: card.bgColor }}
>
<Text className='text-2xl'>{card.icon}</Text>
</View>
{/* 文字 */}
<View className='flex-1 min-w-0'>
<Text className='text-gray-800 text-base font-medium block'>{card.title}</Text>
<Text className='text-gray-400 text-xs mt-0.5 block'>{card.desc}</Text>
</View>
{/* 待处理角标 */}
{card.showBadge && (
<Badge count={getBadgeCount(card.key)} className='absolute -top-1 -right-1' size={20} fontSize={11} fontWeight='bold' />
)}
{/* 箭头 */}
<Text className='text-gray-300 text-lg flex-shrink-0'></Text>
if (loading) {
return (
<View className='min-h-full bg-gray-50 flex items-center justify-center'>
<Text className='text-gray-400'>...</Text>
</View>
))}
</View>
</View>
)
}
{/* 底部提示 */}
<View className='mx-3 mt-6 mb-6'>
{/* 订阅消息提醒 */}
<View
className='bg-white rounded-xl p-4 flex items-center gap-3 active:opacity-80 mb-3'
onClick={handleSubscribe}
>
<View className='w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center flex-shrink-0'>
<Text className='text-xl'>🔔</Text>
</View>
<View className='flex-1 min-w-0'>
<Text className='text-gray-800 text-sm font-medium block'></Text>
<Text className='text-gray-400 text-xs mt-0.5 block'>
</Text>
</View>
<Text className='text-orange-500 text-sm flex-shrink-0'></Text>
</View>
if (!storeInfo) return null
{/*<Text className='text-gray-300 text-xs text-center block'>*/}
{/* 未来更多功能将持续上线*/}
{/*</Text>*/}
</View>
</View>
)
return (
<View className='min-h-full bg-gray-50'>
{/* 顶部信息区:店员头像 + 门店名称 */}
<View
className='pt-8 pb-12 px-5 rounded-b-3xl relative overflow-hidden'
style={{background: 'linear-gradient(135deg, #15803d 0%, #22c55e 60%, #4ade80 100%)'}}
>
<View className='absolute -top-10 -right-10 w-40 h-40 rounded-full opacity-10'
style={{background: 'radial-gradient(circle, #ffffff, transparent)'}}/>
<View className='absolute -bottom-6 -left-6 w-24 h-24 rounded-full opacity-15'
style={{background: 'radial-gradient(circle, #ffffff, transparent)'}}/>
{/* 右上角扫码登录按钮 */}
<View
className='absolute top-3 right-4 z-20 w-9 h-9 rounded-full bg-white/20 border border-white/30 flex items-center justify-center active:opacity-70'
onClick={handleScanLogin}
>
<Image src={QR_SCAN_ICON} className='w-5 h-5' mode='aspectFit'/>
</View>
<View className='relative z-10 flex items-center gap-4'>
{/* 店员头像 */}
<View
className='w-16 h-16 rounded-full bg-white/20 border-2 border-white/40 overflow-hidden flex-shrink-0'>
{storeInfo.avatar ? (
<Image
className='w-full h-full'
src={storeInfo.avatar}
mode='aspectFill'
/>
) : (
<View className='w-full h-full flex items-center justify-center'>
<Text className='text-2xl'>👤</Text>
</View>
)}
</View>
{/* 门店信息 */}
<View className='flex-1 min-w-0'>
<Text className='text-white text-lg font-bold block truncate'>
{storeInfo.storeName || '门店中心'}
</Text>
<Text className='text-white text-opacity-80 text-sm mt-1 block truncate'>
{storeInfo.name} {storeInfo.phone ? `· ${storeInfo.phone}` : ''}
</Text>
</View>
</View>
</View>
{/* 功能卡片区 */}
<View className='mx-3 -mt-6 relative z-20'>
<View className='flex flex-col gap-3'>
{FEATURE_CARDS.map(card => (
<View
key={card.key}
className='bg-white rounded-xl p-4 flex items-center gap-4 active:opacity-80 relative'
onClick={() => Taro.navigateTo({url: card.url})}
>
{/* 图标 */}
<View
className='w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0'
style={{background: card.bgColor}}
>
<Text className='text-2xl'>{card.icon}</Text>
</View>
{/* 文字 */}
<View className='flex-1 min-w-0'>
<Text className='text-gray-800 text-base font-medium block'>{card.title}</Text>
<Text className='text-gray-400 text-xs mt-0.5 block'>{card.desc}</Text>
</View>
{/* 待处理角标 */}
{card.showBadge && (
<Badge count={getBadgeCount(card.key)} className='absolute -top-1 -right-1' size={20}
fontSize={11} fontWeight='bold'/>
)}
{/* 箭头 */}
<Text className='text-gray-300 text-lg flex-shrink-0'></Text>
</View>
))}
</View>
</View>
{/* 底部提示 */}
<View className='mx-3 mt-3 mb-6'>
{/* 订阅消息提醒 */}
<View
className='bg-white rounded-xl p-4 flex items-center gap-3 active:opacity-80 mb-3'
onClick={handleSubscribe}
>
<View
className='w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center flex-shrink-0'>
<Text className='text-xl'>🔔</Text>
</View>
<View className='flex-1 min-w-0'>
<Text className='text-gray-800 text-sm font-medium block'></Text>
<Text className='text-gray-400 text-xs mt-0.5 block'>
</Text>
</View>
<Text className='text-orange-500 text-sm flex-shrink-0'></Text>
</View>
<Text className='text-gray-300 text-xs text-center block'>
线
</Text>
<View className={'h-4'}></View>
</View>
</View>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -28,20 +28,22 @@ const TABS: { key: TabKey; label: string; params: Partial<ShopOrderParam>[] }[]
]
// ─── 操作类型 ─────────────────────────────────────────────────────
type OpType = 'confirmPay' | 'complete' | 'editPrice' | 'ship'
type OpType = 'confirmPay' | 'deliver' | 'complete' | 'ship' | 'editPrice'
const OP_LABEL: Record<OpType, string> = {
confirmPay: '收款',
deliver: '送达',
complete: '已完成',
editPrice: '金额',
ship: '发货',
editPrice: '改价',
}
const OP_DESC: Record<OpType, string> = {
confirmPay: '确认已收到线下付款,订单将进入待发货状态',
complete: '同时变更支付状态为已付款、收货状态为已收货、订单状态为已完成',
editPrice: '修改订单实付金额',
confirmPay: '确认已收到线下付款',
deliver: '上传送达凭证照片,标记为已收货(不改变付款和订单完成状态)',
complete: '确认订单已完成,收款后点击此按钮标记订单完成',
ship: '选择发货人员并生成发货单,订单将进入已发货状态',
editPrice: '修改订单实付金额',
}
// ─── 图片上传 ─────────────────────────────────────────────────────
@@ -82,6 +84,17 @@ function formatDateTime(date: Date): string {
return `${y}-${m}-${d} ${h}:${mi}:${s}`
}
/** 格式化下单时间为「YYYY-MM-DD HH:mm」兼容 "2026-07-23T02:00:18" 与 "2026-07-23 02:00:18" */
function formatOrderTime(raw?: string): string {
if (!raw) return ''
const s = raw.replace('T', ' ').trim()
const parts = s.split(' ')
if (parts.length < 2) return s
const timeParts = parts[1].split(':')
const hhmm = `${timeParts[0]}:${timeParts[1] || '00'}`
return `${parts[0]} ${hhmm}`
}
/** 解析 sendEndImg兼容 JSON 数组(新格式)和逗号分隔(旧格式) */
function parseSendEndImg(raw: string): string[] {
if (!raw || !raw.trim()) return []
@@ -100,20 +113,25 @@ function parseSendEndImg(raw: string): string[] {
function getStatusText(order: ShopOrder): string {
if (order.orderStatus === 1) return '已完成'
if (order.orderStatus === 2) return '已关闭'
// 线下付款·已发货待收款(先发货后结款)
if (order.payType === 9 && order.deliveryStatus === 20) return '已发货待收款'
if (order.payStatus === false || order.payStatus === null) {
// 线下付款待确认收款
if (order.payType === 9) return '待确认收款'
return '待收款'
// 线下付款待确认收款
if (order.payType === 9) return '待确认收款'
return '待收款'
}
if (order.payStatus && order.deliveryStatus === 10) return '待发货'
if (order.payStatus && order.deliveryStatus === 20) return '待收货'
if (order.payStatus && order.deliveryStatus === 10) return '待发货'
return '进行中'
}
function getStatusColor(order: ShopOrder): string {
if (order.orderStatus === 1) return '#0e932e'
if (order.orderStatus === 2) return '#999'
// 已发货待收款:红色(与用户端一致)
if (order.payType === 9 && order.deliveryStatus === 20) return '#ee0a24'
if (order.payStatus === false || order.payStatus === null) return '#ee0a24'
if (order.deliveryStatus === 20) return '#4b9cf5'
if (order.payStatus) return '#ff7d00'
return '#999'
}
@@ -127,22 +145,30 @@ function isOrderActionable(order: ShopOrder): boolean {
function getOrderActions(order: ShopOrder): { label: string; type: OpType }[] {
if (!isOrderActionable(order)) return []
const actions: { label: string; type: OpType }[] = []
// 修改金额:仅在确认收款显示(未付款时才可改价)
if (!order.payStatus) {
actions.push({label: '修改金额', type: 'editPrice'})
}
// 线下付款·待确认收款:显示"确认收款"按钮
// 线下付款·待确认收款显示"确认收款"和"改价"按钮
if (!order.payStatus && order.payType === 9 && order.orderStatus === 0) {
actions.push({label: '改价', type: 'editPrice'})
actions.push({label: '确认收款', type: 'confirmPay'})
}
// 确认完成:仅在确认收款后显示(已付款才能确认完成)
// 发货:
// - 已付款且未发货:正常发货
// - 线下付款未付款且未发货:支持先发货后结款
if (order.orderStatus !== 1) {
const notShipped = order.deliveryStatus == null || order.deliveryStatus < 20
if (notShipped && (order.payStatus || order.payType === 9)) {
actions.push({label: '发货', type: 'ship'})
}
}
// 送达已发货但未上传送达照片时显示deliveryStatus=20 且 sendEndImg 为空)
const shipped = order.deliveryStatus != null && order.deliveryStatus === 20
const hasDeliverProof = order.sendEndImg && order.sendEndImg !== '[]'
if (shipped && !hasDeliverProof) {
actions.push({label: '送达', type: 'deliver'})
}
// 确认完成:仅已付款才可标记订单为已完成
if (order.payStatus) {
actions.push({label: '确认完成', type: 'complete'})
}
// 发货确认收款后、且尚未发货deliveryStatus<20时显示
if (order.payStatus && (order.deliveryStatus == null || order.deliveryStatus < 20) && order.orderStatus !== 1) {
actions.push({label: '发货', type: 'ship'})
}
return actions
}
@@ -164,10 +190,12 @@ export default function StoreOrdersPage() {
const [payRemarks, setPayRemarks] = useState('')
const [submitting, setSubmitting] = useState(false)
// 修改金额弹窗状态
// 改价弹窗状态
const [showEditPriceModal, setShowEditPriceModal] = useState(false)
const [editPayPrice, setEditPayPrice] = useState('')
const [editReason, setEditReason] = useState('')
const [editPriceOrder, setEditPriceOrder] = useState<ShopOrder | null>(null)
const [editPriceValue, setEditPriceValue] = useState('')
const [editPriceRemarks, setEditPriceRemarks] = useState('')
const [editingPrice, setEditingPrice] = useState(false)
// 发货弹窗状态
const [showShipModal, setShowShipModal] = useState(false)
@@ -299,6 +327,11 @@ export default function StoreOrdersPage() {
/** 打开操作弹窗 */
const openModal = (order: ShopOrder, type: OpType) => {
// 改价走独立弹窗
if (type === 'editPrice') {
openEditPriceModal(order)
return
}
setCurrentOrder(order)
setOpType(type)
setProofImages([])
@@ -314,57 +347,6 @@ export default function StoreOrdersPage() {
setPayRemarks('')
}
/** 打开修改金额弹窗 */
const openEditPriceModal = (order: ShopOrder) => {
setCurrentOrder(order)
setEditPayPrice(String(order.payPrice || order.totalPrice || ''))
setEditReason('')
setShowEditPriceModal(true)
}
/** 关闭修改金额弹窗 */
const closeEditPriceModal = () => {
setShowEditPriceModal(false)
setCurrentOrder(null)
setEditPayPrice('')
setEditReason('')
}
/** 提交修改金额 */
const submitEditPrice = async () => {
if (!currentOrder) return
const newPrice = parseFloat(editPayPrice)
if (isNaN(newPrice) || newPrice < 0) {
Taro.showToast({title: '请输入有效的金额', icon: 'none'})
return
}
if (!editReason.trim()) {
Taro.showToast({title: '请输入修改原因', icon: 'none'})
return
}
setSubmitting(true)
try {
const oldPrice = currentOrder.payPrice || currentOrder.totalPrice || '0'
const changeRecord = `【门店修改金额】原实付¥${oldPrice} → 新实付¥${newPrice.toFixed(2)},原因:${editReason.trim()}`
await updateShopOrder({
orderId: currentOrder.orderId,
payPrice: newPrice.toFixed(2),
merchantRemarks: (currentOrder.merchantRemarks || '') + `\n${changeRecord}`,
} as ShopOrder)
Taro.showToast({title: '修改成功', icon: 'success'})
closeEditPriceModal()
loadOrders(activeTab, 1)
} catch (e: any) {
Taro.showToast({title: e.message || '修改失败', icon: 'none'})
} finally {
setSubmitting(false)
}
}
/** 选择并上传凭证图片 */
const chooseProofImage = async () => {
const maxCount = opType === 'confirmPay' ? 1 : 3
@@ -407,9 +389,9 @@ export default function StoreOrdersPage() {
const submitOperation = async () => {
if (!currentOrder) return
// 确认完成必须上传凭证照片
if (opType === 'complete' && proofImages.length === 0) {
Taro.showToast({title: '请上传送凭证照片', icon: 'none'})
// 确认送达必须上传凭证照片
if (opType === 'deliver' && proofImages.length === 0) {
Taro.showToast({title: '请上传送凭证照片', icon: 'none'})
return
}
@@ -422,7 +404,7 @@ export default function StoreOrdersPage() {
setSubmitting(true)
try {
if (opType === 'confirmPay') {
// 确认线下收款:调用专用接口
// 确认线下收款
await confirmOfflinePayment(
currentOrder.orderId!,
payRemarks.trim() || undefined,
@@ -431,14 +413,13 @@ export default function StoreOrdersPage() {
} else {
const updateData: any = {orderId: currentOrder.orderId}
if (opType === 'complete') {
// 确认完成:用户已收到货
updateData.payStatus = true
updateData.payTime = formatDateTime(new Date())
updateData.deliveryStatus = 20 // 发货状态 → 已收货
updateData.deliveryTime = formatDateTime(new Date()) // 收货时间
if (opType === 'deliver') {
// 送达上传送达照片不改变付款状态、deliveryStatus 和订单完成状态)
updateData.deliveryTime = formatDateTime(new Date())
updateData.sendEndImg = JSON.stringify(proofImages)
} else if (opType === 'complete') {
// 确认完成:标记订单为已完成
updateData.orderStatus = 1 // 订单状态 → 已完成
updateData.sendEndImg = JSON.stringify(proofImages) // 配送员送达拍照JSON数组
}
await updateShopOrder(updateData)
@@ -454,6 +435,54 @@ export default function StoreOrdersPage() {
}
}
/** 打开改价弹窗 */
const openEditPriceModal = (order: ShopOrder) => {
setEditPriceOrder(order)
setEditPriceValue(String(order.payPrice || order.totalPrice || ''))
setEditPriceRemarks('')
setShowEditPriceModal(true)
}
/** 关闭改价弹窗 */
const closeEditPriceModal = () => {
setShowEditPriceModal(false)
setEditPriceOrder(null)
setEditPriceValue('')
setEditPriceRemarks('')
}
/** 提交改价 */
const submitEditPrice = async () => {
if (!editPriceOrder) return
const newPrice = parseFloat(editPriceValue)
if (isNaN(newPrice) || newPrice < 0) {
Taro.showToast({title: '请输入有效金额', icon: 'none'})
return
}
const oldPriceStr = String(editPriceOrder.payPrice || editPriceOrder.totalPrice || '0')
const oldPrice = parseFloat(oldPriceStr)
if (!isNaN(oldPrice) && Math.abs(newPrice - oldPrice) < 0.01) {
Taro.showToast({title: '金额未变化', icon: 'none'})
return
}
setEditingPrice(true)
try {
const changeRecord = `【门店修改金额】原实付¥${oldPriceStr} → 新实付¥${newPrice.toFixed(2)}`
await updateShopOrder({
orderId: editPriceOrder.orderId,
payPrice: newPrice.toFixed(2),
merchantRemarks: (editPriceOrder.merchantRemarks || '') + `\n${changeRecord}`,
} as ShopOrder)
Taro.showToast({title: '改价成功', icon: 'success'})
closeEditPriceModal()
loadOrders(activeTab, 1)
} catch (e: any) {
Taro.showToast({title: e.message || '改价失败', icon: 'none'})
} finally {
setEditingPrice(false)
}
}
/** 打开发货弹窗:拉取门店店员列表供选择发货人员 */
const openShipModal = async (order: ShopOrder) => {
setCurrentOrder(order)
@@ -462,8 +491,21 @@ export default function StoreOrdersPage() {
setShowShipModal(true)
setLoadingClerks(true)
try {
const res = await listShopStoreUser({storeId: order.storeId})
// 获取当前登录店员(含 storeId / userId
// 用 storeId 拉取本门店店员列表,避免 ?storeId=0 拉不到数据
const myClerk = await getMyClerk()
const storeId = myClerk?.storeId
if (!storeId) {
Taro.showToast({title: '门店信息未就绪,请稍后再试', icon: 'none'})
return
}
const res = await listShopStoreUser({storeId})
setClerkList(res || [])
// 默认选中与当前登录用户 userId 一致的店员
if (res && myClerk?.userId) {
const match = res.find(c => c.userId === myClerk!.userId)
if (match) setSelectedClerkId(match.id ?? null)
}
} catch (e) {
Taro.showToast({title: '加载店员失败', icon: 'none'})
} finally {
@@ -554,8 +596,13 @@ export default function StoreOrdersPage() {
return (
<View key={order.orderId} className='bg-white rounded-xl mx-3 mt-3 p-4'>
{/* 订单头部 */}
<View className='flex justify-between items-center mb-3'>
<Text className='text-xs text-gray-400'>{order.orderNo}</Text>
<View className='flex justify-between items-start mb-3'>
<View className='flex flex-col'>
<Text className='text-xs text-gray-400'>{order.orderNo}</Text>
{order.createTime ? (
<Text className='text-xs text-gray-400 mt-1'>{formatOrderTime(order.createTime)}</Text>
) : null}
</View>
<Text className='text-xs' style={{color: getStatusColor(order)}}>
{getStatusText(order)}
</Text>
@@ -608,6 +655,22 @@ export default function StoreOrdersPage() {
</View>
)}
{/* 付款凭证(线下付款确认收款后显示) */}
{order.paymentVoucher && (
<View className='bg-green-50 rounded-lg p-3 mb-3'>
<Text className='text-xs text-green-500 mb-2 block'></Text>
<Image
className='w-20 h-20 rounded-lg bg-gray-100'
src={getCompressedImageUrl(order.paymentVoucher)}
mode='aspectFill'
onClick={() => Taro.previewImage({
current: order.paymentVoucher!,
urls: [order.paymentVoucher!]
})}
/>
</View>
)}
{/* 送达凭证(已完成订单) */}
{order.sendEndImg && (() => {
const imgs = parseSendEndImg(order.sendEndImg)
@@ -665,20 +728,18 @@ export default function StoreOrdersPage() {
? 'bg-green-500'
: act.type === 'confirmPay'
? 'bg-blue-500'
: act.type === 'editPrice'
? 'bg-orange-500'
: act.type === 'ship'
? 'bg-purple-500'
: act.type === 'editPrice'
? 'bg-orange-500'
: 'border border-blue-500'
}`}
onClick={() => act.type === 'editPrice'
? openEditPriceModal(order)
: act.type === 'ship'
? openShipModal(order)
: openModal(order, act.type)}
onClick={() => act.type === 'ship'
? openShipModal(order)
: openModal(order, act.type)}
>
<Text className={`text-sm ${
act.type === 'complete' || act.type === 'editPrice' || act.type === 'confirmPay' || act.type === 'ship' ? 'text-white' : 'text-blue-500'
act.type === 'complete' || act.type === 'confirmPay' || act.type === 'ship' || act.type === 'editPrice' ? 'text-white' : 'text-blue-500'
}`}>{act.label}</Text>
</View>
))}
@@ -807,35 +868,39 @@ export default function StoreOrdersPage() {
</View>
)}
{/* 凭证上传 */}
<Text className='text-sm text-gray-600 mb-3 block'>
{opType === 'confirmPay'
? '上传支付凭证(必填,如微信转账截图)'
: opType === 'complete'
? '上传凭证照片必填配送货物到达后拍照上传最多3张'
: '上传凭证照片选填最多3张'}
</Text>
<View className='flex flex-wrap gap-3 mb-4'>
{proofImages.map((url, idx) => (
<View key={idx} className='relative'>
<Image className='w-20 h-20 rounded-lg bg-gray-50' src={getCompressedImageUrl(url)} mode='aspectFill'/>
<View
className='absolute -top-2 -right-2 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center'
onClick={() => removeProofImage(idx)}
>
<Text className='text-white text-xs'>×</Text>
</View>
{/* 凭证上传(确认完成不需要凭证) */}
{opType !== 'complete' && (
<>
<Text className='text-sm text-gray-600 mb-3 block'>
{opType === 'confirmPay'
? '上传支付凭证(必填,如微信转账截图'
: opType === 'deliver'
? '上传送达凭证必填配送货物到达后拍照上传最多3张'
: '上传凭证照片选填最多3张'}
</Text>
<View className='flex flex-wrap gap-3 mb-4'>
{proofImages.map((url, idx) => (
<View key={idx} className='relative'>
<Image className='w-20 h-20 rounded-lg bg-gray-50' src={getCompressedImageUrl(url)} mode='aspectFill'/>
<View
className='absolute -top-2 -right-2 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center'
onClick={() => removeProofImage(idx)}
>
<Text className='text-white text-xs'>×</Text>
</View>
</View>
))}
{proofImages.length < (opType === 'confirmPay' ? 1 : 3) && (
<View
className='w-20 h-20 rounded-lg bg-gray-50 border-2 border-dashed border-gray-200 flex items-center justify-center'
onClick={chooseProofImage}
>
<Text className='text-2xl text-gray-300'>+</Text>
</View>
)}
</View>
))}
{proofImages.length < (opType === 'confirmPay' ? 1 : 3) && (
<View
className='w-20 h-20 rounded-lg bg-gray-50 border-2 border-dashed border-gray-200 flex items-center justify-center'
onClick={chooseProofImage}
>
<Text className='text-2xl text-gray-300'>+</Text>
</View>
)}
</View>
</>
)}
{/* 备注(仅确认收款时显示) */}
{opType === 'confirmPay' && (
@@ -877,70 +942,6 @@ export default function StoreOrdersPage() {
</View>
)}
{/* 修改金额弹窗 */}
{showEditPriceModal && currentOrder && (
<View className='fixed inset-0 z-50 flex items-end justify-center'>
{/* 遮罩 */}
<View className='absolute inset-0 bg-black/50' onClick={closeEditPriceModal}/>
{/* 弹窗内容 */}
<View className='relative bg-white rounded-t-2xl w-full px-5 pt-6 pb-10'>
<Text className='text-lg font-medium text-gray-800 text-center mb-5 block'>
</Text>
{/* 订单信息 */}
<View className='bg-gray-50 rounded-xl p-4 mb-5'>
<Text className='text-sm text-gray-700 block'>{currentOrder.orderNo}</Text>
<Text className='text-sm text-gray-700 mt-1 block'>
<Text className='text-red-500 font-medium'>¥{currentOrder.payPrice || currentOrder.totalPrice}</Text>
</Text>
</View>
{/* 新金额输入 */}
<View className='mb-5'>
<Text className='text-sm text-gray-600 mb-2 block'></Text>
<Input
type='digit'
className='w-full px-4 py-3 rounded-xl border border-gray-200 text-lg text-gray-800'
value={editPayPrice}
onInput={(e) => setEditPayPrice(e.detail.value)}
placeholder='请输入新的实付金额'
/>
</View>
{/* 修改原因 */}
<View className='mb-6'>
<Text className='text-sm text-gray-600 mb-2 block'></Text>
<Textarea
className='w-full px-4 py-3 rounded-xl border border-gray-200 text-sm text-gray-800'
style={{minHeight: '60px'}}
value={editReason}
onInput={(e) => setEditReason(e.detail.value)}
placeholder='请输入修改原因,如:商品缺货调整、协商降价等'
maxlength={100}
/>
</View>
{/* 操作按钮 */}
<View className='flex gap-3'>
<View className='flex-1 py-3 rounded-xl bg-gray-100 text-center' onClick={closeEditPriceModal}>
<Text className='text-sm text-gray-600'></Text>
</View>
<View
className='flex-1 py-3 rounded-xl bg-orange-500 text-center flex items-center justify-center'
onClick={submitting ? undefined : submitEditPrice}
>
{submitting ? (
<Text className='text-sm text-white'>...</Text>
) : (
<Text className='text-sm text-white'></Text>
)}
</View>
</View>
</View>
</View>
)}
{/* 发货弹窗:选择发货人员 */}
{showShipModal && currentOrder && (
<View className='fixed inset-0 z-50 flex items-end justify-center'>
@@ -1019,6 +1020,68 @@ export default function StoreOrdersPage() {
</View>
</View>
)}
{/* 改价弹窗 */}
{showEditPriceModal && editPriceOrder && (
<View className='fixed inset-0 z-50 flex items-end justify-center'>
<View className='absolute inset-0 bg-black/50' onClick={closeEditPriceModal}/>
<View className='relative bg-white rounded-t-2xl w-full px-5 pt-6 pb-10'>
<Text className='text-lg font-medium text-gray-800 text-center mb-5 block'>
</Text>
{/* 订单信息 */}
<View className='bg-gray-50 rounded-xl p-4 mb-5'>
<Text className='text-sm text-gray-700 block'>{editPriceOrder.orderNo}</Text>
<Text className='text-sm text-gray-700 mt-1 block'>
<Text className='text-red-500 font-medium'>¥{editPriceOrder.payPrice || editPriceOrder.totalPrice}</Text>
</Text>
</View>
{/* 新金额输入 */}
<View className='mb-6'>
<Text className='text-sm text-gray-600 mb-2 block'></Text>
<Input
type='digit'
className='w-full px-4 py-3 rounded-xl border border-gray-200 text-lg text-gray-800'
value={editPriceValue}
onInput={(e) => setEditPriceValue(e.detail.value)}
placeholder='请输入新的实付金额'
/>
</View>
{/* 改价备注 */}
<View className='mb-6'>
<Text className='text-sm text-gray-600 mb-2 block'></Text>
<Textarea
className='w-full px-4 py-3 rounded-xl border border-gray-200 text-sm text-gray-800'
style={{minHeight: '60px'}}
value={editPriceRemarks}
onInput={(e) => setEditPriceRemarks(e.detail.value)}
placeholder='如:客户协商优惠、多收退款等'
maxlength={200}
/>
</View>
{/* 操作按钮 */}
<View className='flex gap-3'>
<View className='flex-1 py-3 rounded-xl bg-gray-100 text-center' onClick={closeEditPriceModal}>
<Text className='text-sm text-gray-600'></Text>
</View>
<View
className='flex-1 py-3 rounded-xl bg-orange-500 text-center flex items-center justify-center'
onClick={editingPrice ? undefined : submitEditPrice}
>
{editingPrice ? (
<Text className='text-sm text-white'>...</Text>
) : (
<Text className='text-sm text-white'></Text>
)}
</View>
</View>
</View>
</View>
)}
</View>
)
}

View File

@@ -183,7 +183,7 @@ export default function StoreUsersPage() {
{displayName}
</Text>
<View
className='px-1.5 py-0.5 rounded text-[10px]'
className='px-1 py-1 rounded text-xs'
style={{
color: isDisabled ? '#dc2626' : '#16a34a',
background: isDisabled ? '#fef2f2' : '#f0fdf4',
@@ -192,14 +192,14 @@ export default function StoreUsersPage() {
{isDisabled ? '已禁用' : '正常'}
</View>
</View>
<Text className='text-xs text-gray-400 mt-0.5 block'>
<Text className='text-xs text-gray-400 mt-1 block'>
{user.phone || user.mobile || '未绑定手机'}
</Text>
</View>
{/* 禁用/启用按钮 */}
<View
className={`px-3 py-1.5 rounded-lg flex-shrink-0 ${isDisabled ? 'bg-green-50' : 'bg-red-50'}`}
className={`px-3 py-1 rounded-lg flex-shrink-0 ${isDisabled ? 'bg-green-50' : 'bg-red-50'}`}
onClick={(e) => {
e.stopPropagation()
handleToggleStatus(user)
@@ -215,17 +215,17 @@ export default function StoreUsersPage() {
<View className='flex items-center gap-4 mt-3 pt-3 border-t border-gray-50'>
{user.memberLevelName && (
<View className='flex items-center gap-1'>
<Text className='text-[10px] text-gray-400'>:</Text>
<Text className='text-xs text-gray-400'>:</Text>
<Text className='text-xs text-purple-500'>{user.memberLevelName}</Text>
</View>
)}
<View className='flex items-center gap-1'>
<Text className='text-[10px] text-gray-400'>:</Text>
<Text className='text-xs text-gray-400'>:</Text>
<Text className='text-xs text-gray-500'>{formatTime(user.createTime)}</Text>
</View>
{user.balance !== undefined && Number(user.balance) > 0 && (
<View className='flex items-center gap-1'>
<Text className='text-[10px] text-gray-400'>:</Text>
<Text className='text-xs text-gray-400'>:</Text>
<Text className='text-xs text-orange-500'>¥{user.balance}</Text>
</View>
)}
@@ -238,7 +238,7 @@ export default function StoreUsersPage() {
<View className='min-h-full bg-gray-50'>
{/* 搜索栏 */}
<View className='bg-white px-3 py-2 flex items-center gap-2'>
<View className='flex-1 flex items-center bg-gray-100 rounded-lg px-3 py-1.5'>
<View className='flex-1 flex items-center bg-gray-100 rounded-lg px-3 py-1'>
<Input
className='flex-1 text-sm'
placeholder='搜索昵称 / 手机号'
@@ -255,7 +255,7 @@ export default function StoreUsersPage() {
) : null}
</View>
<View
className='px-3 py-1.5 rounded-lg bg-blue-500'
className='px-3 py-1 rounded-lg bg-blue-500'
onClick={handleSearch}
>
<Text className='text-sm text-white'></Text>
@@ -342,7 +342,7 @@ export default function StoreUsersPage() {
{detailUser.nickname || detailUser.realName || '未设置昵称'}
</Text>
<View
className='px-2 py-0.5 rounded text-xs'
className='px-2 py-1 rounded text-xs'
style={{
color: detailUser.status === 1 ? '#dc2626' : '#16a34a',
background: detailUser.status === 1 ? '#fef2f2' : '#f0fdf4',

View File

@@ -1,229 +1,207 @@
import React, { useState, useEffect } from 'react'
import { View, Text, Image, Input, Button as TaroButton } from '@tarojs/components'
import React, {useState, useEffect} from 'react'
import {View, Text, Image, Input, Button as TaroButton} from '@tarojs/components'
import Taro from '@tarojs/taro'
import BottomButton from '@/components/common/BottomButton'
import { useUser } from '@/hooks/useUser'
import { updateUser } from '@/api/system/user'
import type { User } from '@/api/system/user/model'
import { TenantId } from '@/config/app'
import {useUser} from '@/hooks/useUser'
import {updateUser} from '@/api/system/user'
import type {User} from '@/api/system/user/model'
import {TenantId} from '@/config/app'
definePageConfig({
navigationBarTitleText: '个人信息',
navigationBarTitleText: '个人信息',
})
const genderOptions = ['保密', '男', '女']
const ProfilePage: React.FC = () => {
const { user, refreshUser } = useUser()
const [form, setForm] = useState<Partial<User>>({})
const [loading, setLoading] = useState(false)
const [showGenderSheet, setShowGenderSheet] = useState(false)
const {user, refreshUser} = useUser()
const [form, setForm] = useState<Partial<User>>({})
const [loading, setLoading] = useState(false)
const [showGenderSheet, setShowGenderSheet] = useState(false)
useEffect(() => {
if (user) {
setForm({
userId: (user as any)?.userId || (user as any)?.id,
nickname: (user as any)?.nickname || '',
avatar: (user as any)?.avatar || '',
gender: (user as any)?.gender ?? 0,
phone: (user as any)?.phone || '',
merchantName: (user as any)?.merchantName || '',
address: (user as any)?.address || '',
})
}
}, [user])
// 微信头像选择回调open-type="chooseAvatar"
const handleWechatAvatar = (e: any) => {
const { avatarUrl } = e.detail
if (!avatarUrl) return
Taro.showLoading({ title: '上传中...' })
Taro.uploadFile({
url: 'https://server.websoft.top/api/oss/upload',
filePath: avatarUrl,
name: 'file',
header: {
'content-type': 'application/json',
TenantId,
},
success: (uploadRes) => {
const data = JSON.parse(uploadRes.data)
if (data.code === 0 && data.data?.url) {
setForm(prev => ({ ...prev, avatar: data.data.url }))
Taro.showToast({ title: '上传成功', icon: 'success' })
} else {
Taro.showToast({ title: data.message || '上传失败', icon: 'none' })
useEffect(() => {
if (user) {
setForm({
userId: (user as any)?.userId || (user as any)?.id,
nickname: (user as any)?.nickname || '',
avatar: (user as any)?.avatar || '',
gender: (user as any)?.gender ?? 0,
phone: (user as any)?.phone || '',
merchantName: (user as any)?.merchantName || '',
address: (user as any)?.address || '',
})
}
},
fail: () => {
Taro.showToast({ title: '上传失败', icon: 'none' })
},
complete: () => {
Taro.hideLoading()
},
})
}
}, [user])
// 选择性别
const handleGenderSelect = (index: number) => {
setForm(prev => ({ ...prev, gender: index }))
setShowGenderSheet(false)
}
// 保存
const handleSave = async () => {
if (!form.nickname?.trim()) {
Taro.showToast({ title: '昵称不能为空', icon: 'none' })
return
// 微信头像选择回调open-type="chooseAvatar"
const handleWechatAvatar = (e: any) => {
const {avatarUrl} = e.detail
if (!avatarUrl) return
Taro.showLoading({title: '上传中...'})
Taro.uploadFile({
url: 'https://server.websoft.top/api/oss/upload',
filePath: avatarUrl,
name: 'file',
header: {
'content-type': 'application/json',
TenantId,
},
success: (uploadRes) => {
const data = JSON.parse(uploadRes.data)
if (data.code === 0 && data.data?.url) {
setForm(prev => ({...prev, avatar: data.data.url}))
Taro.showToast({title: '上传成功', icon: 'success'})
} else {
Taro.showToast({title: data.message || '上传失败', icon: 'none'})
}
},
fail: () => {
Taro.showToast({title: '上传失败', icon: 'none'})
},
complete: () => {
Taro.hideLoading()
},
})
}
setLoading(true)
try {
await updateUser(form as User)
await refreshUser()
Taro.showToast({ title: '保存成功', icon: 'success' })
} catch (error) {
console.error('保存失败:', error)
Taro.showToast({ title: '保存失败', icon: 'none' })
} finally {
setLoading(false)
// 选择性别
const handleGenderSelect = (index: number) => {
setForm(prev => ({...prev, gender: index}))
setShowGenderSheet(false)
}
}
const displayId = (user as any)?.userId || (user as any)?.id || ''
const registerTime = (user as any)?.createTime || (user as any)?.createdAt || ''
// 保存
const handleSave = async () => {
if (!form.nickname?.trim()) {
Taro.showToast({title: '昵称不能为空', icon: 'none'})
return
}
setLoading(true)
try {
await updateUser(form as User)
await refreshUser()
Taro.showToast({title: '保存成功', icon: 'success'})
} catch (error) {
console.error('保存失败:', error)
Taro.showToast({title: '保存失败', icon: 'none'})
} finally {
setLoading(false)
}
}
return (
<View className='min-h-screen bg-gray-50 pb-32'>
{/* 头像 - 使用 Taro 原生 Button 支持 open-type="chooseAvatar" */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<View className='flex items-center gap-2'>
<TaroButton
openType='chooseAvatar'
onChooseAvatar={handleWechatAvatar}
plain
style={{
padding: 0,
margin: 0,
background: 'transparent',
border: 'none',
lineHeight: '40px',
width: 'auto',
minHeight: 0,
}}
>
{form.avatar ? (
<Image className='w-10 h-10 rounded-full' src={form.avatar} mode='aspectFill' />
) : (
<View className='w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center'>
<Text className='text-gray-300 text-xs'></Text>
</View>
const displayId = (user as any)?.userId || (user as any)?.id || ''
const registerTime = (user as any)?.createTime || (user as any)?.createdAt || ''
return (
<View className='min-h-screen bg-gray-50 pb-32'>
{/* 用户ID */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'>ID</Text>
<Text className='text-sm text-gray-600'>{displayId || '未设置'}</Text>
</View>
{/* 头像 - 使用 Taro 原生 Button 支持 open-type="chooseAvatar" */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<View className='flex items-center gap-2'>
<TaroButton
openType='chooseAvatar'
onChooseAvatar={handleWechatAvatar}
plain
style={{
padding: 0,
margin: 0,
background: 'transparent',
border: 'none',
lineHeight: '40px',
width: 'auto',
minHeight: 0,
}}
>
{form.avatar ? (
<Image className='w-10 h-10 rounded-full' src={form.avatar} mode='aspectFill'/>
) : (
<View className='w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center'>
<Text className='text-gray-300 text-xs'></Text>
</View>
)}
</TaroButton>
<Text className='text-gray-300 text-sm'>{'>'}</Text>
</View>
</View>
{/* 昵称 - 支持获取微信昵称 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<Input
className='flex-1 text-right text-sm text-gray-600'
placeholder='请输入昵称'
value={form.nickname || ''}
onInput={(e: any) => setForm(prev => ({...prev, nickname: e.detail.value}))}
maxlength={20}
type='nickname'
/>
</View>
{/* 性别 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'
onClick={() => setShowGenderSheet(true)}>
<Text className='text-sm text-gray-700 w-20'></Text>
<View className='flex items-center gap-2'>
<Text className='text-sm text-gray-600'>{genderOptions[form.gender ?? 0]}</Text>
<Text className='text-gray-300 text-sm'>{'>'}</Text>
</View>
</View>
{/* 手机号 - 不可编辑 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<Text className='text-sm text-gray-600'>{form.phone || '未绑定'}</Text>
</View>
{/* 注册时间 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<Text className='text-sm text-gray-600'>{registerTime || '未知'}</Text>
</View>
{/* 保存按钮 */}
<BottomButton
text='保存'
onClick={handleSave}
loading={loading}
disabled={loading}
/>
{/* 性别选择弹窗 */}
{showGenderSheet && (
<View className='fixed inset-0 z-50'>
<View className='absolute inset-0 bg-black bg-opacity-50'
onClick={() => setShowGenderSheet(false)}/>
<View className='absolute bottom-0 left-0 right-0 bg-white rounded-t-xl'>
<View className='px-4 py-3 border-b border-gray-50'>
<Text className='text-base font-medium text-gray-800'></Text>
</View>
{genderOptions.map((label, index) => (
<View
key={index}
className='px-4 py-3 border-b border-gray-50 flex items-center justify-between'
onClick={() => handleGenderSelect(index)}
>
<Text
className={`text-sm ${(form.gender ?? 0) === index ? 'text-green-600 font-medium' : 'text-gray-700'}`}>
{label}
</Text>
{(form.gender ?? 0) === index && <Text className='text-green-600'></Text>}
</View>
))}
<View className='px-4 py-3' onClick={() => setShowGenderSheet(false)}>
<Text className='text-sm text-gray-400 text-center block'></Text>
</View>
</View>
</View>
)}
</TaroButton>
<Text className='text-gray-300 text-sm'>{'>'}</Text>
</View>
</View>
{/* 昵称 - 支持获取微信昵称 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<Input
className='flex-1 text-right text-sm text-gray-600'
placeholder='请输入昵称'
value={form.nickname || ''}
onInput={(e: any) => setForm(prev => ({ ...prev, nickname: e.detail.value }))}
maxlength={20}
type='nickname'
/>
</View>
{/* 性别 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50' onClick={() => setShowGenderSheet(true)}>
<Text className='text-sm text-gray-700 w-20'></Text>
<View className='flex items-center gap-2'>
<Text className='text-sm text-gray-600'>{genderOptions[form.gender ?? 0]}</Text>
<Text className='text-gray-300 text-sm'>{'>'}</Text>
</View>
</View>
{/* 手机号 - 不可编辑 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<Text className='text-sm text-gray-600'>{form.phone || '未绑定'}</Text>
</View>
{/* 用户ID */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'>ID</Text>
<Text className='text-sm text-gray-600'>{displayId || '未设置'}</Text>
</View>
{/* 注册时间 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<Text className='text-sm text-gray-600'>{registerTime || '未知'}</Text>
</View>
{/* 门店名称 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<Input
className='flex-1 text-right text-sm text-gray-600'
placeholder='请输入门店名称'
value={(form as any)?.merchantName || ''}
onInput={(e: any) => setForm(prev => ({ ...prev, merchantName: e.detail.value }))}
maxlength={50}
/>
</View>
{/* 门店地址 */}
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
<Text className='text-sm text-gray-700 w-20'></Text>
<Input
className='flex-1 text-right text-sm text-gray-600'
placeholder='请输入门店地址'
value={(form as any)?.address || ''}
onInput={(e: any) => setForm(prev => ({ ...prev, address: e.detail.value }))}
maxlength={100}
/>
</View>
{/* 保存按钮 */}
<BottomButton
text='保存'
onClick={handleSave}
loading={loading}
disabled={loading}
/>
{/* 性别选择弹窗 */}
{showGenderSheet && (
<View className='fixed inset-0 z-50'>
<View className='absolute inset-0 bg-black bg-opacity-50' onClick={() => setShowGenderSheet(false)} />
<View className='absolute bottom-0 left-0 right-0 bg-white rounded-t-xl'>
<View className='px-4 py-3 border-b border-gray-50'>
<Text className='text-base font-medium text-gray-800'></Text>
</View>
{genderOptions.map((label, index) => (
<View
key={index}
className='px-4 py-3 border-b border-gray-50 flex items-center justify-between'
onClick={() => handleGenderSelect(index)}
>
<Text className={`text-sm ${(form.gender ?? 0) === index ? 'text-green-600 font-medium' : 'text-gray-700'}`}>
{label}
</Text>
{(form.gender ?? 0) === index && <Text className='text-green-600'></Text>}
</View>
))}
<View className='px-4 py-3' onClick={() => setShowGenderSheet(false)}>
<Text className='text-sm text-gray-400 text-center block'></Text>
</View>
</View>
</View>
)}
</View>
)
)
}
export default ProfilePage

View File

@@ -281,7 +281,7 @@ const UserPage: React.FC = () => {
<Text className='text-xl mb-1'>{item.icon}</Text>
<Text className='text-xs text-gray-600'>{item.label}</Text>
{/* 数量角标count 为 0 时组件内部不渲染 */}
<Badge count={item.count} className='absolute -top-1 -right-1' />
{item.tabIndex != 4 && <Badge count={item.count} className='absolute -top-1 -right-1' />}
</View>
))}
</View>