Compare commits
17 Commits
7071ec408e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f532b3923d | |||
| f1c0349f64 | |||
| 0cc0d089e9 | |||
| c6e022f815 | |||
| 0d0c4b327a | |||
| 7ff921d973 | |||
| 69862bfc0c | |||
| 184763a15c | |||
| 147802dbe9 | |||
| 37cea1b6d0 | |||
| 65bcd07ca7 | |||
| bd3589af61 | |||
| 21b8403fab | |||
| 955b156399 | |||
| ceba1cabec | |||
| 01f0638a37 | |||
| 197722adeb |
@@ -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` 有值时渲染,支持点击预览大图
|
||||
|
||||
7
.workbuddy/memory/2026-07-23.md
Normal file
7
.workbuddy/memory/2026-07-23.md
Normal 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` 两种格式,仅保留到分钟。
|
||||
29
.workbuddy/memory/2026-08-01.md
Normal file
29
.workbuddy/memory/2026-08-01.md
Normal 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下单),默认值10,load 解析 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)。
|
||||
@@ -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驳回
|
||||
|
||||
|
||||
55
2026-07-17- 功能修复点.txt
Normal file
55
2026-07-17- 功能修复点.txt
Normal 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 && (...)}
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user