diff --git a/.vercel/project.json b/.vercel/project.json
index 752ae2e..a4ff8d7 100644
--- a/.vercel/project.json
+++ b/.vercel/project.json
@@ -1 +1 @@
-{"projectName":"trae_template-10550_mhk8"}
\ No newline at end of file
+{"projectName":"trae_template-10584_mhk8"}
diff --git a/config/app.ts b/config/app.ts
index 7d73acb..8256046 100644
--- a/config/app.ts
+++ b/config/app.ts
@@ -1,12 +1,14 @@
import { API_BASE_URL } from './env'
// 租户ID - 请根据实际情况修改
-export const TenantId = '10550';
+export const TenantId = '10584';
+// 租户名称
+export const TenantName = '桂乐淘';
// 接口地址 - 请根据实际情况修改
export const BaseUrl = API_BASE_URL;
// 当前版本
export const Version = 'v3.0.8';
// 版权信息
-export const Copyright = 'WebSoft Inc.';
+export const Copyright = '桂乐淘·购享无界 乐惠万家';
// java -jar CertificateDownloader.jar -k 0kF5OlPr482EZwtn9zGufUcqa7ovgxRL -m 1723321338 -f ./apiclient_key.pem -s 2B933F7C35014A1C363642623E4A62364B34C4EB -o ./
diff --git a/config/env.ts b/config/env.ts
index 152eae3..4ec1704 100644
--- a/config/env.ts
+++ b/config/env.ts
@@ -2,20 +2,21 @@
export const ENV_CONFIG = {
// 开发环境
development: {
- API_BASE_URL: 'http://127.0.0.1:9200/api',
- // API_BASE_URL: 'https://cms-api.websoft.top/api',
+ // API_BASE_URL: 'http://127.0.0.1:9200/api',
+ API_BASE_URL: 'https://glt-api.websoft.top/api',
APP_NAME: '开发环境',
DEBUG: 'true',
},
// 生产环境
production: {
- API_BASE_URL: 'https://cms-api.websoft.top/api',
- APP_NAME: '时里院子市集',
+ API_BASE_URL: 'https://glt-api.websoft.top/api',
+ APP_NAME: '桂乐淘',
DEBUG: 'false',
},
// 测试环境
test: {
- API_BASE_URL: 'https://cms-api.s209.websoft.top/api',
+ // API_BASE_URL: 'http://127.0.0.1:9200/api',
+ API_BASE_URL: 'https://glt-api.websoft.top/api',
APP_NAME: '测试环境',
DEBUG: 'true',
}
diff --git a/docs/DEALER_OPTIMIZATION.md b/docs/DEALER_OPTIMIZATION.md
index d6f384a..b52633c 100644
--- a/docs/DEALER_OPTIMIZATION.md
+++ b/docs/DEALER_OPTIMIZATION.md
@@ -20,7 +20,7 @@
#### 新增功能
- 用户头像和基本信息展示
-- 佣金统计(可提现、冻结中、累计收益)
+- 佣金统计(可提现、待使用、累计收益)
- 团队统计(一级、二级、三级成员)
- 功能导航网格(分销订单、提现申请、我的团队、推广二维码)
diff --git a/docs/GRADIENT_DESIGN_GUIDE.md b/docs/GRADIENT_DESIGN_GUIDE.md
index 926cbf6..e362e6e 100644
--- a/docs/GRADIENT_DESIGN_GUIDE.md
+++ b/docs/GRADIENT_DESIGN_GUIDE.md
@@ -35,7 +35,7 @@ dealer: {
// 金额相关
money: {
available: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', // 可提现 - 绿色
- frozen: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', // 冻结中 - 蓝色
+ frozen: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', // 待使用 - 蓝色
total: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)' // 累计 - 橙色
}
```
diff --git a/docs/IMPLICIT_ANY_TYPE_FIX.md b/docs/IMPLICIT_ANY_TYPE_FIX.md
index c8720f6..f63abda 100644
--- a/docs/IMPLICIT_ANY_TYPE_FIX.md
+++ b/docs/IMPLICIT_ANY_TYPE_FIX.md
@@ -90,7 +90,7 @@ const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?:
success: function () {
if (code) {
Taro.request({
- url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
+ url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
code,
diff --git a/docs/水票配送订单-后端提示词.md b/docs/水票配送订单-后端提示词.md
new file mode 100644
index 0000000..c064ea9
--- /dev/null
+++ b/docs/水票配送订单-后端提示词.md
@@ -0,0 +1,41 @@
+# 水票配送订单:后端提示词(可直接发给后端)
+
+## 1) 订单查询(配送员端)
+请在 `GET /glt/glt-ticket-order/page` 支持以下筛选,并保证权限隔离:
+- `riderId`:只返回该配送员的订单(必要)
+- `deliveryStatus`:10待配送、20配送中、30待客户确认、40已完成(必要)
+- 排序:建议 `sendTime asc` + `createTime desc`(或给前端一个可控排序字段)
+
+## 2) 配送流程字段(建议后端落库并回传)
+订单表建议确保有以下字段(当前前端已按这些字段做流程判断/展示):
+- `riderId/riderName/riderPhone`:配送员信息
+- `deliveryStatus`:10/20/30/40
+- `sendStartTime`:配送员点击“开始配送”的时间
+- `sendEndTime`:配送员点击“确认送达”的时间
+- `sendEndImg`:送达拍照留档图片 URL(可选/必填由后端策略决定)
+- `receiveConfirmTime`:客户确认收货时间
+- `receiveConfirmType`:10客户手动确认、20配送照片自动确认、30超时自动确认
+
+## 3) 状态流转与校验(强烈建议在后端做)
+请在更新订单时做状态机校验,避免前端绕过流程:
+- `10 -> 20`:仅允许订单属于当前配送员,且未开始/未送达
+- `20 -> 30`:配送员确认送达(可带 `sendEndImg`)
+- `20/30 -> 40`:完成;来源可能是
+ - 客户手动确认(写 `receiveConfirmTime` + `receiveConfirmType=10`)
+ - 配送照片直接完成(写 `receiveConfirmTime` + `receiveConfirmType=20`,并要求 `sendEndImg`)
+ - 超时自动确认(写 `receiveConfirmTime` + `receiveConfirmType=30`,建议由定时任务执行)
+
+## 4) 建议新增/明确的接口能力
+为了避免并发抢单/越权更新,建议新增更语义化的接口(或在 update 内做等价校验):
+- 接单(抢单/派单):`POST /glt/glt-ticket-order/{id}/accept`
+ - 后端原子校验:仅当 `riderId is null` 才能写入当前 rider 信息
+- 开始配送:`POST /glt/glt-ticket-order/{id}/start`(写 `sendStartTime` + `deliveryStatus=20`)
+- 确认送达:`POST /glt/glt-ticket-order/{id}/delivered`(写 `sendEndTime` + `deliveryStatus=30` + 可选 `sendEndImg`)
+- 客户确认收货:`POST /glt/glt-ticket-order/{id}/confirm-receive`
+ - 校验:只能本人 `userId` 操作,且必须已送达
+
+## 5) 为了“导航到收货地址/取货点”的字段补充(建议)
+当前仅有 `address` 字符串,无法在小程序内 `openLocation` 精准导航;建议补充:
+- 收货地址:`receiverName`、`receiverPhone`、`province/city/district/detail`、`latitude/longitude`
+- 取货点(门店/仓库):`storeLatitude/storeLongitude` 或 `warehouseLatitude/warehouseLongitude`
+
diff --git a/package.json b/package.json
index e57ea99..cb6d20f 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "template-10550",
+ "name": "template-10584",
"version": "1.0.0",
"private": true,
"description": "WebSoft Inc.",
diff --git a/project.config.json b/project.config.json
index 33ad7f0..443f70e 100644
--- a/project.config.json
+++ b/project.config.json
@@ -1,8 +1,8 @@
{
"miniprogramRoot": "dist/",
- "projectname": "template-10550",
- "description": "时里院子市集",
- "appid": "wx5170f9f17a813877",
+ "projectname": "template-10584",
+ "description": "桂乐淘",
+ "appid": "wxad831ba00ad6a026",
"setting": {
"urlCheck": true,
"es6": false,
diff --git a/project.tt.json b/project.tt.json
index 71d3700..986f0e8 100644
--- a/project.tt.json
+++ b/project.tt.json
@@ -1,7 +1,7 @@
{
"miniprogramRoot": "./",
"projectname": "mp-react",
- "description": "时里院子市集",
+ "description": "桂乐淘",
"appid": "touristappid",
"setting": {
"urlCheck": true,
diff --git a/src/admin/components/UserCard.tsx b/src/admin/components/UserCard.tsx
index a83f124..97d808e 100644
--- a/src/admin/components/UserCard.tsx
+++ b/src/admin/components/UserCard.tsx
@@ -144,7 +144,7 @@ function UserCard() {
success: function () {
if (code) {
Taro.request({
- url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
+ url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
code,
@@ -237,7 +237,7 @@ function UserCard() {
navTo('/user/gift/index', true)}>
- 礼品卡
+ 水票
{giftCount}
{/**/}
diff --git a/src/api/glt/gltTicketOrder/index.ts b/src/api/glt/gltTicketOrder/index.ts
new file mode 100644
index 0000000..46693fb
--- /dev/null
+++ b/src/api/glt/gltTicketOrder/index.ts
@@ -0,0 +1,101 @@
+import request from '@/utils/request';
+import type { ApiResult, PageResult } from '@/api/index';
+import type { GltTicketOrder, GltTicketOrderParam } from './model';
+
+/**
+ * 分页查询送水订单
+ */
+export async function pageGltTicketOrder(params: GltTicketOrderParam) {
+ const res = await request.get
>>(
+ '/glt/glt-ticket-order/page',
+ params
+ );
+ if (res.code === 0) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 查询送水订单列表
+ */
+export async function listGltTicketOrder(params?: GltTicketOrderParam) {
+ const res = await request.get>(
+ '/glt/glt-ticket-order',
+ params
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 添加送水订单
+ */
+export async function addGltTicketOrder(data: GltTicketOrder) {
+ const res = await request.post>(
+ '/glt/glt-ticket-order',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 修改送水订单
+ */
+export async function updateGltTicketOrder(data: GltTicketOrder) {
+ const res = await request.put>(
+ '/glt/glt-ticket-order',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 删除送水订单
+ */
+export async function removeGltTicketOrder(id?: number) {
+ const res = await request.del>(
+ '/glt/glt-ticket-order/' + id
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 批量删除送水订单
+ */
+export async function removeBatchGltTicketOrder(data: (number | undefined)[]) {
+ const res = await request.del>(
+ '/glt/glt-ticket-order/batch',
+ {
+ data
+ }
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 根据id查询送水订单
+ */
+export async function getGltTicketOrder(id: number) {
+ const res = await request.get>(
+ '/glt/glt-ticket-order/' + id
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
diff --git a/src/api/glt/gltTicketOrder/model/index.ts b/src/api/glt/gltTicketOrder/model/index.ts
new file mode 100644
index 0000000..6e4b75a
--- /dev/null
+++ b/src/api/glt/gltTicketOrder/model/index.ts
@@ -0,0 +1,94 @@
+import type { PageParam } from '@/api';
+
+/**
+ * 送水订单
+ */
+export interface GltTicketOrder {
+ //
+ id?: number;
+ // 用户水票ID
+ userTicketId?: number;
+ // 订单编号
+ orderNo?: string;
+ // 门店ID
+ storeId?: number;
+ // 门店名称
+ storeName?: string;
+ // 门店地址
+ storeAddress?: string;
+ // 门店电话
+ storePhone?: string;
+ // 配送员
+ riderId?: number;
+ // 配送员名称
+ riderName?: string;
+ // 配送员电话
+ riderPhone?: string;
+ // 仓库ID
+ warehouseId?: number;
+ // 仓库名称
+ warehouseName?: string;
+ // 仓库地址
+ warehouseAddress?: string;
+ // 关联收货地址
+ addressId?: number;
+ // 收货地址
+ address?: string;
+ // 配送时间
+ sendTime?: string;
+ // 配送开始时间(配送员点击“开始配送”)
+ sendStartTime?: string;
+ // 配送结束时间(配送员确认送达)
+ sendEndTime?: string;
+ // 配送员送达拍照(选填/必填由后端策略决定)
+ sendEndImg?: string;
+ // 发货/配送状态(建议:10待配送 20配送中 30待客户确认 40已完成)
+ deliveryStatus?: number;
+ // 客户确认收货时间(客户点击确认收货)
+ receiveConfirmTime?: string;
+ // 客户确认方式(建议:10客户手动确认 20配送照片自动确认 30后台超时自动确认)
+ receiveConfirmType?: number;
+ // 买家留言
+ buyerRemarks?: string;
+ // 用于统计
+ price?: string;
+ // 购买数量
+ totalNum?: number;
+ // 用户ID
+ userId?: number;
+ // 昵称
+ nickname?: string;
+ // 头像
+ avatar?: string;
+ // 手机号码
+ phone?: string;
+ // 排序(数字越小越靠前)
+ sortNumber?: number;
+ // 备注
+ comments?: string;
+ // 状态, 0正常, 1冻结
+ status?: number;
+ // 是否删除, 0否, 1是
+ deleted?: number;
+ // 租户id
+ tenantId?: number;
+ // 创建时间
+ createTime?: string;
+ // 修改时间
+ updateTime?: string;
+}
+
+/**
+ * 送水订单搜索条件
+ */
+export interface GltTicketOrderParam extends PageParam {
+ id?: number;
+ keywords?: string;
+ userId?: number;
+ // 配送员用户ID(用于配送员端查询)
+ riderId?: number;
+ // 发货/配送状态(建议与 GltTicketOrder.deliveryStatus 对齐)
+ deliveryStatus?: number;
+ // 兼容 ShopOrderParam 的筛选字段(如后端已实现可直接复用)
+ statusFilter?: number;
+}
diff --git a/src/api/glt/gltTicketTemplate/index.ts b/src/api/glt/gltTicketTemplate/index.ts
new file mode 100644
index 0000000..f145300
--- /dev/null
+++ b/src/api/glt/gltTicketTemplate/index.ts
@@ -0,0 +1,118 @@
+import request from '@/utils/request';
+import type { ApiResult, PageResult } from '@/api';
+import type { GltTicketTemplate, GltTicketTemplateParam } from './model';
+
+/**
+ * 分页查询水票
+ */
+export async function pageGltTicketTemplate(params: GltTicketTemplateParam) {
+ const res = await request.get>>(
+ '/glt/glt-ticket-template/page',
+ {
+ params
+ }
+ );
+ if (res.code === 0) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 查询水票列表
+ */
+export async function listGltTicketTemplate(params?: GltTicketTemplateParam) {
+ const res = await request.get>(
+ '/glt/glt-ticket-template',
+ {
+ params
+ }
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 添加水票
+ */
+export async function addGltTicketTemplate(data: GltTicketTemplate) {
+ const res = await request.post>(
+ '/glt/glt-ticket-template',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 修改水票
+ */
+export async function updateGltTicketTemplate(data: GltTicketTemplate) {
+ const res = await request.put>(
+ '/glt/glt-ticket-template',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 删除水票
+ */
+export async function removeGltTicketTemplate(id?: number) {
+ const res = await request.del>(
+ '/glt/glt-ticket-template/' + id
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 批量删除水票
+ */
+export async function removeBatchGltTicketTemplate(data: (number | undefined)[]) {
+ const res = await request.del>(
+ '/glt/glt-ticket-template/batch',
+ {
+ data
+ }
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 根据id查询水票
+ */
+export async function getGltTicketTemplate(id: number) {
+ const res = await request.get>(
+ '/glt/glt-ticket-template/' + id
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 根据商品ID查询水票模板
+ */
+export async function getGltTicketTemplateByGoodsId(id: number) {
+ const res = await request.get>(
+ '/glt/glt-ticket-template/getByGoodsId/' + id
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
diff --git a/src/api/glt/gltTicketTemplate/model/index.ts b/src/api/glt/gltTicketTemplate/model/index.ts
new file mode 100644
index 0000000..0e18f2a
--- /dev/null
+++ b/src/api/glt/gltTicketTemplate/model/index.ts
@@ -0,0 +1,55 @@
+import type { PageParam } from '@/api';
+
+/**
+ * 水票
+ */
+export interface GltTicketTemplate {
+ //
+ id?: number;
+ // 关联商品ID
+ goodsId?: number;
+ // 名称
+ name?: string;
+ // 启用
+ enabled?: boolean;
+ // 单位名称
+ unitName?: string;
+ // 最小购买数量
+ minBuyQty?: number;
+ // 起始发送数量
+ startSendQty?: number;
+ // 买赠:买1送4 => gift_multiplier=4
+ giftMultiplier?: number;
+ // 是否把购买量也计入套票总量(默认仅计入赠送量)
+ includeBuyQty?: boolean;
+ // 每期释放数量(默认每月释放10)
+ monthlyReleaseQty?: number;
+ // 总共释放多少期(若配置>0,则按期数平均分摊)
+ releasePeriods?: number;
+ // 首期释放时机:0=支付成功当刻;1=下个月同日
+ firstReleaseMode?: number;
+ // 用户ID
+ userId?: number;
+ // 排序(数字越小越靠前)
+ sortNumber?: number;
+ // 备注
+ comments?: string;
+ // 状态, 0正常, 1冻结
+ status?: number;
+ // 是否删除, 0否, 1是
+ deleted?: number;
+ // 租户id
+ tenantId?: number;
+ // 创建时间
+ createTime?: string;
+ // 修改时间
+ updateTime?: string;
+}
+
+/**
+ * 水票搜索条件
+ */
+export interface GltTicketTemplateParam extends PageParam {
+ id?: number;
+ keywords?: string;
+}
diff --git a/src/api/glt/gltUserTicket/index.ts b/src/api/glt/gltUserTicket/index.ts
new file mode 100644
index 0000000..c412cc0
--- /dev/null
+++ b/src/api/glt/gltUserTicket/index.ts
@@ -0,0 +1,170 @@
+import request from '@/utils/request';
+import type { ApiResult, PageResult } from '@/api';
+import type { GltUserTicket, GltUserTicketParam } from './model';
+
+function normalizeTotal(input: unknown): number {
+ if (typeof input === 'number' && Number.isFinite(input)) return input;
+ if (typeof input === 'string') {
+ const n = Number(input);
+ if (Number.isFinite(n)) return n;
+ }
+ if (input && typeof input === 'object') {
+ const obj: any = input;
+ // Common shapes from different backends.
+ for (const key of ['total', 'count', 'value', 'num', 'ticketTotal', 'totalQty']) {
+ const v = obj?.[key];
+ const n = normalizeTotal(v);
+ if (n) return n;
+ }
+ // Sometimes nested: { data: { total: ... } } / { data: 12 }
+ if ('data' in obj) {
+ const n = normalizeTotal(obj.data);
+ if (n) return n;
+ }
+ }
+ return 0;
+}
+
+/**
+ * 分页查询我的水票
+ */
+export async function pageGltUserTicket(params: GltUserTicketParam) {
+ const res = await request.get>>(
+ '/glt/glt-user-ticket/page',
+ params
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 查询我的水票列表
+ */
+export async function listGltUserTicket(params?: GltUserTicketParam) {
+ const res = await request.get>(
+ '/glt/glt-user-ticket',
+ params
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 添加我的水票
+ */
+export async function addGltUserTicket(data: GltUserTicket) {
+ const res = await request.post>(
+ '/glt/glt-user-ticket',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 修改我的水票
+ */
+export async function updateGltUserTicket(data: GltUserTicket) {
+ const res = await request.put>(
+ '/glt/glt-user-ticket',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 删除我的水票
+ */
+export async function removeGltUserTicket(id?: number) {
+ const res = await request.del>(
+ '/glt/glt-user-ticket/' + id
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 批量删除我的水票
+ */
+export async function removeBatchGltUserTicket(data: (number | undefined)[]) {
+ const res = await request.del>(
+ '/glt/glt-user-ticket/batch',
+ {
+ data
+ }
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 根据id查询我的水票
+ */
+export async function getGltUserTicket(id: number) {
+ const res = await request.get>(
+ '/glt/glt-user-ticket/' + id
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 获取我的水票总数
+ */
+export async function getMyGltUserTicketTotal(userId?: number) {
+ const params = userId ? { userId } : undefined
+
+ const extract = (res: any) => {
+ // Some backends may return a raw number instead of ApiResult.
+ if (typeof res === 'number' || typeof res === 'string') return normalizeTotal(res)
+ if (res && typeof res === 'object' && 'code' in res) {
+ const apiRes = res as ApiResult
+ if (apiRes.code === 0) return normalizeTotal(apiRes.data)
+ throw new Error(apiRes.message)
+ }
+ return normalizeTotal(res)
+ }
+
+ // Try both the configured BaseUrl host and the auth-server host.
+ // If the first one returns 0, keep trying; some tenants deploy GLT on a different host.
+ const urls = [
+ '/glt/glt-user-ticket/my-total'
+ ]
+
+ let lastError: unknown
+ let firstTotal: number | undefined
+ for (const url of urls) {
+ try {
+ const res = await request.get(url, params)
+ if (process.env.NODE_ENV === 'development') {
+ console.log('[getMyGltUserTicketTotal] response:', { url, res })
+ }
+ const total = extract(res)
+ if (firstTotal === undefined) firstTotal = total
+ if (total) return total
+ } catch (e) {
+ if (process.env.NODE_ENV === 'development') {
+ console.warn('[getMyGltUserTicketTotal] failed:', { url, error: e })
+ }
+ lastError = e
+ }
+ }
+
+ if (firstTotal !== undefined) return firstTotal
+ return Promise.reject(lastError instanceof Error ? lastError : new Error('获取水票总数失败'))
+}
diff --git a/src/api/glt/gltUserTicket/model/index.ts b/src/api/glt/gltUserTicket/model/index.ts
new file mode 100644
index 0000000..7de0b48
--- /dev/null
+++ b/src/api/glt/gltUserTicket/model/index.ts
@@ -0,0 +1,66 @@
+import type { PageParam } from '@/api';
+
+/**
+ * 我的水票
+ */
+export interface GltUserTicket {
+ //
+ id?: number;
+ // 模板ID
+ templateId?: number;
+ // 模板名称
+ templateName?: string;
+ // 商品ID
+ goodsId?: number;
+ // 订单ID
+ orderId?: number;
+ // 订单编号
+ orderNo?: string;
+ // 订单商品ID
+ orderGoodsId?: number;
+ // 总数量
+ totalQty?: number;
+ // 可用数量
+ availableQty?: number;
+ // 冻结数量
+ frozenQty?: number;
+ // 已使用数量
+ usedQty?: number;
+ // 已释放数量
+ releasedQty?: number;
+ // 用户ID
+ userId?: number;
+ // 用户昵称
+ nickname?: string;
+ // 用户头像
+ avatar?: string;
+ // 用户手机号
+ phone?: string;
+ // 排序(数字越小越靠前)
+ sortNumber?: number;
+ // 备注
+ comments?: string;
+ // 状态, 0正常, 1冻结
+ status?: number;
+ // 是否删除, 0否, 1是
+ deleted?: number;
+ // 租户id
+ tenantId?: number;
+ // 创建时间
+ createTime?: string;
+ // 修改时间
+ updateTime?: string;
+}
+
+/**
+ * 我的水票搜索条件
+ */
+export interface GltUserTicketParam extends PageParam {
+ id?: number;
+ templateId?: number;
+ userId?: number;
+ phone?: string;
+ keywords?: string;
+ // 状态过滤:0正常,1冻结
+ status?: number;
+}
diff --git a/src/api/glt/gltUserTicketLog/index.ts b/src/api/glt/gltUserTicketLog/index.ts
new file mode 100644
index 0000000..87b74d8
--- /dev/null
+++ b/src/api/glt/gltUserTicketLog/index.ts
@@ -0,0 +1,101 @@
+import request from '@/utils/request';
+import type { ApiResult, PageResult } from '@/api';
+import type { GltUserTicketLog, GltUserTicketLogParam } from './model';
+
+/**
+ * 分页查询消费日志
+ */
+export async function pageGltUserTicketLog(params: GltUserTicketLogParam) {
+ const res = await request.get>>(
+ '/glt/glt-user-ticket-log/page',
+ params
+ );
+ if (res.code === 0) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 查询消费日志列表
+ */
+export async function listGltUserTicketLog(params?: GltUserTicketLogParam) {
+ const res = await request.get>(
+ '/glt/glt-user-ticket-log',
+ params
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 添加消费日志
+ */
+export async function addGltUserTicketLog(data: GltUserTicketLog) {
+ const res = await request.post>(
+ '/glt/glt-user-ticket-log',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 修改消费日志
+ */
+export async function updateGltUserTicketLog(data: GltUserTicketLog) {
+ const res = await request.put>(
+ '/glt/glt-user-ticket-log',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 删除消费日志
+ */
+export async function removeGltUserTicketLog(id?: number) {
+ const res = await request.del>(
+ '/glt/glt-user-ticket-log/' + id
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 批量删除消费日志
+ */
+export async function removeBatchGltUserTicketLog(data: (number | undefined)[]) {
+ const res = await request.del>(
+ '/glt/glt-user-ticket-log/batch',
+ {
+ data
+ }
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 根据id查询消费日志
+ */
+export async function getGltUserTicketLog(id: number) {
+ const res = await request.get>(
+ '/glt/glt-user-ticket-log/' + id
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
diff --git a/src/api/glt/gltUserTicketLog/model/index.ts b/src/api/glt/gltUserTicketLog/model/index.ts
new file mode 100644
index 0000000..b974f5b
--- /dev/null
+++ b/src/api/glt/gltUserTicketLog/model/index.ts
@@ -0,0 +1,54 @@
+import type { PageParam } from '@/api';
+
+/**
+ * 消费日志
+ */
+export interface GltUserTicketLog {
+ //
+ id?: number;
+ // 用户水票ID
+ userTicketId?: number;
+ // 变更类型
+ changeType?: number;
+ // 可更改
+ changeAvailable?: number;
+ // 更改冻结状态
+ changeFrozen?: number;
+ // 已使用更改
+ changeUsed?: number;
+ // 可用后
+ availableAfter?: number;
+ // 冻结后
+ frozenAfter?: number;
+ // 使用后
+ usedAfter?: number;
+ // 订单ID
+ orderId?: number;
+ // 订单编号
+ orderNo?: string;
+ // 用户ID
+ userId?: number;
+ // 排序(数字越小越靠前)
+ sortNumber?: number;
+ // 备注
+ comments?: string;
+ // 状态, 0正常, 1冻结
+ status?: number;
+ // 是否删除, 0否, 1是
+ deleted?: number;
+ // 租户id
+ tenantId?: number;
+ // 创建时间
+ createTime?: string;
+ // 修改时间
+ updateTime?: string;
+}
+
+/**
+ * 消费日志搜索条件
+ */
+export interface GltUserTicketLogParam extends PageParam {
+ id?: number;
+ keywords?: string;
+ userId?: number;
+}
diff --git a/src/api/glt/gltUserTicketRelease/index.ts b/src/api/glt/gltUserTicketRelease/index.ts
new file mode 100644
index 0000000..40b5e7d
--- /dev/null
+++ b/src/api/glt/gltUserTicketRelease/index.ts
@@ -0,0 +1,105 @@
+import request from '@/utils/request';
+import type { ApiResult, PageResult } from '@/api';
+import type { GltUserTicketRelease, GltUserTicketReleaseParam } from './model';
+
+/**
+ * 分页查询水票释放
+ */
+export async function pageGltUserTicketRelease(params: GltUserTicketReleaseParam) {
+ const res = await request.get>>(
+ '/glt/glt-user-ticket-release/page',
+ {
+ params
+ }
+ );
+ if (res.code === 0) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 查询水票释放列表
+ */
+export async function listGltUserTicketRelease(params?: GltUserTicketReleaseParam) {
+ const res = await request.get>(
+ '/glt/glt-user-ticket-release',
+ {
+ params
+ }
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 添加水票释放
+ */
+export async function addGltUserTicketRelease(data: GltUserTicketRelease) {
+ const res = await request.post>(
+ '/glt/glt-user-ticket-release',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 修改水票释放
+ */
+export async function updateGltUserTicketRelease(data: GltUserTicketRelease) {
+ const res = await request.put>(
+ '/glt/glt-user-ticket-release',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 删除水票释放
+ */
+export async function removeGltUserTicketRelease(id?: number) {
+ const res = await request.del>(
+ '/glt/glt-user-ticket-release/' + id
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 批量删除水票释放
+ */
+export async function removeBatchGltUserTicketRelease(data: (number | undefined)[]) {
+ const res = await request.del>(
+ '/glt/glt-user-ticket-release/batch',
+ {
+ data
+ }
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 根据id查询水票释放
+ */
+export async function getGltUserTicketRelease(id: number) {
+ const res = await request.get>(
+ '/glt/glt-user-ticket-release/' + id
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
diff --git a/src/api/glt/gltUserTicketRelease/model/index.ts b/src/api/glt/gltUserTicketRelease/model/index.ts
new file mode 100644
index 0000000..14cf1cd
--- /dev/null
+++ b/src/api/glt/gltUserTicketRelease/model/index.ts
@@ -0,0 +1,38 @@
+import type { PageParam } from '@/api';
+
+/**
+ * 水票释放
+ */
+export interface GltUserTicketRelease {
+ //
+ id?: string;
+ // 水票ID
+ userTicketId?: string;
+ // 用户ID
+ userId?: number;
+ // 周期编号
+ periodNo?: number;
+ // 释放数量
+ releaseQty?: number;
+ // 释放时间
+ releaseTime?: string;
+ // 状态
+ status?: number;
+ // 是否删除, 0否, 1是
+ deleted?: number;
+ // 租户id
+ tenantId?: number;
+ // 创建时间
+ createTime?: string;
+ // 修改时间
+ updateTime?: string;
+}
+
+/**
+ * 水票释放搜索条件
+ */
+export interface GltUserTicketReleaseParam extends PageParam {
+ id?: number;
+ userId?: number;
+ keywords?: string;
+}
diff --git a/src/api/shop/shopDealerCapital/model/index.ts b/src/api/shop/shopDealerCapital/model/index.ts
index e6a6bc2..00d29a7 100644
--- a/src/api/shop/shopDealerCapital/model/index.ts
+++ b/src/api/shop/shopDealerCapital/model/index.ts
@@ -31,5 +31,11 @@ export interface ShopDealerCapital {
*/
export interface ShopDealerCapitalParam extends PageParam {
id?: number;
+ // 仅查询当前分销商的收益/资金明细
+ userId?: number;
+ // 可选:按订单过滤
+ orderId?: number;
+ // 可选:资金流动类型过滤
+ flowType?: number;
keywords?: string;
}
diff --git a/src/api/shop/shopDealerOrder/model/index.ts b/src/api/shop/shopDealerOrder/model/index.ts
index 68b4928..03c9512 100644
--- a/src/api/shop/shopDealerOrder/model/index.ts
+++ b/src/api/shop/shopDealerOrder/model/index.ts
@@ -8,6 +8,9 @@ export interface ShopDealerOrder {
id?: number;
// 买家用户ID
userId?: number;
+ nickname?: string;
+ // 订单编号(部分接口会直接返回订单号字符串)
+ orderNo?: string;
// 订单ID
orderId?: number;
// 订单总金额(不含运费)
@@ -28,6 +31,10 @@ export interface ShopDealerOrder {
isInvalid?: number;
// 佣金结算(0未结算 1已结算)
isSettled?: number;
+ // 佣金解冻(0未解冻 1已解冻)
+ isUnfreeze?: number;
+ // 订单状态
+ orderStatus?: number;
// 结算时间
settleTime?: number;
// 商城ID
@@ -47,5 +54,7 @@ export interface ShopDealerOrderParam extends PageParam {
secondUserId?: number;
thirdUserId?: number;
userId?: number;
+ // 数据权限/资源ID(通常传当前登录用户ID)
+ resourceId?: number;
keywords?: string;
}
diff --git a/src/api/shop/shopDealerUser/index.ts b/src/api/shop/shopDealerUser/index.ts
index 0f673a1..df82b27 100644
--- a/src/api/shop/shopDealerUser/index.ts
+++ b/src/api/shop/shopDealerUser/index.ts
@@ -95,8 +95,9 @@ export async function getShopDealerUser(userId: number) {
const res = await request.get>(
'/shop/shop-dealer-user/' + userId
);
- if (res.code === 0 && res.data) {
- return res.data;
+ if (res.code === 0) {
+ // 未注册为分销商时,后端可能返回 data=null,这里用 null 表示“没有分销商信息”
+ return res.data || null;
}
return Promise.reject(new Error(res.message));
}
diff --git a/src/api/shop/shopDealerWithdraw/index.ts b/src/api/shop/shopDealerWithdraw/index.ts
index 6968031..398b9be 100644
--- a/src/api/shop/shopDealerWithdraw/index.ts
+++ b/src/api/shop/shopDealerWithdraw/index.ts
@@ -2,6 +2,21 @@ import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopDealerWithdraw, ShopDealerWithdrawParam } from './model';
+// WeChat transfer v3: backend may return `package_info` for MiniProgram to open the
+// "confirm receipt" page via `wx.requestMerchantTransfer`.
+export type ShopDealerWithdrawCreateResult =
+ | string
+ | {
+ package_info?: string;
+ packageInfo?: string;
+ [k: string]: any;
+ }
+ | null
+ | undefined;
+
+// When applyStatus=20, user can "receive" (WeChat confirm receipt flow).
+export type ShopDealerWithdrawReceiveResult = ShopDealerWithdrawCreateResult;
+
/**
* 分页查询分销商提现明细表
*/
@@ -33,11 +48,40 @@ export async function listShopDealerWithdraw(params?: ShopDealerWithdrawParam) {
/**
* 添加分销商提现明细表
*/
-export async function addShopDealerWithdraw(data: ShopDealerWithdraw) {
- const res = await request.post>(
+export async function addShopDealerWithdraw(data: ShopDealerWithdraw): Promise {
+ const res = await request.post>(
'/shop/shop-dealer-withdraw',
data
);
+ if (res.code === 0) {
+ // Some backends return `message`, while WeChat transfer flow returns `data.package_info`.
+ return res.data ?? res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 用户领取(仅当 applyStatus=20 时)- 后台返回 package_info 供小程序调起确认收款页
+ */
+export async function receiveShopDealerWithdraw(id: number): Promise {
+ const res = await request.post>(
+ '/shop/shop-dealer-withdraw/receive/' + id,
+ {}
+ );
+ if (res.code === 0) {
+ return res.data ?? res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 领取成功回调:前端确认收款后通知后台把状态置为 applyStatus=40
+ */
+export async function receiveSuccessShopDealerWithdraw(id: number) {
+ const res = await request.post>(
+ '/shop/shop-dealer-withdraw/receive-success/' + id,
+ {}
+ );
if (res.code === 0) {
return res.message;
}
diff --git a/src/api/shop/shopGift/model/index.ts b/src/api/shop/shopGift/model/index.ts
index 93713a6..f3c7450 100644
--- a/src/api/shop/shopGift/model/index.ts
+++ b/src/api/shop/shopGift/model/index.ts
@@ -1,7 +1,7 @@
import type { PageParam } from '@/api';
/**
- * 礼品卡
+ * 水票
*/
export interface ShopGift {
// 礼品卡ID
diff --git a/src/api/shop/shopGoods/model/index.ts b/src/api/shop/shopGoods/model/index.ts
index ce4328f..977d90f 100644
--- a/src/api/shop/shopGoods/model/index.ts
+++ b/src/api/shop/shopGoods/model/index.ts
@@ -146,4 +146,7 @@ export interface ShopGoodsParam extends PageParam {
isShow?: number;
stock?: number;
keywords?: string;
+ recommend?: number;
+ // 0上架 1下架(以实际后端约定为准)
+ status?: number;
}
diff --git a/src/api/shop/shopOrder/index.ts b/src/api/shop/shopOrder/index.ts
index ca1e805..8e61482 100644
--- a/src/api/shop/shopOrder/index.ts
+++ b/src/api/shop/shopOrder/index.ts
@@ -1,4 +1,4 @@
-import request from '@/utils/request';
+import request, { ErrorType, RequestError } from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopOrder, ShopOrderParam, OrderCreateRequest } from './model';
@@ -113,6 +113,44 @@ export interface WxPayResult {
paySign: string;
}
+/**
+ * 订单重新发起支付(对“已创建但未支付”的订单生成新的预支付参数,不应重复创建订单)
+ *
+ * 说明:不同后端版本可能暴露不同路径,这里做兼容探测;若全部失败,调用方可自行降级处理。
+ */
+export interface OrderPrepayRequest {
+ orderId: number;
+ payType: number;
+}
+
+export async function prepayShopOrder(data: OrderPrepayRequest) {
+ const urls = [
+ '/shop/shop-order/pay',
+ '/shop/shop-order/prepay',
+ '/shop/shop-order/repay'
+ ];
+
+ let lastError: unknown;
+ let businessError: unknown;
+ for (const url of urls) {
+ try {
+ const res = await request.post>(url, data, { showError: false });
+ // request.ts 在 code!=0 时会直接 throw;走到这里通常都是 code===0
+ if (res.code === 0) return res.data;
+ } catch (e) {
+ // 若已命中“业务错误”(例如订单已取消/已支付),优先保留该错误用于向上提示;
+ // 不要被后续的 404/网络错误覆盖掉,避免调用方误判为“不支持该接口”而降级走创建订单。
+ if (!businessError && e instanceof RequestError && e.type === ErrorType.BUSINESS_ERROR) {
+ businessError = e;
+ } else {
+ lastError = e;
+ }
+ }
+ }
+
+ return Promise.reject(businessError || lastError || new Error('发起支付失败'));
+}
+
/**
* 创建订单
*/
@@ -140,3 +178,18 @@ export async function repairOrder(data: ShopOrder) {
}
return Promise.reject(new Error(res.message));
}
+
+
+/**
+ * 申请|同意退款
+ */
+export async function refundShopOrder(data: ShopOrder) {
+ const res = await request.put>(
+ '/shop/shop-order/refund',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
diff --git a/src/api/shop/shopOrder/model/index.ts b/src/api/shop/shopOrder/model/index.ts
index 92709f8..ee1320a 100644
--- a/src/api/shop/shopOrder/model/index.ts
+++ b/src/api/shop/shopOrder/model/index.ts
@@ -1,5 +1,5 @@
import type { PageParam } from '@/api/index';
-import {OrderGoods} from "@/api/system/orderGoods/model";
+import type { ShopOrderGoods } from '@/api/shop/shopOrderGoods/model';
/**
* 订单
@@ -27,6 +27,14 @@ export interface ShopOrder {
merchantName?: string;
// 商户编号
merchantCode?: string;
+ // 归属门店ID(shop_store.id)
+ storeId?: number;
+ // 归属门店名称
+ storeName?: string;
+ // 配送员用户ID(优先级派单)
+ riderId?: number;
+ // 发货仓库ID
+ warehouseId?: number;
// 使用的优惠券id
couponId?: number;
// 使用的会员卡id
@@ -61,6 +69,8 @@ export interface ShopOrder {
sendStartTime?: string;
// 配送结束时间
sendEndTime?: string;
+ // 配送员送达拍照(选填)
+ sendEndImg?: string;
// 发货店铺id
expressMerchantId?: number;
// 发货店铺
@@ -83,6 +93,8 @@ export interface ShopOrder {
totalNum?: number;
// 教练id
coachId?: number;
+ // 商品ID
+ formId?: number;
// 支付的用户id
payUserId?: number;
// 0余额支付, 1微信支付,102微信Native,2会员卡支付,3支付宝,4现金,5POS机,6VIP月卡,7VIP年卡,8VIP次卡,9IC月卡,10IC年卡,11IC次卡,12免费,13VIP充值卡,14IC充值卡,15积分支付,16VIP季卡,17IC季卡,18代付
@@ -146,7 +158,7 @@ export interface ShopOrder {
// 是否已收到赠品
hasTakeGift?: string;
// 订单商品项
- orderGoods?: OrderGoods[];
+ orderGoods?: ShopOrderGoods[];
}
/**
@@ -165,6 +177,14 @@ export interface OrderGoodsItem {
export interface OrderCreateRequest {
// 商品信息列表
goodsItems: OrderGoodsItem[];
+ // 归属门店ID(shop_store.id)
+ storeId?: number;
+ // 归属门店名称(可选)
+ storeName?: string;
+ // 配送员用户ID(优先级派单)
+ riderId?: number;
+ // 发货仓库ID
+ warehouseId?: number;
// 收货地址ID
addressId?: number;
// 支付方式
@@ -173,6 +193,8 @@ export interface OrderCreateRequest {
couponId?: number;
// 备注
comments?: string;
+ // 配送开始时间(用于预约/配送时间)
+ sendStartTime?: string;
// 配送方式 0快递 1自提
deliveryType?: number;
// 自提店铺ID
@@ -197,6 +219,14 @@ export interface OrderGoodsItem {
export interface OrderCreateRequest {
// 商品信息列表
goodsItems: OrderGoodsItem[];
+ // 归属门店ID(shop_store.id)
+ storeId?: number;
+ // 归属门店名称(可选)
+ storeName?: string;
+ // 配送员用户ID(优先级派单)
+ riderId?: number;
+ // 发货仓库ID
+ warehouseId?: number;
// 收货地址ID
addressId?: number;
// 支付方式
@@ -205,6 +235,8 @@ export interface OrderCreateRequest {
couponId?: number;
// 备注
comments?: string;
+ // 配送开始时间(用于预约/配送时间)
+ sendStartTime?: string;
// 配送方式 0快递 1自提
deliveryType?: number;
// 自提店铺ID
@@ -223,6 +255,12 @@ export interface ShopOrderParam extends PageParam {
payType?: number;
isInvoice?: boolean;
userId?: number;
+ // 归属门店ID(shop_store.id)
+ storeId?: number;
+ // 配送员用户ID
+ riderId?: number;
+ // 发货仓库ID
+ warehouseId?: number;
keywords?: string;
deliveryStatus?: number;
statusFilter?: number;
diff --git a/src/api/shop/shopStore/index.ts b/src/api/shop/shopStore/index.ts
new file mode 100644
index 0000000..e67303f
--- /dev/null
+++ b/src/api/shop/shopStore/index.ts
@@ -0,0 +1,101 @@
+import request from '@/utils/request';
+import type { ApiResult, PageResult } from '@/api';
+import type { ShopStore, ShopStoreParam } from './model';
+
+/**
+ * 分页查询门店
+ */
+export async function pageShopStore(params: ShopStoreParam) {
+ const res = await request.get>>(
+ '/shop/shop-store/page',
+ params
+ );
+ if (res.code === 0) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 查询门店列表
+ */
+export async function listShopStore(params?: ShopStoreParam) {
+ const res = await request.get>(
+ '/shop/shop-store',
+ params
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 添加门店
+ */
+export async function addShopStore(data: ShopStore) {
+ const res = await request.post>(
+ '/shop/shop-store',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 修改门店
+ */
+export async function updateShopStore(data: ShopStore) {
+ const res = await request.put>(
+ '/shop/shop-store',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 删除门店
+ */
+export async function removeShopStore(id?: number) {
+ const res = await request.del>(
+ '/shop/shop-store/' + id
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 批量删除门店
+ */
+export async function removeBatchShopStore(data: (number | undefined)[]) {
+ const res = await request.del>(
+ '/shop/shop-store/batch',
+ {
+ data
+ }
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 根据id查询门店
+ */
+export async function getShopStore(id: number) {
+ const res = await request.get>(
+ '/shop/shop-store/' + id
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
diff --git a/src/api/shop/shopStore/model/index.ts b/src/api/shop/shopStore/model/index.ts
new file mode 100644
index 0000000..3e5bcd1
--- /dev/null
+++ b/src/api/shop/shopStore/model/index.ts
@@ -0,0 +1,63 @@
+import type { PageParam } from '@/api';
+
+/**
+ * 门店
+ */
+export interface ShopStore {
+ // 自增ID
+ id?: number;
+ // 店铺名称
+ name?: string;
+ // 门店地址
+ address?: string;
+ // 手机号码
+ phone?: string;
+ // 邮箱
+ email?: string;
+ // 门店经理
+ managerName?: string;
+ // 门店banner
+ shopBanner?: string;
+ // 所在省份
+ province?: string;
+ // 所在城市
+ city?: string;
+ // 所在辖区
+ region?: string;
+ // 经度和纬度
+ lngAndLat?: string;
+ // 位置
+ location?:string;
+ // 区域
+ district?: string;
+ // 轮廓
+ points?: string;
+ // 用户ID
+ userId?: number;
+ // 默认仓库ID(shop_warehouse.id)
+ warehouseId?: number;
+ // 默认仓库名称(可选)
+ warehouseName?: string;
+ // 状态
+ status?: number;
+ // 备注
+ comments?: string;
+ // 排序号
+ sortNumber?: number;
+ // 是否删除
+ isDelete?: number;
+ // 租户id
+ tenantId?: number;
+ // 创建时间
+ createTime?: string;
+ // 修改时间
+ updateTime?: string;
+}
+
+/**
+ * 门店搜索条件
+ */
+export interface ShopStoreParam extends PageParam {
+ id?: number;
+ keywords?: string;
+}
diff --git a/src/api/shop/shopStoreFence/index.ts b/src/api/shop/shopStoreFence/index.ts
new file mode 100644
index 0000000..609defa
--- /dev/null
+++ b/src/api/shop/shopStoreFence/index.ts
@@ -0,0 +1,101 @@
+import request from '@/utils/request';
+import type { ApiResult, PageResult } from '@/api/index';
+import type { ShopStoreFence, ShopStoreFenceParam } from './model';
+
+/**
+ * 分页查询黄家明_电子围栏
+ */
+export async function pageShopStoreFence(params: ShopStoreFenceParam) {
+ const res = await request.get>>(
+ '/shop/shop-store-fence/page',
+ params
+ );
+ if (res.code === 0) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 查询黄家明_电子围栏列表
+ */
+export async function listShopStoreFence(params?: ShopStoreFenceParam) {
+ const res = await request.get>(
+ '/shop/shop-store-fence',
+ params
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 添加黄家明_电子围栏
+ */
+export async function addShopStoreFence(data: ShopStoreFence) {
+ const res = await request.post>(
+ '/shop/shop-store-fence',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 修改黄家明_电子围栏
+ */
+export async function updateShopStoreFence(data: ShopStoreFence) {
+ const res = await request.put>(
+ '/shop/shop-store-fence',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 删除黄家明_电子围栏
+ */
+export async function removeShopStoreFence(id?: number) {
+ const res = await request.del>(
+ '/shop/shop-store-fence/' + id
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 批量删除黄家明_电子围栏
+ */
+export async function removeBatchShopStoreFence(data: (number | undefined)[]) {
+ const res = await request.del>(
+ '/shop/shop-store-fence/batch',
+ {
+ data
+ }
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 根据id查询黄家明_电子围栏
+ */
+export async function getShopStoreFence(id: number) {
+ const res = await request.get>(
+ '/shop/shop-store-fence/' + id
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
diff --git a/src/api/shop/shopStoreFence/model/index.ts b/src/api/shop/shopStoreFence/model/index.ts
new file mode 100644
index 0000000..99c20dd
--- /dev/null
+++ b/src/api/shop/shopStoreFence/model/index.ts
@@ -0,0 +1,43 @@
+import type { PageParam } from '@/api/index';
+
+/**
+ * 黄家明_电子围栏
+ */
+export interface ShopStoreFence {
+ // 自增ID
+ id?: number;
+ // 围栏名称
+ name?: string;
+ // 类型 0圆形 1方形
+ type?: number;
+ // 定位
+ location?: string;
+ // 经度
+ longitude?: string;
+ // 纬度
+ latitude?: string;
+ // 区域
+ district?: string;
+ // 电子围栏轮廓
+ points?: string;
+ // 排序(数字越小越靠前)
+ sortNumber?: number;
+ // 备注
+ comments?: string;
+ // 状态, 0正常, 1冻结
+ status?: number;
+ // 租户id
+ tenantId?: number;
+ // 创建时间
+ createTime?: string;
+ // 修改时间
+ updateTime?: string;
+}
+
+/**
+ * 黄家明_电子围栏搜索条件
+ */
+export interface ShopStoreFenceParam extends PageParam {
+ id?: number;
+ keywords?: string;
+}
diff --git a/src/api/shop/shopStoreRider/index.ts b/src/api/shop/shopStoreRider/index.ts
new file mode 100644
index 0000000..c411aef
--- /dev/null
+++ b/src/api/shop/shopStoreRider/index.ts
@@ -0,0 +1,101 @@
+import request from '@/utils/request';
+import type { ApiResult, PageResult } from '@/api';
+import type { ShopStoreRider, ShopStoreRiderParam } from './model';
+
+/**
+ * 分页查询配送员
+ */
+export async function pageShopStoreRider(params: ShopStoreRiderParam) {
+ const res = await request.get>>(
+ '/shop/shop-store-rider/page',
+ params
+ );
+ if (res.code === 0) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 查询配送员列表
+ */
+export async function listShopStoreRider(params?: ShopStoreRiderParam) {
+ const res = await request.get>(
+ '/shop/shop-store-rider',
+ params
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 添加配送员
+ */
+export async function addShopStoreRider(data: ShopStoreRider) {
+ const res = await request.post>(
+ '/shop/shop-store-rider',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 修改配送员
+ */
+export async function updateShopStoreRider(data: ShopStoreRider) {
+ const res = await request.put>(
+ '/shop/shop-store-rider',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 删除配送员
+ */
+export async function removeShopStoreRider(id?: number) {
+ const res = await request.del>(
+ '/shop/shop-store-rider/' + id
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 批量删除配送员
+ */
+export async function removeBatchShopStoreRider(data: (number | undefined)[]) {
+ const res = await request.del>(
+ '/shop/shop-store-rider/batch',
+ {
+ data
+ }
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 根据id查询配送员
+ */
+export async function getShopStoreRider(id: number) {
+ const res = await request.get>(
+ '/shop/shop-store-rider/' + id
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
diff --git a/src/api/shop/shopStoreRider/model/index.ts b/src/api/shop/shopStoreRider/model/index.ts
new file mode 100644
index 0000000..0b7ed68
--- /dev/null
+++ b/src/api/shop/shopStoreRider/model/index.ts
@@ -0,0 +1,71 @@
+import type { PageParam } from '@/api';
+
+/**
+ * 配送员
+ */
+export interface ShopStoreRider {
+ // 主键ID
+ id?: string;
+ // 配送点ID(shop_dealer.id)
+ dealerId?: number;
+ // 骑手编号(可选)
+ riderNo?: string;
+ // 姓名
+ realName?: string;
+ // 手机号
+ mobile?: string;
+ // 头像
+ avatar?: string;
+ // 身份证号(可选)
+ idCardNo?: string;
+ // 状态:1启用;0禁用
+ status?: number;
+ // 接单状态:0休息/下线;1在线;2忙碌
+ workStatus?: number;
+ // 是否开启自动派单:1是;0否
+ autoDispatchEnabled?: number;
+ // 派单优先级(同小区多骑手时可用,值越大越优先)
+ dispatchPriority?: number;
+ // 最大同时配送单数(0表示不限制)
+ maxOnhandOrders?: number;
+ // 是否计算工资(提成):1计算;0不计算(如三方配送点可设0)
+ commissionCalcEnabled?: number;
+ // 水每桶提成金额(元/桶)
+ waterBucketUnitFee?: string;
+ // 其他商品提成方式:1按订单固定金额;2按订单金额比例;3按商品规则(另表)
+ otherGoodsCommissionType?: number;
+ // 其他商品提成值:固定金额(元)或比例(%)
+ otherGoodsCommissionValue?: string;
+ // 用户ID
+ userId?: number;
+ // 经度(配送员当前位置)
+ longitude?: string;
+ // 纬度(配送员当前位置)
+ latitude?: string;
+ // 备注
+ comments?: string;
+ // 排序号
+ sortNumber?: number;
+ // 是否删除
+ isDelete?: number;
+ // 租户id
+ tenantId?: number;
+ // 创建时间
+ createTime?: string;
+ // 修改时间
+ updateTime?: string;
+}
+
+/**
+ * 配送员搜索条件
+ */
+export interface ShopStoreRiderParam extends PageParam {
+ id?: number;
+ keywords?: string;
+ // 配送点/门店ID(后端可能用 dealerId 或 storeId)
+ dealerId?: number;
+ storeId?: number;
+ status?: number;
+ workStatus?: number;
+ autoDispatchEnabled?: number;
+}
diff --git a/src/api/shop/shopStoreUser/index.ts b/src/api/shop/shopStoreUser/index.ts
new file mode 100644
index 0000000..0e500ab
--- /dev/null
+++ b/src/api/shop/shopStoreUser/index.ts
@@ -0,0 +1,101 @@
+import request from '@/utils/request';
+import type { ApiResult, PageResult } from '@/api';
+import type { ShopStoreUser, ShopStoreUserParam } from './model';
+
+/**
+ * 分页查询店员
+ */
+export async function pageShopStoreUser(params: ShopStoreUserParam) {
+ const res = await request.get>>(
+ '/shop/shop-store-user/page',
+ params
+ );
+ if (res.code === 0) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 查询店员列表
+ */
+export async function listShopStoreUser(params?: ShopStoreUserParam) {
+ const res = await request.get>(
+ '/shop/shop-store-user',
+ params
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 添加店员
+ */
+export async function addShopStoreUser(data: ShopStoreUser) {
+ const res = await request.post>(
+ '/shop/shop-store-user',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 修改店员
+ */
+export async function updateShopStoreUser(data: ShopStoreUser) {
+ const res = await request.put>(
+ '/shop/shop-store-user',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 删除店员
+ */
+export async function removeShopStoreUser(id?: number) {
+ const res = await request.del>(
+ '/shop/shop-store-user/' + id
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 批量删除店员
+ */
+export async function removeBatchShopStoreUser(data: (number | undefined)[]) {
+ const res = await request.del>(
+ '/shop/shop-store-user/batch',
+ {
+ data
+ }
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 根据id查询店员
+ */
+export async function getShopStoreUser(id: number) {
+ const res = await request.get>(
+ '/shop/shop-store-user/' + id
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
diff --git a/src/api/shop/shopStoreUser/model/index.ts b/src/api/shop/shopStoreUser/model/index.ts
new file mode 100644
index 0000000..46151f5
--- /dev/null
+++ b/src/api/shop/shopStoreUser/model/index.ts
@@ -0,0 +1,36 @@
+import type { PageParam } from '@/api';
+
+/**
+ * 店员
+ */
+export interface ShopStoreUser {
+ // 主键ID
+ id?: number;
+ // 配送点ID(shop_dealer.id)
+ storeId?: number;
+ // 用户ID
+ userId?: number;
+ // 备注
+ comments?: string;
+ // 排序号
+ sortNumber?: number;
+ // 是否删除
+ isDelete?: number;
+ // 租户id
+ tenantId?: number;
+ // 创建时间
+ createTime?: string;
+ // 修改时间
+ updateTime?: string;
+}
+
+/**
+ * 店员搜索条件
+ */
+export interface ShopStoreUserParam extends PageParam {
+ id?: number;
+ keywords?: string;
+ storeId?: number;
+ userId?: number;
+ isDelete?: number;
+}
diff --git a/src/api/shop/shopStoreWarehouse/index.ts b/src/api/shop/shopStoreWarehouse/index.ts
new file mode 100644
index 0000000..898e7b9
--- /dev/null
+++ b/src/api/shop/shopStoreWarehouse/index.ts
@@ -0,0 +1,101 @@
+import request from '@/utils/request';
+import type { ApiResult, PageResult } from '@/api/index';
+import type { ShopStoreWarehouse, ShopStoreWarehouseParam } from './model';
+
+/**
+ * 分页查询仓库
+ */
+export async function pageShopStoreWarehouse(params: ShopStoreWarehouseParam) {
+ const res = await request.get>>(
+ '/shop/shop-store-warehouse/page',
+ params
+ );
+ if (res.code === 0) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 查询仓库列表
+ */
+export async function listShopStoreWarehouse(params?: ShopStoreWarehouseParam) {
+ const res = await request.get>(
+ '/shop/shop-store-warehouse',
+ params
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 添加仓库
+ */
+export async function addShopStoreWarehouse(data: ShopStoreWarehouse) {
+ const res = await request.post>(
+ '/shop/shop-store-warehouse',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 修改仓库
+ */
+export async function updateShopStoreWarehouse(data: ShopStoreWarehouse) {
+ const res = await request.put>(
+ '/shop/shop-store-warehouse',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 删除仓库
+ */
+export async function removeShopStoreWarehouse(id?: number) {
+ const res = await request.del>(
+ '/shop/shop-store-warehouse/' + id
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 批量删除仓库
+ */
+export async function removeBatchShopStoreWarehouse(data: (number | undefined)[]) {
+ const res = await request.del>(
+ '/shop/shop-store-warehouse/batch',
+ {
+ data
+ }
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 根据id查询仓库
+ */
+export async function getShopStoreWarehouse(id: number) {
+ const res = await request.get>(
+ '/shop/shop-store-warehouse/' + id
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
diff --git a/src/api/shop/shopStoreWarehouse/model/index.ts b/src/api/shop/shopStoreWarehouse/model/index.ts
new file mode 100644
index 0000000..90d5d84
--- /dev/null
+++ b/src/api/shop/shopStoreWarehouse/model/index.ts
@@ -0,0 +1,53 @@
+import type { PageParam } from '@/api/index';
+
+/**
+ * 仓库
+ */
+export interface ShopStoreWarehouse {
+ // 自增ID
+ id?: number;
+ // 仓库名称
+ name?: string;
+ // 唯一标识
+ code?: string;
+ // 类型 中心仓,区域仓,门店仓
+ type?: string;
+ // 仓库地址
+ address?: string;
+ // 真实姓名
+ realName?: string;
+ // 联系电话
+ phone?: string;
+ // 所在省份
+ province?: string;
+ // 所在城市
+ city?: string;
+ // 所在辖区
+ region?: string;
+ // 经纬度
+ lngAndLat?: string;
+ // 用户ID
+ userId?: number;
+ // 状态
+ status?: number;
+ // 备注
+ comments?: string;
+ // 排序号
+ sortNumber?: number;
+ // 是否删除
+ isDelete?: number;
+ // 租户id
+ tenantId?: number;
+ // 创建时间
+ createTime?: string;
+ // 修改时间
+ updateTime?: string;
+}
+
+/**
+ * 仓库搜索条件
+ */
+export interface ShopStoreWarehouseParam extends PageParam {
+ id?: number;
+ keywords?: string;
+}
diff --git a/src/api/shop/shopUserAddress/model/index.ts b/src/api/shop/shopUserAddress/model/index.ts
index 9bf7f50..85696b9 100644
--- a/src/api/shop/shopUserAddress/model/index.ts
+++ b/src/api/shop/shopUserAddress/model/index.ts
@@ -38,6 +38,8 @@ export interface ShopUserAddress {
tenantId?: number;
// 注册时间
createTime?: string;
+ // 更新时间
+ updateTime?: string;
}
/**
diff --git a/src/api/system/file/index.ts b/src/api/system/file/index.ts
index 18a06dd..a1b1b19 100644
--- a/src/api/system/file/index.ts
+++ b/src/api/system/file/index.ts
@@ -21,7 +21,7 @@ export async function uploadOssByPath(filePath: string) {
let stsExpired = Taro.getStorageSync('stsExpiredAt');
if (!sts || (stsExpired && dayjs().isBefore(dayjs(stsExpired)))) {
// @ts-ignore
- const {data: {data: {credentials}}} = await request.get(`https://server.websoft.top/api/oss/getSTSToken`)
+ const {data: {data: {credentials}}} = await request.get(`https://gle-server.websoft.top/api/oss/getSTSToken`)
Taro.setStorageSync('sts', credentials)
Taro.setStorageSync('stsExpiredAt', credentials.expiration)
sts = credentials
@@ -49,7 +49,7 @@ export async function uploadOssByPath(filePath: string) {
})
}
-const computeSignature = (accessKeySecret, canonicalString) => {
+const computeSignature = (accessKeySecret: string, canonicalString: string): string => {
return crypto.enc.Base64.stringify(crypto.HmacSHA1(canonicalString, accessKeySecret));
}
@@ -66,7 +66,7 @@ export async function uploadFile() {
const tempFilePath = res.tempFilePaths[0];
// 上传图片到OSS
Taro.uploadFile({
- url: 'https://server.websoft.top/api/oss/upload',
+ url: 'https://glt-server.websoft.top/api/oss/upload',
filePath: tempFilePath,
name: 'file',
header: {
diff --git a/src/api/system/userRole/index.ts b/src/api/system/userRole/index.ts
index 3dfa2e1..ecaefcc 100644
--- a/src/api/system/userRole/index.ts
+++ b/src/api/system/userRole/index.ts
@@ -30,3 +30,18 @@ export async function updateUserRole(data: UserRole) {
}
return Promise.reject(new Error(res.message));
}
+
+/**
+ * 新增用户角色
+ * 说明:部分后端实现为 POST 新增、PUT 修改;这里补齐 API 以便新用户无角色时可以创建默认角色。
+ */
+export async function addUserRole(data: UserRole) {
+ const res = await request.post>(
+ SERVER_API_URL + '/system/user-role',
+ data
+ );
+ if (res.code === 0) {
+ return res.message;
+ }
+ return Promise.reject(new Error(res.message));
+}
diff --git a/src/api/user/index.ts b/src/api/user/index.ts
index 7189f58..8f8b3c6 100644
--- a/src/api/user/index.ts
+++ b/src/api/user/index.ts
@@ -43,6 +43,15 @@ export interface UserOrderStats {
total: number
}
+// 用户卡片统计(个人中心头部:余额/积分/优惠券/水票)
+export interface UserCardStats {
+ balance: string
+ points: number
+ coupons: number
+ giftCards: number
+ lastUpdateTime?: string
+}
+
// 用户完整数据
export interface UserDashboard {
balance: UserBalance
@@ -108,6 +117,17 @@ export async function getUserOrderStats() {
return Promise.reject(new Error(res.message))
}
+/**
+ * 获取用户卡片统计(一次性返回余额/积分/可用优惠券/未使用礼品卡数量)
+ */
+export async function getUserCardStats() {
+ const res = await request.get>('/user/card/stats')
+ if (res.code === 0 && res.data) {
+ return res.data
+ }
+ return Promise.reject(new Error(res.message))
+}
+
/**
* 获取用户完整仪表板数据(一次性获取所有数据)
*/
diff --git a/src/app.config.ts b/src/app.config.ts
index 78df1ad..f0e520f 100644
--- a/src/app.config.ts
+++ b/src/app.config.ts
@@ -54,10 +54,15 @@ export default {
"wallet/wallet",
"coupon/index",
"points/points",
- "gift/index",
- "gift/redeem",
- "gift/detail",
+ "ticket/index",
+ "ticket/use",
+ "ticket/orders/index",
+ // "gift/index",
+ // "gift/redeem",
+ // "gift/detail",
+ // "gift/add",
"store/verification",
+ "store/orders/index",
"theme/index",
"poster/poster",
"chat/conversation/index",
@@ -73,6 +78,7 @@ export default {
"apply/add",
"withdraw/index",
"orders/index",
+ "capital/index",
"team/index",
"qrcode/index",
"invite-stats/index",
@@ -90,6 +96,21 @@ export default {
'comments/index',
'search/index']
},
+ {
+ "root": "store",
+ "pages": [
+ "index",
+ "orders/index"
+ ]
+ },
+ {
+ "root": "rider",
+ "pages": [
+ "index",
+ "orders/index",
+ "ticket/verification/index"
+ ]
+ },
{
"root": "admin",
"pages": [
@@ -116,12 +137,6 @@ export default {
selectedIconPath: "assets/tabbar/home-active.png",
text: "首页",
},
- {
- pagePath: "pages/category/index",
- iconPath: "assets/tabbar/category.png",
- selectedIconPath: "assets/tabbar/category-active.png",
- text: "基地生活",
- },
{
pagePath: "pages/cart/cart",
iconPath: "assets/tabbar/cart.png",
@@ -144,6 +159,9 @@ export default {
permission: {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
+ },
+ "scope.writePhotosAlbum": {
+ "desc": "用于保存小程序码到相册,方便分享给好友"
}
}
}
diff --git a/src/app.scss b/src/app.scss
index ae872ba..272310c 100644
--- a/src/app.scss
+++ b/src/app.scss
@@ -87,6 +87,10 @@ button[open-type="chooseAvatar"] {
justify-content: center;
height: 80px;
}
+ .cart-buy-only{
+ border-radius: 20px;
+ flex: 1;
+ }
}
image {
diff --git a/src/cms/category/index.tsx b/src/cms/category/index.tsx
index fe81623..75a784b 100644
--- a/src/cms/category/index.tsx
+++ b/src/cms/category/index.tsx
@@ -44,7 +44,7 @@ function Category() {
useShareAppMessage(() => {
return {
- title: `${nav?.categoryName}_时里院子市集`,
+ title: `${nav?.categoryName}_桂乐淘`,
path: `/shop/category/index?id=${categoryId}`,
success: function () {
console.log('分享成功');
diff --git a/src/components/AddCartBar.tsx b/src/components/AddCartBar.tsx
index c095208..c2e807e 100644
--- a/src/components/AddCartBar.tsx
+++ b/src/components/AddCartBar.tsx
@@ -5,6 +5,7 @@ import {getUserInfo} from "@/api/layout";
import {useEffect, useState} from "react";
import {getCmsArticle} from "@/api/cms/cmsArticle";
import {CmsArticle} from "@/api/cms/cmsArticle/model";
+import { goToRegister } from '@/utils/auth'
function AddCartBar() {
const { router } = getCurrentInstance();
@@ -13,13 +14,8 @@ function AddCartBar() {
const [IsLogin, setIsLogin] = useState(false)
const onPay = () => {
if (!IsLogin) {
- Taro.showToast({title: `请先登录`, icon: 'error'})
setTimeout(() => {
- Taro.switchTab(
- {
- url: '/pages/user/user',
- },
- )
+ goToRegister({ redirect: '/pages/user/user' })
}, 1000)
return false;
}
diff --git a/src/components/GiftCard.tsx b/src/components/GiftCard.tsx
index b6f89e6..55a9824 100644
--- a/src/components/GiftCard.tsx
+++ b/src/components/GiftCard.tsx
@@ -24,7 +24,7 @@ export interface GiftCardProps {
faceValue?: string
/** 商品原价 */
originalPrice?: string
- /** 礼品卡类型:10-实物礼品卡 20-虚拟礼品卡 30-服务礼品卡 */
+ /** 礼品卡类型:10-礼品劵 20-虚拟礼品卡 30-服务礼品卡 */
type?: number
/** 状态:0-未使用 1-已使用 2-失效 */
status?: number
@@ -112,10 +112,10 @@ const GiftCard: React.FC = ({
// 获取礼品卡类型文本
const getTypeText = () => {
switch (type) {
- case 10: return '实物礼品卡'
+ case 10: return '礼品劵'
case 20: return '虚拟礼品卡'
case 30: return '服务礼品卡'
- default: return '礼品卡'
+ default: return '水票'
}
}
diff --git a/src/components/GiftCardGuide.tsx b/src/components/GiftCardGuide.tsx
index 6b1db57..b1e7eb1 100644
--- a/src/components/GiftCardGuide.tsx
+++ b/src/components/GiftCardGuide.tsx
@@ -51,7 +51,7 @@ const GiftCardGuide: React.FC = ({
title: '礼品卡类型说明',
icon: ,
content: [
- '🎁 实物礼品卡:需到指定地址领取商品',
+ '🎁 礼品劵:需到指定地址领取商品',
'💻 虚拟礼品卡:自动发放到账户余额',
'🛎️ 服务礼品卡:联系客服预约服务',
'⏰ 注意查看有效期,过期无法使用'
diff --git a/src/components/GiftCardShare.tsx b/src/components/GiftCardShare.tsx
index ea4e217..0d29302 100644
--- a/src/components/GiftCardShare.tsx
+++ b/src/components/GiftCardShare.tsx
@@ -28,10 +28,10 @@ const GiftCardShare: React.FC = ({
// 获取礼品卡类型文本
const getTypeText = () => {
switch (giftCard.type) {
- case 10: return '实物礼品卡'
+ case 10: return '礼品劵'
case 20: return '虚拟礼品卡'
case 30: return '服务礼品卡'
- default: return '礼品卡'
+ default: return '水票'
}
}
diff --git a/src/components/PaymentCountdown.md b/src/components/PaymentCountdown.md
index abe4137..b4c7d9a 100644
--- a/src/components/PaymentCountdown.md
+++ b/src/components/PaymentCountdown.md
@@ -1,6 +1,6 @@
# PaymentCountdown 支付倒计时组件
-基于订单创建时间的支付倒计时组件,支持静态显示和实时更新两种模式。
+基于订单过期时间(`expirationTime`)的支付倒计时组件,支持静态显示和实时更新两种模式。
## 功能特性
@@ -19,7 +19,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
// 订单列表页 - 静态显示
{
- const timeLeft = usePaymentCountdown(
- order.createTime,
- order.payStatus,
- true, // 实时更新
- 24 // 24小时超时
- );
+ const timeLeft = usePaymentCountdown({
+ expirationTime: order.expirationTime,
+ createTime: order.createTime, // expirationTime 缺失时回退
+ payStatus: order.payStatus,
+ realTime: true,
+ timeoutHours: 24
+ });
const countdownText = formatCountdownText(timeLeft, true);
diff --git a/src/components/PaymentCountdown.tsx b/src/components/PaymentCountdown.tsx
index c576c9e..2d6048e 100644
--- a/src/components/PaymentCountdown.tsx
+++ b/src/components/PaymentCountdown.tsx
@@ -11,6 +11,8 @@ import './PaymentCountdown.scss';
export interface PaymentCountdownProps {
/** 订单创建时间 */
createTime?: string;
+ /** 订单过期时间(推荐直接传后端返回的 expirationTime) */
+ expirationTime?: string;
/** 支付状态 */
payStatus?: boolean;
/** 是否实时更新(详情页用true,列表页用false) */
@@ -29,18 +31,25 @@ export interface PaymentCountdownProps {
const PaymentCountdown: React.FC = ({
createTime,
+ expirationTime,
payStatus = false,
realTime = false,
- timeoutHours = 1,
+ timeoutHours = 24,
showSeconds = false,
className = '',
onExpired,
mode = 'badge'
}) => {
- const timeLeft = usePaymentCountdown(createTime, payStatus, realTime, timeoutHours);
+ const timeLeft = usePaymentCountdown({
+ createTime,
+ expirationTime,
+ payStatus,
+ realTime,
+ timeoutHours
+ });
- // 如果已支付或没有创建时间,不显示倒计时
- if (payStatus || !createTime) {
+ // 如果已支付或没有可计算的截止时间,不显示倒计时
+ if (payStatus || (!expirationTime && !createTime)) {
return null;
}
diff --git a/src/components/SimpleQRCodeModal.tsx b/src/components/SimpleQRCodeModal.tsx
index 1acde23..c429754 100644
--- a/src/components/SimpleQRCodeModal.tsx
+++ b/src/components/SimpleQRCodeModal.tsx
@@ -81,7 +81,7 @@ const SimpleQRCodeModal: React.FC = ({
{qrContent ? (
= ({
setTimeout(() => {
Taro.showModal({
title: '核销成功',
- content: '是否继续扫码核销其他礼品卡?',
+ content: '是否继续扫码核销其他水票/礼品卡?',
success: (res) => {
if (res.confirm) {
handleClick(); // 递归调用继续扫码
diff --git a/src/dealer/apply/add.config.ts b/src/dealer/apply/add.config.ts
index ac37521..71f30bd 100644
--- a/src/dealer/apply/add.config.ts
+++ b/src/dealer/apply/add.config.ts
@@ -1,4 +1,4 @@
export default definePageConfig({
- navigationBarTitleText: '邀请注册',
+ navigationBarTitleText: '注册成为会员',
navigationBarTextStyle: 'black'
})
diff --git a/src/dealer/apply/add.tsx b/src/dealer/apply/add.tsx
index f862bf3..a6aea7f 100644
--- a/src/dealer/apply/add.tsx
+++ b/src/dealer/apply/add.tsx
@@ -10,7 +10,9 @@ import {updateUser} from "@/api/system/user";
import {User} from "@/api/system/user/model";
import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite";
import {addShopDealerUser} from "@/api/shop/shopDealerUser";
-import {listUserRole, updateUserRole} from "@/api/system/userRole";
+import {addUserRole, listUserRole, updateUserRole} from "@/api/system/userRole";
+import { listRoles } from "@/api/system/role";
+import type { UserRole } from "@/api/system/userRole/model";
// 类型定义
interface ChooseAvatarEvent {
@@ -26,7 +28,7 @@ interface InputEvent {
}
const AddUserAddress = () => {
- const {user, loginUser} = useUser()
+ const {user, loginUser, fetchUserInfo} = useUser()
const [loading, setLoading] = useState(true)
const [FormData, setFormData] = useState()
const formRef = useRef(null)
@@ -59,7 +61,7 @@ const AddUserAddress = () => {
setFormData(tempFormData)
Taro.uploadFile({
- url: 'https://server.websoft.top/api/oss/upload',
+ url: 'https://glt-server.websoft.top/api/oss/upload',
filePath: detail.avatarUrl,
name: 'file',
header: {
@@ -127,7 +129,7 @@ const AddUserAddress = () => {
}
// 提交表单
- const submitSucceed = async (values: any) => {
+ const submitSucceed = async (values: User) => {
try {
// 验证必填字段
if (!values.phone && !FormData?.phone) {
@@ -142,8 +144,8 @@ const AddUserAddress = () => {
const nickname = values.realName || FormData?.nickname || '';
if (!nickname || nickname.trim() === '') {
Taro.showToast({
- title: '请填写昵称',
- icon: 'error'
+ title: '请上传头像和填写昵称',
+ icon: 'none'
});
return;
}
@@ -176,12 +178,27 @@ const AddUserAddress = () => {
}
console.log(values,FormData)
- const roles = await listUserRole({userId: user?.userId})
- console.log(roles, 'roles...')
+ if (!user?.userId) {
+ Taro.showToast({
+ title: '用户信息缺失,请先登录',
+ icon: 'error'
+ });
+ return;
+ }
+
+ let roles: UserRole[] = [];
+ try {
+ roles = await listUserRole({userId: user.userId})
+ console.log(roles, 'roles...')
+ } catch (e) {
+ // 新用户/权限限制时可能查不到角色列表,不影响基础注册流程
+ console.warn('查询用户角色失败,将尝试直接写入默认角色:', e)
+ roles = []
+ }
// 准备提交的数据
await updateUser({
- userId: user?.userId,
+ userId: user.userId,
nickname: values.realName || FormData?.nickname,
phone: values.phone || FormData?.phone,
avatar: values.avatar || FormData?.avatar,
@@ -189,17 +206,55 @@ const AddUserAddress = () => {
});
await addShopDealerUser({
- userId: user?.userId,
+ userId: user.userId,
realName: values.realName || FormData?.nickname,
mobile: values.phone || FormData?.phone,
- refereeId: values.refereeId || FormData?.refereeId
+ refereeId: Number(values.refereeId) || Number(FormData?.refereeId)
})
- if (roles.length > 0) {
- await updateUserRole({
- ...roles[0],
- roleId: 1848
- })
+ // 通知其他页面(如“我的”页、分销中心页)刷新经销商信息
+ Taro.eventCenter.trigger('dealerUser:changed')
+
+ // 角色为空时这里会导致“注册成功但没有角色”,这里做一次兜底写入默认 user 角色
+ try {
+ // 1) 先尝试通过 roleCode=user 查询角色ID(避免硬编码)
+ // 2) 取不到就回退到旧的默认ID(1848)
+ let userRoleId: number | undefined;
+ try {
+ // 注意:当前 request.get 的封装不支持 axios 风格的 { params: ... },
+ // 某些自动生成的 API 可能无法按参数过滤;这里直接取全量再本地查找更稳。
+ const roleList = await listRoles();
+ userRoleId = roleList?.find(r => r.roleCode === 'user')?.roleId;
+ } catch (_) {
+ // ignore
+ }
+ if (!userRoleId) userRoleId = 1848;
+
+ const baseRolePayload = {
+ userId: user.userId,
+ tenantId: Number(TenantId),
+ roleId: userRoleId
+ };
+
+ // 后端若已创建 user-role 记录则更新;否则尝试“无id更新”触发创建(多数实现会 upsert)
+ if (roles.length > 0) {
+ await updateUserRole({
+ ...roles[0],
+ roleId: userRoleId
+ });
+ } else {
+ try {
+ await addUserRole(baseRolePayload);
+ } catch (_) {
+ // 兼容后端仅支持 PUT upsert 的情况
+ await updateUserRole(baseRolePayload);
+ }
+ }
+
+ // 刷新一次用户信息,确保 roles 写回本地缓存,避免“我的”页显示为空/不一致
+ await fetchUserInfo();
+ } catch (e) {
+ console.warn('写入默认角色失败(不影响注册成功):', e)
}
@@ -209,7 +264,8 @@ const AddUserAddress = () => {
});
setTimeout(() => {
- Taro.navigateBack();
+ // “我的”是 tabBar 页面,注册完成后直接切到“我的”
+ Taro.switchTab({ url: '/pages/user/user' });
}, 1000);
} catch (error) {
@@ -241,7 +297,7 @@ const AddUserAddress = () => {
success: (loginRes) => {
if (code) {
Taro.request({
- url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
+ url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
authCode: loginRes.code,
@@ -382,9 +438,9 @@ const AddUserAddress = () => {
>
-
-
-
+ {/**/}
+ {/* */}
+ {/**/}
{
+ const {dealerUser} = useDealerUser()
+
+ const [loading, setLoading] = useState(false)
+ const [refreshing, setRefreshing] = useState(false)
+ const [loadingMore, setLoadingMore] = useState(false)
+ const [currentPage, setCurrentPage] = useState(1)
+ const [hasMore, setHasMore] = useState(true)
+ const [records, setRecords] = useState([])
+
+ const getFlowTypeText = (flowType?: number) => {
+ switch (flowType) {
+ case 10:
+ return '佣金收入'
+ case 20:
+ return '提现支出'
+ case 30:
+ return '转账支出'
+ case 40:
+ return '转账收入'
+ default:
+ return '资金变动'
+ }
+ }
+
+ const getFlowTypeTag = (flowType?: number) => {
+ // 收入:success;支出:danger;其它:default
+ if (flowType === 10 || flowType === 40) return 'success'
+ if (flowType === 20 || flowType === 30) return 'danger'
+ return 'default'
+ }
+
+ const formatMoney = (flowType?: number, money?: string) => {
+ const isIncome = flowType === 10 || flowType === 40
+ const isExpense = flowType === 20 || flowType === 30
+ const sign = isIncome ? '+' : isExpense ? '-' : ''
+ return `${sign}${money || '0.00'}`
+ }
+
+ const fetchRecords = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
+ if (!dealerUser?.userId) return
+
+ try {
+ if (isRefresh) {
+ setRefreshing(true)
+ } else if (page === 1) {
+ setLoading(true)
+ } else {
+ setLoadingMore(true)
+ }
+
+ const result = await pageShopDealerCapital({
+ page,
+ limit: PAGE_SIZE,
+ // 只显示与当前登录用户相关的收益明细
+ userId: dealerUser.userId
+ })
+
+ const list = result?.list || []
+ if (page === 1) {
+ setRecords(list)
+ } else {
+ setRecords(prev => [...prev, ...list])
+ }
+
+ setHasMore(list.length === PAGE_SIZE)
+ setCurrentPage(page)
+ } catch (error) {
+ console.error('获取收益明细失败:', error)
+ Taro.showToast({
+ title: '获取收益明细失败',
+ icon: 'error'
+ })
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ setLoadingMore(false)
+ }
+ }, [dealerUser?.userId])
+
+ const handleRefresh = async () => {
+ await fetchRecords(1, true)
+ }
+
+ const handleLoadMore = async () => {
+ if (!loadingMore && hasMore) {
+ await fetchRecords(currentPage + 1)
+ }
+ }
+
+ useEffect(() => {
+ if (dealerUser?.userId) {
+ fetchRecords(1)
+ }
+ }, [fetchRecords, dealerUser?.userId])
+
+ if (!dealerUser) {
+ return (
+
+
+ 加载中...
+
+ )
+ }
+
+ return (
+
+
+
+
+ {loading && records.length === 0 ? (
+
+
+ 加载中...
+
+ ) : records.length > 0 ? (
+ <>
+ {records.map((item) => (
+
+
+
+ {item.describe || '收益明细'}
+
+
+ {getFlowTypeText(item.flowType)}
+
+
+
+
+
+ 佣金收入
+
+
+ {formatMoney(item.flowType, item.money)}
+
+
+
+
+
+ {/*用户:{item.userId ?? '-'}*/}
+
+
+ {item.createTime || '-'}
+
+
+
+ ))}
+
+ {loadingMore && (
+
+
+ 加载更多...
+
+ )}
+ {!hasMore && records.length > 0 && (
+
+ 没有更多数据了
+
+ )}
+ >
+ ) : (
+
+ )}
+
+
+
+
+ )
+}
+
+export default DealerCapital
diff --git a/src/dealer/index.config.ts b/src/dealer/index.config.ts
index d456dbd..babaa66 100644
--- a/src/dealer/index.config.ts
+++ b/src/dealer/index.config.ts
@@ -1,3 +1,3 @@
export default definePageConfig({
- navigationBarTitleText: '分销中心'
+ navigationBarTitleText: '账户管理中心'
})
diff --git a/src/dealer/index.tsx b/src/dealer/index.tsx
index 06c514f..ca1f6d7 100644
--- a/src/dealer/index.tsx
+++ b/src/dealer/index.tsx
@@ -108,7 +108,7 @@ const DealerIndex: React.FC = () => {
- ID: {dealerUser.userId} | 推荐人: {dealerUser.refereeId || '无'}
+ ID: {dealerUser.userId}
@@ -129,26 +129,26 @@ const DealerIndex: React.FC = () => {
{dealerUser && (
- 佣金统计
+ 资金统计
-
+ }} onClick={() => navigateToPage('/dealer/withdraw/index')}>
{formatMoney(dealerUser.money)}
可提现
-
{formatMoney(dealerUser.freezeMoney)}
- 冻结中
+ 待使用
-
@@ -217,7 +217,7 @@ const DealerIndex: React.FC = () => {
- navigateToPage('/dealer/withdraw/index')}>
+ navigateToPage('/dealer/withdraw/index')}>
@@ -225,7 +225,7 @@ const DealerIndex: React.FC = () => {
- navigateToPage('/dealer/team/index')}>
+ navigateToPage('/dealer/team/index')}>
@@ -233,7 +233,7 @@ const DealerIndex: React.FC = () => {
- navigateToPage('/dealer/qrcode/index')}>
+ navigateToPage('/user/userVerify/index')}>
diff --git a/src/dealer/orders/index.tsx b/src/dealer/orders/index.tsx
index f3b6bbf..419586c 100644
--- a/src/dealer/orders/index.tsx
+++ b/src/dealer/orders/index.tsx
@@ -24,7 +24,8 @@ const DealerOrders: React.FC = () => {
// 获取订单数据
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
- if (!dealerUser?.userId) return
+ // 需要当前登录用户ID(用于 resourceId 参数)
+ if (!dealerUser || !dealerUser.userId) return
try {
if (isRefresh) {
@@ -37,14 +38,17 @@ const DealerOrders: React.FC = () => {
const result = await pageShopDealerOrder({
page,
- limit: 10
+ limit: 10,
+ // 后端需要 resourceId=当前登录用户ID 才能正确过滤分销订单
+ resourceId: dealerUser.userId
})
if (result?.list) {
const newOrders = result.list.map(order => ({
...order,
- orderNo: `${order.orderId}`,
- customerName: `用户${order.userId}`,
+ // 优先使用接口返回的订单号;没有则降级展示 orderId
+ orderNo: order.orderNo ?? (order.orderId != null ? String(order.orderId) : undefined),
+ customerName: `${order.nickname}${order.userId}`,
userCommission: order.firstMoney || '0.00'
}))
@@ -90,44 +94,53 @@ const DealerOrders: React.FC = () => {
}
}, [fetchOrders])
- const getStatusText = (isSettled?: number, isInvalid?: number) => {
+ const getStatusText = (isSettled?: number, isInvalid?: number, isUnfreeze?: number, orderStatus?: number) => {
+ if (orderStatus === 2 || orderStatus === 5 || orderStatus === 6) return '已取消'
if (isInvalid === 1) return '已失效'
+ if (isUnfreeze === 1) return '已解冻'
if (isSettled === 1) return '已结算'
return '待结算'
}
- const getStatusColor = (isSettled?: number, isInvalid?: number) => {
+ const getStatusColor = (isSettled?: number, isInvalid?: number, isUnfreeze?: number, orderStatus?: number) => {
+ if (orderStatus === 2 || orderStatus === 5 || orderStatus === 6) return 'default'
if (isInvalid === 1) return 'danger'
- if (isSettled === 1) return 'success'
+ if (isUnfreeze === 1) return 'success'
+ if (isSettled === 1) return 'info'
return 'warning'
}
+ const handleGoCapital = () => {
+ Taro.navigateTo({url: '/dealer/capital/index'})
+ }
+
const renderOrderItem = (order: OrderWithDetails) => (
-
+
- 订单号:{order.orderNo}
+ 订单号:{order.orderNo || '-'}
-
- {getStatusText(order.isSettled, order.isInvalid)}
+
+ {getStatusText(order.isSettled, order.isInvalid, order.isUnfreeze,order.orderStatus)}
-
-
- 订单金额:¥{order.orderPrice || '0.00'}
-
-
- 我的佣金:¥{order.userCommission}
-
-
+ {/**/}
+ {/* */}
+ {/* 订单金额:¥{order.orderPrice || '0.00'}*/}
+ {/* */}
+ {/**/}
- 客户:{order.customerName}
+ {order.createTime}
- {order.createTime}
+ 订单金额:¥{order.orderPrice || '0.00'}
diff --git a/src/dealer/qrcode/index.config.ts b/src/dealer/qrcode/index.config.ts
index b075b21..7abe843 100644
--- a/src/dealer/qrcode/index.config.ts
+++ b/src/dealer/qrcode/index.config.ts
@@ -1,3 +1,6 @@
export default definePageConfig({
- navigationBarTitleText: '推广二维码'
+ navigationBarTitleText: '账户管理中心',
+ // Enable "Share to friends" and "Share to Moments" (timeline) for this page.
+ enableShareAppMessage: true,
+ enableShareTimeline: true
})
diff --git a/src/dealer/qrcode/index.tsx b/src/dealer/qrcode/index.tsx
index 90edb58..a721389 100644
--- a/src/dealer/qrcode/index.tsx
+++ b/src/dealer/qrcode/index.tsx
@@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react'
import {View, Text, Image} from '@tarojs/components'
import {Button, Loading} from '@nutui/nutui-react-taro'
import {Download, QrCode} from '@nutui/icons-react-taro'
-import Taro from '@tarojs/taro'
+import Taro, {useShareAppMessage} from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {generateInviteCode} from '@/api/invite'
// import type {InviteStats} from '@/api/invite'
@@ -10,10 +10,44 @@ import {businessGradients} from '@/styles/gradients'
const DealerQrcode: React.FC = () => {
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState('')
- const [loading, setLoading] = useState(false)
+ const [codeLoading, setCodeLoading] = useState(false)
+ const [saving, setSaving] = useState(false)
// const [inviteStats, setInviteStats] = useState(null)
// const [statsLoading, setStatsLoading] = useState(false)
- const {dealerUser} = useDealerUser()
+ const {dealerUser, loading: dealerLoading, error, refresh} = useDealerUser()
+
+ // Enable "转发给朋友" + "分享到朋友圈" items in the share panel/menu.
+ useEffect(() => {
+ // Some clients require explicit call to show both share entries.
+ Taro.showShareMenu({
+ withShareTicket: true,
+ showShareItems: ['shareAppMessage', 'shareTimeline']
+ }).catch(() => {})
+ }, [])
+
+ // 转发给朋友(分享小程序链接)
+ useShareAppMessage(() => {
+ const inviterRaw = dealerUser?.userId ?? Taro.getStorageSync('UserId')
+ const inviter = Number(inviterRaw)
+ const hasInviter = Number.isFinite(inviter) && inviter > 0
+
+ const user = Taro.getStorageSync('User') || {}
+ const nickname = (user && (user.nickname || user.realName || user.username)) || ''
+ const title = hasInviter ? `${nickname || '我'}邀请你加入桂乐淘伙伴计划` : '桂乐淘伙伴计划'
+
+ return {
+ title,
+ path: hasInviter
+ ? `/pages/index/index?inviter=${inviter}&source=dealer_qrcode&t=${Date.now()}`
+ : `/pages/index/index`,
+ success: function () {
+ Taro.showToast({title: '分享成功', icon: 'success', duration: 2000})
+ },
+ fail: function () {
+ Taro.showToast({title: '分享失败', icon: 'none', duration: 2000})
+ }
+ }
+ })
// 生成小程序码
const generateMiniProgramCode = async () => {
@@ -22,7 +56,7 @@ const DealerQrcode: React.FC = () => {
}
try {
- setLoading(true)
+ setCodeLoading(true)
// 生成邀请小程序码
const codeUrl = await generateInviteCode(dealerUser.userId)
@@ -40,7 +74,7 @@ const DealerQrcode: React.FC = () => {
// 清空之前的二维码
setMiniProgramCodeUrl('')
} finally {
- setLoading(false)
+ setCodeLoading(false)
}
}
@@ -67,6 +101,66 @@ const DealerQrcode: React.FC = () => {
}
}, [dealerUser?.userId])
+ const isAlbumAuthError = (errMsg?: string) => {
+ if (!errMsg) return false
+ // WeChat uses variants like: "saveImageToPhotosAlbum:fail auth deny",
+ // "saveImageToPhotosAlbum:fail auth denied", "authorize:fail auth deny"
+ return (
+ errMsg.includes('auth deny') ||
+ errMsg.includes('auth denied') ||
+ errMsg.includes('authorize') ||
+ errMsg.includes('scope.writePhotosAlbum')
+ )
+ }
+
+ const ensureWriteAlbumPermission = async (): Promise => {
+ try {
+ const setting = await Taro.getSetting()
+ if (setting?.authSetting?.['scope.writePhotosAlbum']) return true
+
+ await Taro.authorize({scope: 'scope.writePhotosAlbum'})
+ return true
+ } catch (error: any) {
+ const modal = await Taro.showModal({
+ title: '提示',
+ content: '需要您授权保存图片到相册,请在设置中开启相册权限',
+ confirmText: '去设置'
+ })
+ if (modal.confirm) {
+ await Taro.openSetting()
+ }
+ return false
+ }
+ }
+
+ const downloadImageToLocalPath = async (url: string): Promise => {
+ // saveImageToPhotosAlbum must receive a local temp path (e.g. `http://tmp/...` or `wxfile://...`).
+ // Some environments may return a non-existing temp path from getImageInfo, so we verify.
+ if (url.startsWith('http://tmp/') || url.startsWith('wxfile://')) {
+ return url
+ }
+
+ const token = Taro.getStorageSync('access_token')
+ const tenantId = Taro.getStorageSync('TenantId')
+ const header: Record = {}
+ if (token) header.Authorization = token
+ if (tenantId) header.TenantId = tenantId
+
+ // 先下载到本地临时文件再保存到相册
+ const res = await Taro.downloadFile({url, header})
+ if (res.statusCode !== 200 || !res.tempFilePath) {
+ throw new Error(`图片下载失败(${res.statusCode || 'unknown'})`)
+ }
+
+ // Double-check file exists to avoid: saveImageToPhotosAlbum:fail no such file or directory
+ try {
+ await Taro.getFileInfo({filePath: res.tempFilePath})
+ } catch (_) {
+ throw new Error('图片临时文件不存在,请重试')
+ }
+ return res.tempFilePath
+ }
+
// 保存小程序码到相册
const saveMiniProgramCode = async () => {
if (!miniProgramCodeUrl) {
@@ -78,39 +172,64 @@ const DealerQrcode: React.FC = () => {
}
try {
- // 先下载图片到本地
- const res = await Taro.downloadFile({
- url: miniProgramCodeUrl
- })
+ if (saving) return
+ setSaving(true)
+ Taro.showLoading({title: '保存中...'})
- if (res.statusCode === 200) {
- // 保存到相册
- await Taro.saveImageToPhotosAlbum({
- filePath: res.tempFilePath
- })
+ const hasPermission = await ensureWriteAlbumPermission()
+ if (!hasPermission) return
- Taro.showToast({
- title: '保存成功',
- icon: 'success'
- })
+ let filePath = await downloadImageToLocalPath(miniProgramCodeUrl)
+ try {
+ await Taro.saveImageToPhotosAlbum({filePath})
+ } catch (e: any) {
+ const msg = e?.errMsg || e?.message || ''
+ // Fallback: some devices/clients may fail to save directly from a temp path.
+ if (
+ msg.includes('no such file or directory') &&
+ (filePath.startsWith('http://tmp/') || filePath.startsWith('wxfile://'))
+ ) {
+ const saved = (await Taro.saveFile({tempFilePath: filePath})) as unknown as { savedFilePath?: string }
+ if (saved?.savedFilePath) {
+ filePath = saved.savedFilePath
+ }
+ await Taro.saveImageToPhotosAlbum({filePath})
+ } else {
+ throw e
+ }
}
+
+ Taro.showToast({
+ title: '保存成功',
+ icon: 'success'
+ })
} catch (error: any) {
- if (error.errMsg?.includes('auth deny')) {
- Taro.showModal({
+ const errMsg = error?.errMsg || error?.message
+ if (errMsg?.includes('cancel')) {
+ Taro.showToast({title: '已取消', icon: 'none'})
+ return
+ }
+
+ if (isAlbumAuthError(errMsg)) {
+ const modal = await Taro.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
- success: (res) => {
- if (res.confirm) {
- Taro.openSetting()
- }
- }
+ confirmText: '去设置'
})
+ if (modal.confirm) {
+ await Taro.openSetting()
+ }
} else {
- Taro.showToast({
+ // Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
+ await Taro.showModal({
title: '保存失败',
- icon: 'error'
+ content: errMsg || '保存失败,请稍后重试',
+ showCancel: false
})
}
+ } finally {
+ Taro.hideLoading()
+ setSaving(false)
}
}
@@ -126,7 +245,7 @@ const DealerQrcode: React.FC = () => {
//
// const inviteText = `🎉 邀请您加入我的团队!
//
-// 扫描小程序码或搜索"时里院子市集"小程序,即可享受优质商品和服务!
+// 扫描小程序码或搜索"桂乐淘"小程序,即可享受优质商品和服务!
//
// 💰 成为我的团队成员,一起赚取丰厚佣金
// 🎁 新用户专享优惠等你来拿
@@ -162,7 +281,7 @@ const DealerQrcode: React.FC = () => {
// })
// }
- if (!dealerUser) {
+ if (dealerLoading) {
return (
@@ -171,6 +290,33 @@ const DealerQrcode: React.FC = () => {
)
}
+ if (error) {
+ return (
+
+ 加载失败
+ {error}
+
+
+ )
+ }
+
+ // 未成为分销商时给出明确引导,避免一直停留在“加载中”
+ if (!dealerUser) {
+ return (
+
+ 你还不是分销商
+ 申请成为分销商后即可生成分享码
+
+
+ )
+ }
+
return (
{/* 头部卡片 */}
@@ -185,9 +331,9 @@ const DealerQrcode: React.FC = () => {
}}>
- 我的邀请小程序码
+ 我的分享码
- 分享小程序码邀请好友,获得丰厚佣金奖励
+ 与好友“共享福利 一起省、一起赚”
@@ -196,7 +342,7 @@ const DealerQrcode: React.FC = () => {
{/* 小程序码展示区 */}
- {loading ? (
+ {codeLoading ? (
生成中...
@@ -239,10 +385,10 @@ const DealerQrcode: React.FC = () => {
)}
- 扫码加入我的团队
+ 桂乐淘伙伴计划
- 好友扫描小程序码即可直接进入小程序并建立邀请关系
+ 自购省 | 分享赚 | 好友惠
@@ -258,34 +404,12 @@ const DealerQrcode: React.FC = () => {
block
icon={}
onClick={saveMiniProgramCode}
- disabled={!miniProgramCodeUrl || loading}
+ disabled={!miniProgramCodeUrl || codeLoading || saving}
>
保存小程序码到相册
- {/**/}
- {/* */}
- {/**/}
- {/**/}
- {/* */}
- {/**/}
+
{/* 推广说明 */}
diff --git a/src/dealer/team/index.tsx b/src/dealer/team/index.tsx
index af7d8e7..cc22373 100644
--- a/src/dealer/team/index.tsx
+++ b/src/dealer/team/index.tsx
@@ -325,7 +325,7 @@ const DealerTeam: React.FC = () => {
{/* 显示手机号(仅本级可见) */}
{showPhone && member.phone && (
- {
+ {
e.stopPropagation();
makePhoneCall(member.phone || '');
}}>
diff --git a/src/dealer/withdraw/__tests__/withdraw.test.tsx b/src/dealer/withdraw/__tests__/withdraw.test.tsx
deleted file mode 100644
index c3aeab9..0000000
--- a/src/dealer/withdraw/__tests__/withdraw.test.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-import React from 'react'
-import { render, fireEvent, waitFor } from '@testing-library/react'
-import DealerWithdraw from '../index'
-import { useDealerUser } from '@/hooks/useDealerUser'
-import * as withdrawAPI from '@/api/shop/shopDealerWithdraw'
-
-// Mock dependencies
-jest.mock('@/hooks/useDealerUser')
-jest.mock('@/api/shop/shopDealerWithdraw')
-jest.mock('@tarojs/taro', () => ({
- showToast: jest.fn(),
- getStorageSync: jest.fn(() => 123),
-}))
-
-const mockUseDealerUser = useDealerUser as jest.MockedFunction
-const mockAddShopDealerWithdraw = withdrawAPI.addShopDealerWithdraw as jest.MockedFunction
-const mockPageShopDealerWithdraw = withdrawAPI.pageShopDealerWithdraw as jest.MockedFunction
-
-describe('DealerWithdraw', () => {
- const mockDealerUser = {
- userId: 123,
- money: '10000.00',
- realName: '测试用户',
- mobile: '13800138000'
- }
-
- beforeEach(() => {
- mockUseDealerUser.mockReturnValue({
- dealerUser: mockDealerUser,
- loading: false,
- error: null,
- refresh: jest.fn()
- })
-
- mockPageShopDealerWithdraw.mockResolvedValue({
- list: [],
- count: 0
- })
- })
-
- afterEach(() => {
- jest.clearAllMocks()
- })
-
- test('应该正确显示可提现余额', () => {
- const { getByText } = render()
- expect(getByText('10000.00')).toBeInTheDocument()
- expect(getByText('可提现余额')).toBeInTheDocument()
- })
-
- test('应该验证最低提现金额', async () => {
- mockAddShopDealerWithdraw.mockResolvedValue('success')
-
- const { getByPlaceholderText, getByText } = render()
-
- // 输入低于最低金额的数值
- const amountInput = getByPlaceholderText('请输入提现金额')
- fireEvent.change(amountInput, { target: { value: '50' } })
-
- // 选择提现方式
- const wechatRadio = getByText('微信钱包')
- fireEvent.click(wechatRadio)
-
- // 提交表单
- const submitButton = getByText('申请提现')
- fireEvent.click(submitButton)
-
- await waitFor(() => {
- expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
- title: '最低提现金额为100元',
- icon: 'error'
- })
- })
- })
-
- test('应该验证提现金额不超过可用余额', async () => {
- const { getByPlaceholderText, getByText } = render()
-
- // 输入超过可用余额的金额
- const amountInput = getByPlaceholderText('请输入提现金额')
- fireEvent.change(amountInput, { target: { value: '20000' } })
-
- // 选择提现方式
- const wechatRadio = getByText('微信钱包')
- fireEvent.click(wechatRadio)
-
- // 提交表单
- const submitButton = getByText('申请提现')
- fireEvent.click(submitButton)
-
- await waitFor(() => {
- expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
- title: '提现金额超过可用余额',
- icon: 'error'
- })
- })
- })
-
- test('应该验证支付宝账户信息完整性', async () => {
- const { getByPlaceholderText, getByText } = render()
-
- // 输入有效金额
- const amountInput = getByPlaceholderText('请输入提现金额')
- fireEvent.change(amountInput, { target: { value: '1000' } })
-
- // 选择支付宝提现
- const alipayRadio = getByText('支付宝')
- fireEvent.click(alipayRadio)
-
- // 只填写账号,不填写姓名
- const accountInput = getByPlaceholderText('请输入支付宝账号')
- fireEvent.change(accountInput, { target: { value: 'test@alipay.com' } })
-
- // 提交表单
- const submitButton = getByText('申请提现')
- fireEvent.click(submitButton)
-
- await waitFor(() => {
- expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
- title: '请填写完整的支付宝信息',
- icon: 'error'
- })
- })
- })
-
- test('应该成功提交微信提现申请', async () => {
- mockAddShopDealerWithdraw.mockResolvedValue('success')
-
- const { getByPlaceholderText, getByText } = render()
-
- // 输入有效金额
- const amountInput = getByPlaceholderText('请输入提现金额')
- fireEvent.change(amountInput, { target: { value: '1000' } })
-
- // 选择微信提现
- const wechatRadio = getByText('微信钱包')
- fireEvent.click(wechatRadio)
-
- // 提交表单
- const submitButton = getByText('申请提现')
- fireEvent.click(submitButton)
-
- await waitFor(() => {
- expect(mockAddShopDealerWithdraw).toHaveBeenCalledWith({
- userId: 123,
- money: '1000',
- payType: 10,
- applyStatus: 10,
- platform: 'MiniProgram'
- })
- })
-
- await waitFor(() => {
- expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
- title: '提现申请已提交',
- icon: 'success'
- })
- })
- })
-
- test('快捷金额按钮应该正常工作', () => {
- const { getByText, getByPlaceholderText } = render()
-
- // 点击快捷金额按钮
- const quickAmountButton = getByText('500')
- fireEvent.click(quickAmountButton)
-
- // 验证金额输入框的值
- const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
- expect(amountInput.value).toBe('500')
- })
-
- test('全部按钮应该设置为可用余额', () => {
- const { getByText, getByPlaceholderText } = render()
-
- // 点击全部按钮
- const allButton = getByText('全部')
- fireEvent.click(allButton)
-
- // 验证金额输入框的值
- const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
- expect(amountInput.value).toBe('10000.00')
- })
-})
diff --git a/src/dealer/withdraw/index.tsx b/src/dealer/withdraw/index.tsx
index 04a3cef..e8274a9 100644
--- a/src/dealer/withdraw/index.tsx
+++ b/src/dealer/withdraw/index.tsx
@@ -1,13 +1,11 @@
import React, {useState, useRef, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {
- Cell,
Space,
Button,
Form,
Input,
CellGroup,
- Radio,
Tabs,
Tag,
Empty,
@@ -18,32 +16,109 @@ import {Wallet} from '@nutui/icons-react-taro'
import {businessGradients} from '@/styles/gradients'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
-import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
+import {myUserVerify} from '@/api/system/userVerify'
+import {goTo} from '@/utils/navigation'
+import {
+ pageShopDealerWithdraw,
+ addShopDealerWithdraw,
+ receiveShopDealerWithdraw,
+ receiveSuccessShopDealerWithdraw
+} from '@/api/shop/shopDealerWithdraw'
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
accountDisplay?: string
+ // Backend may include these fields for WeChat "confirm receipt" flow after approval.
+ package_info?: string
+ packageInfo?: string
+ package?: string
+}
+
+const extractPackageInfo = (result: unknown): string | null => {
+ if (typeof result === 'string') return result
+ if (!result || typeof result !== 'object') return null
+ const r = result as any
+ return (
+ r.package_info ??
+ r.packageInfo ??
+ r.package ??
+ null
+ )
+}
+
+const canRequestMerchantTransferConfirm = (): boolean => {
+ try {
+ if (typeof (Taro as any).getEnv === 'function' && (Taro as any).ENV_TYPE) {
+ const env = (Taro as any).getEnv()
+ if (env !== (Taro as any).ENV_TYPE.WEAPP) return false
+ }
+
+ const api =
+ (globalThis as any).wx?.requestMerchantTransfer ||
+ (Taro as any).requestMerchantTransfer
+
+ return typeof api === 'function'
+ } catch {
+ return false
+ }
+}
+
+const requestMerchantTransferConfirm = (packageInfo: string): Promise => {
+ if (!canRequestMerchantTransferConfirm()) {
+ return Promise.reject(new Error('请在微信小程序内完成收款确认'))
+ }
+
+ // Backend may wrap/format base64 with newlines; WeChat API requires a clean string.
+ const cleanPackageInfo = String(packageInfo).replace(/\s+/g, '')
+
+ const api =
+ (globalThis as any).wx?.requestMerchantTransfer ||
+ (Taro as any).requestMerchantTransfer
+
+ if (typeof api !== 'function') {
+ return Promise.reject(new Error('当前环境不支持商家转账收款确认(缺少 requestMerchantTransfer)'))
+ }
+
+ return new Promise((resolve, reject) => {
+ api({
+ // WeChat API uses `package`, backend returns `package_info`.
+ package: cleanPackageInfo,
+ mchId: '1737910695',
+ appId: 'wxad831ba00ad6a026',
+ success: (res: any) => resolve(res),
+ fail: (err: any) => reject(err)
+ })
+ })
+}
+
+// Some backends may return money fields as number; keep internal usage always as string.
+const normalizeMoneyString = (money: unknown) => {
+ if (money === null || money === undefined || money === '') return '0.00'
+ return typeof money === 'string' ? money : String(money)
}
const DealerWithdraw: React.FC = () => {
- const [activeTab, setActiveTab] = useState('0')
- const [selectedAccount, setSelectedAccount] = useState('')
+ const [activeTab, setActiveTab] = useState('0')
const [loading, setLoading] = useState(false)
const [refreshing, setRefreshing] = useState(false)
const [submitting, setSubmitting] = useState(false)
+ const [claimingId, setClaimingId] = useState(null)
const [availableAmount, setAvailableAmount] = useState('0.00')
const [withdrawRecords, setWithdrawRecords] = useState([])
const formRef = useRef(null)
const {dealerUser} = useDealerUser()
+ const [verifyStatus, setVerifyStatus] = useState<'unknown' | 'verified' | 'unverified' | 'pending' | 'rejected'>('unknown')
+ const [verifyStatusText, setVerifyStatusText] = useState('')
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换到:', value)
- setActiveTab(value)
+ const next = String(value)
+ setActiveTab(next)
// 如果切换到提现记录页面,刷新数据
- if (String(value) === '1') {
+ if (next === '1') {
fetchWithdrawRecords()
}
}
@@ -52,7 +127,7 @@ const DealerWithdraw: React.FC = () => {
const fetchBalance = useCallback(async () => {
console.log(dealerUser, 'dealerUser...')
try {
- setAvailableAmount(dealerUser?.money || '0.00')
+ setAvailableAmount(normalizeMoneyString(dealerUser?.money))
} catch (error) {
console.error('获取余额失败:', error)
}
@@ -115,12 +190,63 @@ const DealerWithdraw: React.FC = () => {
}
}, [fetchBalance, fetchWithdrawRecords])
+ // 判断实名认证状态:提现前必须完成实名认证(已通过)
+ const fetchVerifyStatus = useCallback(async () => {
+ // Fast path: some pages store this flag after login.
+ if (String(Taro.getStorageSync('Certification')) === '1') {
+ setVerifyStatus('verified')
+ setVerifyStatusText('已实名认证')
+ return
+ }
+
+ try {
+ const r = await myUserVerify({})
+ if (!r) {
+ setVerifyStatus('unverified')
+ setVerifyStatusText('未实名认证')
+ return
+ }
+
+ const s = Number((r as any).status)
+ const st = String((r as any).statusText || '')
+
+ // Common convention in this project: 0审核中/待审核, 1已通过, 2已驳回
+ if (s === 1) {
+ setVerifyStatus('verified')
+ setVerifyStatusText(st || '已实名认证')
+ return
+ }
+ if (s === 0) {
+ setVerifyStatus('pending')
+ setVerifyStatusText(st || '审核中')
+ return
+ }
+ if (s === 2) {
+ setVerifyStatus('rejected')
+ setVerifyStatusText(st || '已驳回')
+ return
+ }
+
+ setVerifyStatus('unverified')
+ setVerifyStatusText(st || '未实名认证')
+ } catch (e) {
+ console.warn('获取实名认证状态失败,将按未认证处理:', e)
+ setVerifyStatus('unverified')
+ setVerifyStatusText('未实名认证')
+ }
+ }, [])
+
+ useEffect(() => {
+ if (!dealerUser?.userId) return
+ fetchVerifyStatus().then()
+ }, [dealerUser?.userId, fetchVerifyStatus])
+
const getStatusText = (status?: number) => {
switch (status) {
case 40:
return '已到账'
case 20:
- return '审核通过'
+ return '待领取'
case 10:
return '待审核'
case 30:
@@ -135,7 +261,7 @@ const DealerWithdraw: React.FC = () => {
case 40:
return 'success'
case 20:
- return 'success'
+ return 'info'
case 10:
return 'warning'
case 30:
@@ -154,17 +280,17 @@ const DealerWithdraw: React.FC = () => {
return
}
- if (!values.accountType) {
+ if (verifyStatus !== 'verified') {
Taro.showToast({
- title: '请选择提现方式',
- icon: 'error'
+ title: '请先完成实名认证',
+ icon: 'none'
})
return
}
// 验证提现金额
- const amount = parseFloat(values.amount)
- const available = parseFloat(availableAmount.replace(/,/g, ''))
+ const amount = parseFloat(String(values.amount))
+ const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, ''))
if (isNaN(amount) || amount <= 0) {
Taro.showToast({
@@ -175,72 +301,41 @@ const DealerWithdraw: React.FC = () => {
}
if (amount < 100) {
- Taro.showToast({
- title: '最低提现金额为100元',
- icon: 'error'
- })
- return
+ // Taro.showToast({
+ // title: '最低提现金额为100元',
+ // icon: 'error'
+ // })
+ // return
}
if (amount > available) {
Taro.showToast({
title: '提现金额超过可用余额',
- icon: 'error'
+ icon: 'none'
})
return
}
- // 验证账户信息
- if (values.accountType === 'alipay') {
- if (!values.account || !values.accountName) {
- Taro.showToast({
- title: '请填写完整的支付宝信息',
- icon: 'error'
- })
- return
- }
- } else if (values.accountType === 'bank') {
- if (!values.account || !values.accountName || !values.bankName) {
- Taro.showToast({
- title: '请填写完整的银行卡信息',
- icon: 'error'
- })
- return
- }
- }
-
try {
setSubmitting(true)
const withdrawData: ShopDealerWithdraw = {
userId: dealerUser.userId,
money: values.amount,
- payType: values.accountType === 'wechat' ? 10 :
- values.accountType === 'alipay' ? 20 : 30,
- applyStatus: 10, // 待审核
+ // Only support WeChat wallet withdrawals.
+ payType: 10,
platform: 'MiniProgram'
}
- // 根据提现方式设置账户信息
- if (values.accountType === 'alipay') {
- withdrawData.alipayAccount = values.account
- withdrawData.alipayName = values.accountName
- } else if (values.accountType === 'bank') {
- withdrawData.bankCard = values.account
- withdrawData.bankAccount = values.accountName
- withdrawData.bankName = values.bankName || '银行卡'
- }
-
+ // Security flow:
+ // 1) user submits => applyStatus=10 (待审核)
+ // 2) backend审核通过 => applyStatus=20 (待领取)
+ // 3) user goes to records to "领取" => applyStatus=40 (已到账)
await addShopDealerWithdraw(withdrawData)
-
- Taro.showToast({
- title: '提现申请已提交',
- icon: 'success'
- })
+ Taro.showToast({title: '提现申请已提交,等待审核', icon: 'success'})
// 重置表单
formRef.current?.resetFields()
- setSelectedAccount('')
// 刷新数据
await handleRefresh()
@@ -259,6 +354,65 @@ const DealerWithdraw: React.FC = () => {
}
}
+ const handleClaim = async (record: WithdrawRecordWithDetails) => {
+ if (!record?.id) {
+ Taro.showToast({title: '记录不存在', icon: 'error'})
+ return
+ }
+
+ if (record.applyStatus !== 20) {
+ Taro.showToast({title: '当前状态不可领取', icon: 'none'})
+ return
+ }
+
+ if (record.payType !== 10) {
+ Taro.showToast({title: '仅支持微信提现领取', icon: 'none'})
+ return
+ }
+
+ if (claimingId !== null) return
+
+ try {
+ setClaimingId(record.id)
+
+ if (!canRequestMerchantTransferConfirm()) {
+ throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作')
+ }
+
+ const receiveResult = await receiveShopDealerWithdraw(record.id)
+ const packageInfo = extractPackageInfo(receiveResult)
+ if (!packageInfo) {
+ throw new Error('后台未返回 package_info,无法领取,请联系管理员')
+ }
+
+ try {
+ await requestMerchantTransferConfirm(packageInfo)
+ } catch (e: any) {
+ const msg = String(e?.errMsg || e?.message || '')
+ if (/cancel/i.test(msg)) {
+ Taro.showToast({title: '已取消领取', icon: 'none'})
+ return
+ }
+ throw new Error(msg || '领取失败,请稍后重试')
+ }
+
+ try {
+ await receiveSuccessShopDealerWithdraw(record.id)
+ Taro.showToast({title: '领取成功', icon: 'success'})
+ } catch (e: any) {
+ console.warn('领取成功,但状态同步失败:', e)
+ Taro.showToast({title: '已收款,状态更新失败,请稍后刷新', icon: 'none'})
+ } finally {
+ await handleRefresh()
+ }
+ } catch (e: any) {
+ console.error('领取失败:', e)
+ Taro.showToast({title: e?.message || '领取失败', icon: 'error'})
+ } finally {
+ setClaimingId(null)
+ }
+ }
+
const quickAmounts = ['100', '300', '500', '1000']
const setQuickAmount = (amount: string) => {
@@ -266,17 +420,37 @@ const DealerWithdraw: React.FC = () => {
}
const setAllAmount = () => {
- formRef.current?.setFieldsValue({amount: availableAmount.replace(/,/g, '')})
+ formRef.current?.setFieldsValue({amount: normalizeMoneyString(availableAmount).replace(/,/g, '')})
}
// 格式化金额
- const formatMoney = (money?: string) => {
- if (!money) return '0.00'
- return parseFloat(money).toFixed(2)
+ const formatMoney = (money?: unknown) => {
+ const n = parseFloat(normalizeMoneyString(money).replace(/,/g, ''))
+ return Number.isFinite(n) ? n.toFixed(2) : '0.00'
+ }
+
+ const goVerify = () => {
+ goTo('/user/userVerify/index')
}
const renderWithdrawForm = () => (
+ {(verifyStatus === 'unverified' || verifyStatus === 'pending' || verifyStatus === 'rejected') && (
+
+
+
+ 提现前请先完成实名认证
+ {verifyStatusText ? (
+ 当前状态:{verifyStatusText}
+ ) : null}
+
+
+ 去认证
+
+
+
+ )}
+
{/* 余额卡片 */}
{
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
}}>
- 最低提现金额:¥100 | 手续费:免费
+ 手续费:免费
@@ -314,18 +488,10 @@ const DealerWithdraw: React.FC = () => {
labelPosition="top"
>
-
+
{
- // 实时验证提现金额
- const amount = parseFloat(value)
- const available = parseFloat(availableAmount.replace(/,/g, ''))
- if (!isNaN(amount) && amount > available) {
- // 可以在这里添加实时提示,但不阻止输入
- }
- }}
/>
@@ -353,54 +519,14 @@ const DealerWithdraw: React.FC = () => {
-
- setSelectedAccount}>
-
-
- 微信钱包
- |
-
- 支付宝
- |
-
- 银行卡
- |
-
-
-
-
- {selectedAccount === 'alipay' && (
- <>
-
-
-
-
-
-
- >
- )}
-
- {selectedAccount === 'bank' && (
- <>
-
-
-
-
-
-
-
-
-
- >
- )}
-
- {selectedAccount === 'wechat' && (
-
-
- 微信钱包提现将直接转入您的微信零钱
-
-
- )}
+
+
+ 注意事项:
+ 1. 提取佣金必须完成实名认证。
+ 2. 佣金非自动到账,在您提取佣金申请通过后,请手动到我的申请记录点击领取。
+ 3. 桂乐淘温馨提示,请您依法依规申报所得,缴税相关税费。
+
+
@@ -409,7 +535,7 @@ const DealerWithdraw: React.FC = () => {
type="primary"
nativeType="submit"
loading={submitting}
- disabled={submitting || !selectedAccount}
+ disabled={submitting || verifyStatus !== 'verified'}
>
{submitting ? '提交中...' : '申请提现'}
@@ -433,35 +559,53 @@ const DealerWithdraw: React.FC = () => {
加载中...
) : withdrawRecords.length > 0 ? (
- withdrawRecords.map(record => (
-
-
-
-
- 提现金额:¥{record.money}
-
-
- 提现账户:{record.accountDisplay}
-
-
-
- {getStatusText(record.applyStatus)}
-
-
+ withdrawRecords.map(record => (
+
+
+
+
+ 提现金额:¥{record.money}
+
+ {/**/}
+ {/* 提现账户:{record.accountDisplay}*/}
+ {/**/}
+
+
+ {getStatusText(record.applyStatus)}
+
+
-
- 申请时间:{record.createTime}
- {record.auditTime && (
-
- 审核时间:{new Date(record.auditTime).toLocaleString()}
-
+
+ {record.applyStatus === 20 && record.payType === 10 && (
+
+
+
)}
- {record.rejectReason && (
-
- 驳回原因:{record.rejectReason}
-
- )}
-
+
+
+
+ 创建时间:{record.createTime}
+ {record.auditTime && (
+
+ 审核时间:{record.auditTime}
+
+ )}
+ {record.rejectReason && (
+
+ 驳回原因:{record.rejectReason}
+
+ )}
+
+
+
))
) : (
@@ -485,13 +629,12 @@ const DealerWithdraw: React.FC = () => {
- {renderWithdrawForm()}
- {renderWithdrawRecords()}
+ {activeTab === '0' ? renderWithdrawForm() : renderWithdrawRecords()}
)
}
diff --git a/src/hooks/useDealerUser.ts b/src/hooks/useDealerUser.ts
index 062777d..3195eb6 100644
--- a/src/hooks/useDealerUser.ts
+++ b/src/hooks/useDealerUser.ts
@@ -1,5 +1,5 @@
import {useState, useEffect, useCallback} from 'react'
-import Taro from '@tarojs/taro'
+import Taro, { useDidShow } from '@tarojs/taro'
import {getShopDealerUser} from '@/api/shop/shopDealerUser'
import type {ShopDealerUser} from '@/api/shop/shopDealerUser/model'
@@ -22,17 +22,20 @@ export interface UseDealerUserReturn {
*/
export const useDealerUser = (): UseDealerUserReturn => {
const [dealerUser, setDealerUser] = useState(null)
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
+ const rawUserId = Taro.getStorageSync('UserId')
+ const userId = Number(rawUserId)
+ const hasUser = Number.isFinite(userId) && userId > 0
- const userId = Taro.getStorageSync('UserId');
+ // If user is logged in, start in loading state to avoid "click too fast" mis-routing.
+ const [loading, setLoading] = useState(hasUser)
+ const [error, setError] = useState(null)
// 获取经销商用户数据
const fetchDealerData = useCallback(async () => {
- if (!userId) {
- console.log('🔍 用户未登录,提前返回')
+ if (!hasUser) {
setDealerUser(null)
+ setLoading(false)
return
}
@@ -55,7 +58,7 @@ export const useDealerUser = (): UseDealerUserReturn => {
} finally {
setLoading(false)
}
- }, [userId])
+ }, [hasUser, userId])
// 刷新数据
const refresh = useCallback(async () => {
@@ -64,13 +67,31 @@ export const useDealerUser = (): UseDealerUserReturn => {
// 初始化加载数据
useEffect(() => {
- if (userId) {
- console.log('🔍 调用 fetchDealerData')
+ if (hasUser) {
fetchDealerData()
} else {
- console.log('🔍 用户ID不存在,不调用 fetchDealerData')
+ setDealerUser(null)
+ setError(null)
+ setLoading(false)
}
- }, [fetchDealerData, userId])
+ }, [fetchDealerData, hasUser])
+
+ // 页面返回/切换到前台时刷新一次,避免“注册成为经销商后,页面不更新”
+ useDidShow(() => {
+ fetchDealerData()
+ })
+
+ // 允许业务侧通过事件主动触发刷新(例如:注册成功后触发)
+ useEffect(() => {
+ const handler = () => {
+ fetchDealerData()
+ }
+ // 事件名尽量语义化;后续可在注册成功处 trigger
+ Taro.eventCenter.on('dealerUser:changed', handler)
+ return () => {
+ Taro.eventCenter.off('dealerUser:changed', handler)
+ }
+ }, [fetchDealerData])
return {
dealerUser,
diff --git a/src/hooks/useOrderStats.ts b/src/hooks/useOrderStats.ts
index a26c29e..75baa07 100644
--- a/src/hooks/useOrderStats.ts
+++ b/src/hooks/useOrderStats.ts
@@ -1,7 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
-import { UserOrderStats } from '@/api/user';
+import { getUserOrderStats, UserOrderStats } from '@/api/user';
import Taro from '@tarojs/taro';
-import {pageShopOrder} from "@/api/shop/shopOrder";
/**
* 订单统计Hook
@@ -31,20 +30,17 @@ export const useOrderStats = () => {
if(!Taro.getStorageSync('UserId')){
return false;
}
- // TODO 读取订单数量
- const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0})
- const paid = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 1})
- const shipped = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 3})
- const completed = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 5})
- const refund = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 6})
- const total = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId')})
+
+ // 聚合接口:一次请求返回各状态数量(后台按用户做了缓存)
+ const stats = await getUserOrderStats();
+
setOrderStats({
- pending: pending?.count || 0,
- paid: paid?.count || 0,
- shipped: shipped?.count || 0,
- completed: completed?.count || 0,
- refund: refund?.count || 0,
- total: total?.count || 0
+ pending: stats?.pending || 0,
+ paid: stats?.paid || 0,
+ shipped: stats?.shipped || 0,
+ completed: stats?.completed || 0,
+ refund: stats?.refund || 0,
+ total: stats?.total || 0
})
if (showToast) {
diff --git a/src/hooks/usePaymentCountdown.ts b/src/hooks/usePaymentCountdown.ts
index a4caa5f..0377f65 100644
--- a/src/hooks/usePaymentCountdown.ts
+++ b/src/hooks/usePaymentCountdown.ts
@@ -13,19 +13,30 @@ export interface CountdownTime {
totalMinutes: number; // 总剩余分钟数
}
+export interface UsePaymentCountdownParams {
+ /** 订单创建时间(用于兼容:当 expirationTime 缺失时按 createTime + timeoutHours 计算) */
+ createTime?: string;
+ /** 订单过期时间(推荐直接传后端返回的 expirationTime) */
+ expirationTime?: string;
+ /** 支付状态 */
+ payStatus?: boolean;
+ /** 是否实时更新(详情页用true,列表页用false) */
+ realTime?: boolean;
+ /** 超时小时数,默认24小时(仅在 expirationTime 缺失时生效) */
+ timeoutHours?: number;
+}
+
/**
* 支付倒计时Hook
- * @param createTime 订单创建时间
- * @param payStatus 支付状态
- * @param realTime 是否实时更新(详情页用true,列表页用false)
- * @param timeoutHours 超时小时数,默认24小时
+ * 优先使用 expirationTime;当 expirationTime 缺失时回退到 createTime + timeoutHours。
*/
-export const usePaymentCountdown = (
- createTime?: string,
- payStatus?: boolean,
- realTime: boolean = false,
- timeoutHours: number = 24
-): CountdownTime => {
+export const usePaymentCountdown = ({
+ createTime,
+ expirationTime,
+ payStatus,
+ realTime = false,
+ timeoutHours = 24
+}: UsePaymentCountdownParams): CountdownTime => {
const [timeLeft, setTimeLeft] = useState({
hours: 0,
minutes: 0,
@@ -37,7 +48,7 @@ export const usePaymentCountdown = (
// 计算剩余时间的函数
const calculateTimeLeft = useMemo(() => {
return (): CountdownTime => {
- if (!createTime || payStatus) {
+ if (payStatus || (!expirationTime && !createTime)) {
return {
hours: 0,
minutes: 0,
@@ -47,8 +58,27 @@ export const usePaymentCountdown = (
};
}
- const createTimeObj = dayjs(createTime);
- const expireTime = createTimeObj.add(timeoutHours, 'hour');
+ // 优先使用后端过期时间;如果无法解析,再回退到 createTime + timeoutHours
+ const expireTimeFromExpiration = expirationTime ? dayjs(expirationTime) : null;
+ const expireTimeFromCreate =
+ createTime ? dayjs(createTime).add(timeoutHours, 'hour') : null;
+ const expireTime =
+ expireTimeFromExpiration?.isValid()
+ ? expireTimeFromExpiration
+ : expireTimeFromCreate?.isValid()
+ ? expireTimeFromCreate
+ : null;
+
+ if (!expireTime) {
+ return {
+ hours: 0,
+ minutes: 0,
+ seconds: 0,
+ isExpired: true,
+ totalMinutes: 0
+ };
+ }
+
const now = dayjs();
const diff = expireTime.diff(now);
@@ -76,10 +106,10 @@ export const usePaymentCountdown = (
totalMinutes
};
};
- }, [createTime, payStatus, timeoutHours]);
+ }, [createTime, expirationTime, payStatus, timeoutHours]);
useEffect(() => {
- if (!createTime || payStatus) {
+ if (payStatus || (!expirationTime && !createTime)) {
setTimeLeft({
hours: 0,
minutes: 0,
@@ -111,7 +141,7 @@ export const usePaymentCountdown = (
}, 1000);
return () => clearInterval(timer);
- }, [createTime, payStatus, realTime, calculateTimeLeft]);
+ }, [createTime, expirationTime, payStatus, realTime, calculateTimeLeft]);
return timeLeft;
};
diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts
index f6684da..0621f86 100644
--- a/src/hooks/useTheme.ts
+++ b/src/hooks/useTheme.ts
@@ -1,5 +1,5 @@
-import { useState, useEffect } from 'react'
-import { gradientThemes, GradientTheme, gradientUtils } from '@/styles/gradients'
+import { useState, useEffect, useCallback } from 'react'
+import { gradientThemes, type GradientTheme, gradientUtils } from '@/styles/gradients'
import Taro from '@tarojs/taro'
export interface UseThemeReturn {
@@ -14,28 +14,42 @@ export interface UseThemeReturn {
* 提供主题切换和状态管理功能
*/
export const useTheme = (): UseThemeReturn => {
- const [currentTheme, setCurrentTheme] = useState(gradientThemes[0])
- const [isAutoTheme, setIsAutoTheme] = useState(true)
-
- // 获取当前主题
- const getCurrentTheme = (): GradientTheme => {
- const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
-
- if (savedTheme === 'auto') {
- // 自动主题:根据用户ID生成
- const userId = Taro.getStorageSync('userId') || '1'
- return gradientUtils.getThemeByUserId(userId)
- } else {
- // 手动选择的主题
- return gradientThemes.find(t => t.name === savedTheme) || gradientThemes[0]
+ const getSavedThemeName = useCallback((): string => {
+ try {
+ return Taro.getStorageSync('user_theme') || 'nature'
+ } catch {
+ return 'nature'
}
- }
+ }, [])
+
+ const getStoredUserId = useCallback((): number => {
+ try {
+ const raw = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId')
+ const asNumber = typeof raw === 'number' ? raw : parseInt(String(raw || '1'), 10)
+ return Number.isFinite(asNumber) ? asNumber : 1
+ } catch {
+ return 1
+ }
+ }, [])
+
+ const resolveTheme = useCallback(
+ (themeName: string): GradientTheme => {
+ if (themeName === 'auto') {
+ return gradientUtils.getThemeByUserId(getStoredUserId())
+ }
+ return gradientThemes.find(t => t.name === themeName) || gradientUtils.getThemeByName('nature') || gradientThemes[0]
+ },
+ [getStoredUserId]
+ )
+
+ const [isAutoTheme, setIsAutoTheme] = useState(() => getSavedThemeName() === 'auto')
+ const [currentTheme, setCurrentTheme] = useState(() => resolveTheme(getSavedThemeName()))
// 初始化主题
useEffect(() => {
- const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
+ const savedTheme = getSavedThemeName()
setIsAutoTheme(savedTheme === 'auto')
- setCurrentTheme(getCurrentTheme())
+ setCurrentTheme(resolveTheme(savedTheme))
}, [])
// 设置主题
@@ -43,7 +57,7 @@ export const useTheme = (): UseThemeReturn => {
try {
Taro.setStorageSync('user_theme', themeName)
setIsAutoTheme(themeName === 'auto')
- setCurrentTheme(getCurrentTheme())
+ setCurrentTheme(resolveTheme(themeName))
} catch (error) {
console.error('保存主题失败:', error)
}
@@ -51,7 +65,7 @@ export const useTheme = (): UseThemeReturn => {
// 刷新主题(用于自动主题模式下用户信息变更时)
const refreshTheme = () => {
- setCurrentTheme(getCurrentTheme())
+ setCurrentTheme(resolveTheme(getSavedThemeName()))
}
return {
diff --git a/src/hooks/useUnifiedQRScan.ts b/src/hooks/useUnifiedQRScan.ts
index 5468046..f2a2b8d 100644
--- a/src/hooks/useUnifiedQRScan.ts
+++ b/src/hooks/useUnifiedQRScan.ts
@@ -5,6 +5,7 @@ import {
parseQRContent
} from '@/api/passport/qr-login';
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
+import { getGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket';
import { useUser } from "@/hooks/useUser";
import { isValidJSON } from "@/utils/jsonUtils";
import dayjs from 'dayjs';
@@ -29,6 +30,15 @@ export enum ScanType {
UNKNOWN = 'unknown' // 未知类型
}
+type VerificationBusinessType = 'gift' | 'ticket';
+
+interface TicketVerificationPayload {
+ userTicketId: number;
+ qty?: number;
+ userId?: number;
+ t?: number;
+}
+
/**
* 统一扫码结果
*/
@@ -73,7 +83,11 @@ export function useUnifiedQRScan() {
// 1. 检查是否为JSON格式(核销二维码)
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
- if (json.businessType === 'gift' && json.token && json.data) {
+ if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
+ return ScanType.VERIFICATION;
+ }
+ // Allow plaintext (non-encrypted) ticket verification payload for debugging/internal use.
+ if (json.userTicketId) {
return ScanType.VERIFICATION;
}
}
@@ -130,35 +144,79 @@ export function useUnifiedQRScan() {
throw new Error('您没有核销权限');
}
- let code = '';
+ let businessType: VerificationBusinessType = 'gift';
+ let decryptedOrRaw = '';
// 判断是否为加密的JSON格式
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
- if (json.businessType === 'gift' && json.token && json.data) {
- // 解密获取核销码
+ if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
+ businessType = json.businessType;
+ // 解密获取核销内容
const decryptedData = await decryptQrData({
token: json.token,
encryptedData: json.data
});
if (decryptedData) {
- code = decryptedData.toString();
+ decryptedOrRaw = decryptedData.toString();
} else {
throw new Error('解密失败');
}
+ } else if (json.userTicketId) {
+ businessType = 'ticket';
+ decryptedOrRaw = scanResult.trim();
}
} else {
- // 直接使用扫码结果作为核销码
- code = scanResult.trim();
+ // 直接使用扫码结果作为核销内容
+ decryptedOrRaw = scanResult.trim();
}
- if (!code) {
+ if (!decryptedOrRaw) {
throw new Error('无法获取有效的核销码');
}
- // 验证核销码
- const gift = await getShopGiftByCode(code);
+ if (businessType === 'ticket') {
+ if (!isValidJSON(decryptedOrRaw)) {
+ throw new Error('水票核销信息格式错误');
+ }
+ const payload = JSON.parse(decryptedOrRaw) as TicketVerificationPayload;
+ const userTicketId = Number(payload.userTicketId);
+ const qty = Math.max(1, Number(payload.qty || 1));
+ if (!Number.isFinite(userTicketId) || userTicketId <= 0) {
+ throw new Error('水票核销信息无效');
+ }
+
+ const ticket = await getGltUserTicket(userTicketId);
+ if (!ticket) throw new Error('水票不存在');
+ if (ticket.status === 1) throw new Error('该水票已冻结');
+ const available = Number(ticket.availableQty || 0);
+ const used = Number(ticket.usedQty || 0);
+ if (available < qty) throw new Error('水票可用次数不足');
+
+ await updateGltUserTicket({
+ ...ticket,
+ availableQty: available - qty,
+ usedQty: used + qty
+ });
+
+ return {
+ type: ScanType.VERIFICATION,
+ data: {
+ businessType: 'ticket',
+ ticket: {
+ ...ticket,
+ availableQty: available - qty,
+ usedQty: used + qty
+ },
+ qty
+ },
+ message: `核销成功(已使用${qty}次)`
+ };
+ }
+
+ // 验证礼品卡核销码
+ const gift = await getShopGiftByCode(decryptedOrRaw);
if (!gift) {
throw new Error('核销码无效');
@@ -187,7 +245,7 @@ export function useUnifiedQRScan() {
return {
type: ScanType.VERIFICATION,
- data: gift,
+ data: { businessType: 'gift', gift },
message: '核销成功'
};
}, [isAdmin]);
@@ -213,7 +271,14 @@ export function useUnifiedQRScan() {
}
},
fail: (err) => {
- reject(new Error(err.errMsg || '扫码失败'));
+ const msg = (err as any)?.errMsg || '';
+ // `scanCode:fail cancel` is a user-driven cancel; don't treat it as an error toast.
+ if (typeof msg === 'string' && msg.toLowerCase().includes('cancel')) {
+ cancelRef.current = true;
+ reject(new Error('取消扫码'));
+ return;
+ }
+ reject(new Error(msg || '扫码失败'));
}
});
});
@@ -265,6 +330,11 @@ export function useUnifiedQRScan() {
return result;
} catch (err: any) {
+ // User cancelled scanning (e.g. `scanCode:fail cancel`).
+ if (cancelRef.current) {
+ reset();
+ return null;
+ }
if (!cancelRef.current) {
setState(UnifiedScanState.ERROR);
const errorMessage = err.message || '处理失败';
diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts
index 479a0f3..df5c040 100644
--- a/src/hooks/useUser.ts
+++ b/src/hooks/useUser.ts
@@ -3,7 +3,7 @@ import Taro from '@tarojs/taro';
import { User } from '@/api/system/user/model';
import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout';
import { TenantId } from '@/config/app';
-import {getStoredInviteParams, handleInviteRelation} from '@/utils/invite';
+import { handleInviteRelation } from '@/utils/invite';
// 用户Hook
export const useUser = () => {
@@ -44,15 +44,10 @@ export const useUser = () => {
reject(new Error('自动登录失败'));
}
}).catch(_ => {
- // 首次注册,跳转到邀请注册页面
- const pages = Taro.getCurrentPages();
- const currentPage = pages[pages.length - 1];
- const inviteParams = getStoredInviteParams()
- if (currentPage?.route !== 'dealer/apply/add' && inviteParams?.inviter) {
- return Taro.navigateTo({
- url: '/dealer/apply/add'
- });
- }
+ // 登录失败(通常是新用户尚未注册/未绑定手机号等)。
+ // 这里不做任何“自动跳转”,避免用户点击「我的」时被强制带到分销/申请页,体验割裂。
+ // 需要登录的页面请使用 utils/auth 的 ensureLoggedIn / goToRegister 做显式跳转。
+ reject(new Error('autoLoginByOpenId failed'));
});
},
fail: reject
@@ -60,7 +55,11 @@ export const useUser = () => {
});
return res;
} catch (error) {
- console.error('自动登录失败:', error);
+ const msg = error instanceof Error ? error.message : String(error);
+ // 新用户首次进入、未绑定手机号等场景属于“预期失败”,避免刷屏报错。
+ if (msg !== 'autoLoginByOpenId failed') {
+ console.error('自动登录失败:', error);
+ }
return null;
}
};
@@ -280,11 +279,14 @@ export const useUser = () => {
// 检查用户是否是管理员
const isAdmin = () => {
- return user?.isAdmin === true;
+ // Some backends use `1/0` (or `1/2`) instead of boolean.
+ const v: any = (user as any)?.isAdmin;
+ return v === true || v === 1 || v === '1';
};
const isSuperAdmin = () => {
- return user?.isSuperAdmin === true;
+ const v: any = (user as any)?.isSuperAdmin;
+ return v === true || v === 1 || v === '1';
};
// 获取用户余额
diff --git a/src/hooks/useUserData.ts b/src/hooks/useUserData.ts
index fcc0423..4c5e462 100644
--- a/src/hooks/useUserData.ts
+++ b/src/hooks/useUserData.ts
@@ -1,12 +1,10 @@
import { useState, useEffect, useCallback } from 'react'
-import {pageShopUserCoupon} from "@/api/shop/shopUserCoupon";
-import {pageShopGift} from "@/api/shop/shopGift";
import {useUser} from "@/hooks/useUser";
import Taro from '@tarojs/taro'
-import {getUserInfo} from "@/api/layout";
+import { getUserCardStats } from '@/api/user'
interface UserData {
- balance: number
+ balance: string
points: number
coupons: number
giftCards: number
@@ -24,7 +22,7 @@ interface UseUserDataReturn {
loading: boolean
error: string | null
refresh: () => Promise
- updateBalance: (newBalance: number) => void
+ updateBalance: (newBalance: string) => void
updatePoints: (newPoints: number) => void
}
@@ -43,18 +41,14 @@ export const useUserData = (): UseUserDataReturn => {
return;
}
- // 并发请求所有数据
- const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
- getUserInfo(),
- pageShopUserCoupon({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0}),
- pageShopGift({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0})
- ])
+ // 聚合接口:一次请求返回余额/积分/优惠券/礼品卡统计(后端可按用户做缓存)
+ const stats = await getUserCardStats()
const newData: UserData = {
- balance: userDataRes?.balance || 0.00,
- points: userDataRes?.points || 0,
- coupons: couponsRes?.count || 0,
- giftCards: giftCardsRes?.count || 0,
+ balance: stats?.balance || '0.00',
+ points: stats?.points || 0,
+ coupons: stats?.coupons || 0,
+ giftCards: stats?.giftCards || 0,
orders: {
pending: 0,
paid: 0,
@@ -78,7 +72,7 @@ export const useUserData = (): UseUserDataReturn => {
}, [fetchUserData])
// 更新余额(本地更新,避免频繁请求)
- const updateBalance = useCallback((newBalance: number) => {
+ const updateBalance = useCallback((newBalance: string) => {
setData(prev => prev ? { ...prev, balance: newBalance } : null)
}, [])
diff --git a/src/pages/cart/cart.tsx b/src/pages/cart/cart.tsx
index e415a03..ae5ffdb 100644
--- a/src/pages/cart/cart.tsx
+++ b/src/pages/cart/cart.tsx
@@ -10,10 +10,11 @@ import {
Divider,
ConfigProvider
} from '@nutui/nutui-react-taro';
-import {ArrowLeft, Del} from '@nutui/icons-react-taro';
+import {Del} from '@nutui/icons-react-taro';
import {View} from '@tarojs/components';
import {CartItem, useCart} from "@/hooks/useCart";
import './cart.scss';
+import { ensureLoggedIn } from '@/utils/auth'
function Cart() {
const [statusBarHeight, setStatusBarHeight] = useState(0);
@@ -41,7 +42,7 @@ function Cart() {
useShareAppMessage(() => {
return {
- title: '购物车 - 时里院子市集',
+ title: '购物车 - 桂乐淘',
success: function () {
console.log('分享成功');
},
@@ -150,6 +151,9 @@ function Cart() {
// 将选中的商品信息存储到本地,供结算页面使用
Taro.setStorageSync('checkout_items', JSON.stringify(selectedCartItems));
+ // 未登录则引导去注册/登录;登录后回到购物车结算页
+ if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
+
// 跳转到购物车结算页面
Taro.navigateTo({
url: '/shop/orderConfirmCart/index'
@@ -171,7 +175,6 @@ function Cart() {
Taro.navigateBack()}/>}
right={
cartItems.length > 0 && (
- {
- }}
- left={
- !IsLogin ? (
-
-
-
-
- {getWebsiteName()}
-
-
-
-
- ) : (
-
-
- {getWebsiteName()}
-
-
- )}>
- {/**/}
-
-
+
>
)
}
diff --git a/src/pages/index/Login.tsx b/src/pages/index/Login.tsx
index 9c88ef7..c3e3459 100644
--- a/src/pages/index/Login.tsx
+++ b/src/pages/index/Login.tsx
@@ -49,7 +49,7 @@ const Login = (props: LoginProps) => {
success: function () {
if (code) {
Taro.request({
- url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
+ url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
code,
diff --git a/src/pages/index/MySearch.tsx b/src/pages/index/MySearch.tsx
index 3c6d9a5..2015ee6 100644
--- a/src/pages/index/MySearch.tsx
+++ b/src/pages/index/MySearch.tsx
@@ -30,7 +30,7 @@ function MySearch(props: any) {
return (
-
+
(false)
- // Tabs粘性状态
- const [_, setTabsStickyStatus] = useState
(false)
+ const [activeTabKey, setActiveTabKey] = useState('recommend')
+ const [goodsList, setGoodsList] = useState([])
+ const [ticketTotal, setTicketTotal] = useState(0)
useShareAppMessage(() => {
// 获取当前用户ID,用于生成邀请链接
const userId = Taro.getStorageSync('UserId');
+ const user = Taro.getStorageSync('User') || {};
+ const nickname =
+ (user && (user.nickname || user.realName || user.username)) || '';
return {
- title: '🏠 首页 🏠',
+ title: (nickname || '') + '超值推荐',
path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`,
success: function () {
console.log('首页分享成功');
@@ -85,14 +91,30 @@ function Home() {
// }
// 处理Tabs粘性状态变化
- const handleTabsStickyChange = (isSticky: boolean) => {
- setTabsStickyStatus(isSticky)
- }
+ // const handleTabsStickyChange = (isSticky: boolean) => {}
const reload = () => {
-
+ const token = Taro.getStorageSync('access_token')
+ const userIdRaw = Taro.getStorageSync('UserId')
+ const userId = Number(userIdRaw)
+ const hasUserId = Number.isFinite(userId) && userId > 0
+ if (!token && !hasUserId) {
+ setTicketTotal(0)
+ return
+ }
+ getMyGltUserTicketTotal(hasUserId ? userId : undefined)
+ .then((total) => setTicketTotal(typeof total === 'number' ? total : 0))
+ .catch((err) => {
+ console.error('首页水票总数加载失败:', err)
+ setTicketTotal(0)
+ })
};
+ // 回到首页/首次进入时都刷新一次(避免依赖 scope.userInfo 导致不触发 reload)
+ useDidShow(() => {
+ reload()
+ })
+
useEffect(() => {
// 获取站点信息
getShopInfo().then(() => {
@@ -135,7 +157,6 @@ function Home() {
if (res.authSetting['scope.userInfo']) {
// 用户已经授权过,可以直接获取用户信息
console.log('用户已经授权过,可以直接获取用户信息')
- reload();
} else {
// 用户未授权,需要弹出授权窗口
console.log('用户未授权,需要弹出授权窗口')
@@ -147,21 +168,210 @@ function Home() {
Taro.getUserInfo({
success: (res) => {
const avatar = res.userInfo.avatarUrl;
+ // Keep WeChat display name in storage so share title can use it.
console.log(avatar, 'avatarUrl')
}
});
}, []);
+ const tabs = useMemo<
+ Array<{ key: string; title: string; params: Partial }>
+ >(
+ () => [
+ { key: 'recommend', title: '推荐', params: { recommend: 1 } },
+ { key: '4476', title: '桶装水', params: { categoryId: 4476 } },
+ { key: '4556', title: '水票套餐', params: { categoryId: 4556 } },
+ // { key: '4557', title: '购机套餐', params: { categoryId: 4557 } },
+ // { key: '4477', title: '饮水设备', params: { categoryId: 4477 } },
+ ],
+ []
+ )
+
+ useEffect(() => {
+ const tab = tabs.find((t) => t.key === activeTabKey) || tabs[0]
+ if (!tab) return
+
+ pageShopGoods({ ...tab.params, status: 0 })
+ .then((res) => setGoodsList((res?.list || []).filter((g) => g?.status === 0)))
+ .catch((err) => {
+ console.error('首页商品列表加载失败:', err)
+ setGoodsList([])
+ })
+ }, [activeTabKey, tabs])
+
+ const shortcuts = useMemo<
+ Array<{ key: string; title: string; icon: ReactNode; onClick: () => void }>
+ >(
+ () => [
+ {
+ key: 'ticket',
+ title: '我的水票',
+ icon: ,
+ onClick: () => {
+ if (!ensureLoggedIn('/user/ticket/index')) return
+ Taro.navigateTo({ url: '/user/ticket/index' })
+ },
+ },
+ {
+ key: 'order',
+ title: '立即送水',
+ icon: ,
+ onClick: () => {
+ if (!ensureLoggedIn('/user/ticket/use?goodsId=10074')) return
+ Taro.navigateTo({ url: '/user/ticket/use?goodsId=10074' })
+ },
+ },
+ {
+ key: 'order',
+ title: '送水订单',
+ icon: ,
+ onClick: () => {
+ if (!ensureLoggedIn('/user/ticket/index')) return
+ Taro.navigateTo({ url: '/user/ticket/index' })
+ },
+ },
+ {
+ key: 'invite',
+ title: '邀请有礼',
+ icon: ,
+ onClick: () => {
+ if (!ensureLoggedIn('/dealer/qrcode/index')) return
+ Taro.navigateTo({ url: '/dealer/qrcode/index' })
+ },
+ },
+ // {
+ // key: 'coupon',
+ // title: '领券中心',
+ // icon: ,
+ // onClick: () => Taro.navigateTo({ url: '/coupon/index' }),
+ // },
+ ],
+ []
+ )
+
+ const visibleGoods = useMemo(() => {
+ // 先按效果图展示两列卡片,数据不够时也保持布局稳定
+ const list = goodsList || []
+ if (list.length <= 6) return list
+ return list.slice(0, 6)
+ }, [goodsList])
+
return (
<>
- {/* Header区域 - 现在由Header组件内部处理吸顶逻辑 */}
-
+ {/* Header区域 */}
+ {/**/}
-
-
-
-
-
+
+ {/* 顶部活动主视觉:使用 Banner 组件 */}
+
+
+ {/* 电子水票 */}
+
+
+ 电子水票
+
+ 您还有 {ticketTotal} 张水票
+
+
+
+
+
+ {shortcuts.map((item) => (
+
+ {item.icon}
+ {item.title}
+
+ ))}
+
+
+
+
+ Taro.navigateTo({ url: `/shop/category/index?id=4560` })}>
+
+ 政企采购专区
+
+
+
+
+ Taro.navigateTo({ url: `/shop/category/index?id=4556` })}>
+
+ 桂乐淘·福利惊爆区
+
+
+
+
+ {/*分类Tabs*/}
+
+
+ {tabs.map((tab) => {
+ const active = tab.key === activeTabKey
+ return (
+ setActiveTabKey(tab.key)}
+ >
+ {tab.title}
+
+ )
+ })}
+
+
+ {/* 商品列表 */}
+
+ {visibleGoods.map((item) => (
+
+
+
+ Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
+ }
+ />
+
+
+
+ {item.name}
+
+ 已购:{item.sales || 0}人
+
+ ¥
+ {item.buyingPrice}
+
+
+
+
+ {/* {*/}
+ {/* if (!ensureLoggedIn('/shop/orderConfirm/index?goodsId=10074')) return*/}
+ {/* Taro.navigateTo({ url: '/shop/orderConfirm/index?goodsId=10074' })*/}
+ {/* }}*/}
+ {/*>*/}
+ {/* 买水票更优惠*/}
+ {/**/}
+
+ Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
+ }
+ >
+ 立即购买
+
+
+
+
+ ))}
+
+
+
>
)
}
diff --git a/src/pages/user/components/IsDealer.tsx b/src/pages/user/components/IsDealer.tsx
index a5fd05e..88d3008 100644
--- a/src/pages/user/components/IsDealer.tsx
+++ b/src/pages/user/components/IsDealer.tsx
@@ -6,12 +6,13 @@ import {useUser} from '@/hooks/useUser'
import {useDealerUser} from "@/hooks/useDealerUser";
import {useThemeStyles} from "@/hooks/useTheme";
import { useConfig } from "@/hooks/useConfig"; // 使用新的自定义Hook
+import Taro from '@tarojs/taro'
const IsDealer = () => {
const themeStyles = useThemeStyles();
const { config } = useConfig(); // 使用新的Hook
const {isSuperAdmin} = useUser();
- const {dealerUser} = useDealerUser()
+ const {dealerUser, loading: dealerLoading} = useDealerUser()
/**
* 管理中心
@@ -51,7 +52,7 @@ const IsDealer = () => {
{config?.vipText || '入驻申请'}
+ className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '账户管理中心'}
{/*门店核销*/}
}
@@ -75,12 +76,18 @@ const IsDealer = () => {
title={
- {config?.vipText || '开通VIP'}
- {config?.vipComments || '享优惠'}
+ {config?.vipText || '账户管理中心'}
+ {config?.vipComments || ''}
}
extra={}
- onClick={() => navTo('/dealer/apply/add', true)}
+ onClick={() => {
+ if (dealerLoading) {
+ Taro.showToast({ title: '正在加载信息,请稍等...', icon: 'none' })
+ return
+ }
+ navTo('/dealer/apply/add', true)
+ }}
/>
>
diff --git a/src/pages/user/components/UserCard.tsx b/src/pages/user/components/UserCard.tsx
index b2e2dc8..93c76b4 100644
--- a/src/pages/user/components/UserCard.tsx
+++ b/src/pages/user/components/UserCard.tsx
@@ -8,33 +8,94 @@ import navTo from "@/utils/common";
import {TenantId} from "@/config/app";
import {useUser} from "@/hooks/useUser";
import {useUserData} from "@/hooks/useUserData";
-import {getStoredInviteParams} from "@/utils/invite";
+import {checkAndHandleInviteRelation, getStoredInviteParams, hasPendingInvite} from "@/utils/invite";
import UnifiedQRButton from "@/components/UnifiedQRButton";
import {useThemeStyles} from "@/hooks/useTheme";
+import {getRootDomain} from "@/utils/domain";
+import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
+import { saveStorageByLoginUser } from '@/utils/server'
const UserCard = forwardRef((_, ref) => {
const {data, refresh} = useUserData()
- const {getDisplayName, getRoleName} = useUser();
+ const {loadUserFromStorage} = useUser();
const [IsLogin, setIsLogin] = useState(false)
const [userInfo, setUserInfo] = useState()
+ const [ticketTotal, setTicketTotal] = useState(0)
const themeStyles = useThemeStyles();
+ const canShowScanButton = (() => {
+ const v: any = (userInfo as any)?.isAdmin
+ return v === true || v === 1 || v === '1'
+ })()
+
+ const getDisplayName = () => {
+ if (!userInfo) return IsLogin ? '用户' : '点击登录'
+ return userInfo.nickname || (userInfo as any).realName || (userInfo as any).username || (IsLogin ? '用户' : '点击登录')
+ }
+
+ // 角色名称:优先取用户 roles 数组的第一个角色名称
+ const getRoleName = () => {
+ return userInfo?.roles?.[0]?.roleName || userInfo?.roleName || '注册用户'
+ }
// 下拉刷新
- const handleRefresh = async () => {
+ const reloadStats = async (showToast = false) => {
await refresh()
- Taro.showToast({
- title: '刷新成功',
- icon: 'success'
- })
+ reloadTicketTotal()
+ if (showToast) {
+ Taro.showToast({
+ title: '刷新成功',
+ icon: 'success'
+ })
+ }
+ }
+
+ const syncUserToStorage = (u: User) => {
+ // Keep storage up-to-date for other places that read user info synchronously.
+ Taro.setStorageSync('User', u)
+ if (u?.userId) Taro.setStorageSync('UserId', u.userId)
+ if (u?.nickname) Taro.setStorageSync('WxNickName', u.nickname)
+ }
+
+ const reloadUserInfo = async () => {
+ try {
+ const u = await getUserInfo()
+ if (u) {
+ setUserInfo(u)
+ setIsLogin(true)
+ syncUserToStorage(u)
+ // Refresh this hook instance's state from storage (defensive).
+ await loadUserFromStorage()
+
+ // 获取openId(不阻塞 UI 刷新)
+ if (!u.openid) {
+ Taro.login({
+ success: (res) => {
+ getWxOpenId({code: res.code}).catch(() => {})
+ }
+ })
+ }
+ }
+ } catch (e) {
+ // Not logged in / token expired: keep UI in "not login" state.
+ // Other error handling is done in request interceptor / callers.
+ }
}
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
- handleRefresh
+ handleRefresh: async () => {
+ await reloadUserInfo()
+ await reloadStats(true)
+ },
+ reloadStats,
+ reloadUserInfo
}))
useEffect(() => {
+ // 独立于用户信息授权:只要有登录 token,就可以拉取水票总数
+ reloadTicketTotal()
+
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
success: (res) => {
@@ -51,6 +112,23 @@ const UserCard = forwardRef((_, ref) => {
});
}, []);
+ const reloadTicketTotal = () => {
+ const token = Taro.getStorageSync('access_token')
+ const userIdRaw = Taro.getStorageSync('UserId')
+ const userId = Number(userIdRaw)
+ const hasUserId = Number.isFinite(userId) && userId > 0
+ if (!token && !hasUserId) {
+ setTicketTotal(0)
+ return
+ }
+ getMyGltUserTicketTotal(hasUserId ? userId : undefined)
+ .then((total) => setTicketTotal(typeof total === 'number' ? total : 0))
+ .catch((err) => {
+ console.error('个人中心水票总数加载失败:', err)
+ setTicketTotal(0)
+ })
+ }
+
const reload = () => {
Taro.getUserInfo({
success: (res) => {
@@ -60,25 +138,15 @@ const UserCard = forwardRef((_, ref) => {
nickname: res.userInfo.nickName,
sexName: res.userInfo.gender == 1 ? '男' : '女'
})
- getUserInfo().then((data) => {
- if (data) {
- setUserInfo(data)
- setIsLogin(true);
- Taro.setStorageSync('UserId', data.userId)
-
- // 获取openId
- if (!data.openid) {
- Taro.login({
- success: (res) => {
- getWxOpenId({code: res.code}).then(() => {
- })
- }
- })
- }
- }
- }).catch(() => {
- console.log('未登录')
- });
+ reloadUserInfo()
+ .then(() => {
+ // 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
+ refresh().then()
+ reloadTicketTotal()
+ })
+ .catch(() => {
+ console.log('未登录')
+ })
}
});
};
@@ -133,7 +201,7 @@ const UserCard = forwardRef((_, ref) => {
success: function () {
if (code) {
Taro.request({
- url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
+ url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
code,
@@ -158,10 +226,19 @@ const UserCard = forwardRef((_, ref) => {
return false;
}
// 登录成功
- Taro.setStorageSync('access_token', res.data.data.access_token)
- Taro.setStorageSync('UserId', res.data.data.user.userId)
+ saveStorageByLoginUser(res.data.data.access_token, res.data.data.user)
setUserInfo(res.data.data.user)
setIsLogin(true)
+ // 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
+ refresh().then()
+ reloadTicketTotal()
+
+ // 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定
+ if (hasPendingInvite()) {
+ checkAndHandleInviteRelation().catch((e) => {
+ console.error('个人中心登录后处理邀请关系失败:', e)
+ })
+ }
}
})
} else {
@@ -189,7 +266,9 @@ const UserCard = forwardRef((_, ref) => {
/>
{getDisplayName() || '点击登录'}
- {getRoleName()}
+ {getRootDomain() && (
+ {getRoleName()}
+ )}
@@ -209,33 +288,62 @@ const UserCard = forwardRef((_, ref) => {
)}
-
- {/*统一扫码入口 - 支持登录和核销*/}
- {
- console.log('统一扫码成功:', result);
- // 根据扫码类型给出不同的提示
- if (result.type === 'verification') {
- // 核销成功,可以显示更多信息或跳转到详情页
- Taro.showModal({
- title: '核销成功',
- content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}`
- });
- }
- }}
- onError={(error) => {
- console.error('统一扫码失败:', error);
- }}
- />
-
+ {/*统一扫码入口 - 仅管理员可见*/}
+ {canShowScanButton && (
+
+ {
+ console.log('统一扫码成功:', result);
+ // 根据扫码类型给出不同的提示
+ if (result.type === 'verification') {
+ const businessType = result?.data?.businessType;
+ if (businessType === 'gift' && result?.data?.gift) {
+ const gift = result.data.gift;
+ Taro.showModal({
+ title: '核销成功',
+ content: `已成功核销:${gift.goodsName || gift.name || '礼品'},面值¥${gift.faceValue}`
+ });
+ return;
+ }
+ if (businessType === 'ticket' && result?.data?.ticket) {
+ const ticket = result.data.ticket;
+ const qty = result.data.qty || 1;
+ Taro.showModal({
+ title: '核销成功',
+ content: `已成功核销:${ticket.templateName || '水票'},本次使用${qty}次,剩余可用${ticket.availableQty ?? 0}次`
+ });
+ return;
+ }
+ Taro.showModal({
+ title: '核销成功',
+ content: '已成功核销'
+ });
+ }
+ }}
+ onError={(error) => {
+ console.error('统一扫码失败:', error);
+ }}
+ />
+
+ )}
+ navTo('/user/ticket/index', true)}>
+ 水票
+ {ticketTotal}
+
+ navTo('/user/coupon/index', true)}>
+ 优惠券
+ {data?.coupons || 0}
+
navTo('/user/wallet/wallet', true)}>
余额
@@ -245,16 +353,6 @@ const UserCard = forwardRef((_, ref) => {
积分
{data?.points || 0}
- navTo('/user/coupon/index', true)}>
- 优惠券
- {data?.coupons || 0}
-
- navTo('/user/gift/index', true)}>
- 礼品卡
- {data?.giftCards || 0}
-
diff --git a/src/pages/user/components/UserCell.tsx b/src/pages/user/components/UserCell.tsx
index 46a0882..9354b40 100644
--- a/src/pages/user/components/UserCell.tsx
+++ b/src/pages/user/components/UserCell.tsx
@@ -55,7 +55,7 @@ const UserCell = () => {
title={
- 收货地址
+ 配送地址
}
align="center"
diff --git a/src/pages/user/components/UserFooter.tsx b/src/pages/user/components/UserFooter.tsx
index fb74b71..a021a0f 100644
--- a/src/pages/user/components/UserFooter.tsx
+++ b/src/pages/user/components/UserFooter.tsx
@@ -47,8 +47,9 @@ const UserFooter = () => {
return (
<>
-
当前版本:{Version}
-
Copyright © { new Date().getFullYear() } {Copyright}
+ {/*
当前版本:{Version}
*/}
+ {/*
Copyright © { new Date().getFullYear() } {Copyright}
*/}
+
{Copyright}
{
- const {logoutUser} = useUser();
+ const {logoutUser, hasRole} = useUser();
const onLogout = () => {
Taro.showModal({
@@ -38,7 +39,7 @@ const UserCell = () => {
return (
<>
- 我的服务
+ 桂乐淘服务中心
{
border: 'none'
} as React.CSSProperties}
>
- navTo('/user/poster/poster', true)}>
+
+ {hasRole('store') && (
+ navTo('/store/index', true)}>
+
+
+
+
+
+
+ )}
+
+ {hasRole('rider') && (
+ navTo('/rider/index', true)}>
+
+
+
+
+
+
+ )}
+
+ {(hasRole('staff') || hasRole('admin')) && (
+ navTo('/user/store/orders/index', true)}>
+
+
+
+
+
+
+ )}
+
+ navTo('/user/address/index', true)}>
-
-
+
+
+
+
+
+
+ navTo('/user/help/index')}>
+
+
+
@@ -71,14 +111,6 @@ const UserCell = () => {
- navTo('/user/address/index', true)}>
-
-
-
-
-
-
-
navTo('/user/userVerify/index', true)}>
@@ -111,13 +143,6 @@ const UserCell = () => {
{/* */}
{/**/}
- navTo('/user/help/index')}>
-
-
-
-
-
-
navTo('/user/about/index')}>
@@ -189,4 +214,3 @@ const UserCell = () => {
)
}
export default UserCell
-
diff --git a/src/pages/user/components/UserOrder.tsx b/src/pages/user/components/UserOrder.tsx
index 0a17847..56da5cd 100644
--- a/src/pages/user/components/UserOrder.tsx
+++ b/src/pages/user/components/UserOrder.tsx
@@ -26,7 +26,7 @@ function UserOrder() {
}}
>
- 我的订单
+ 商城订单
navTo('/user/order/order', true)}
diff --git a/src/pages/user/user.tsx b/src/pages/user/user.tsx
index 336348d..d9ff840 100644
--- a/src/pages/user/user.tsx
+++ b/src/pages/user/user.tsx
@@ -1,33 +1,41 @@
-import {useEffect, useRef} from 'react'
+import {useEffect, useRef, useState} from 'react'
import {PullToRefresh} from '@nutui/nutui-react-taro'
import UserCard from "./components/UserCard";
import UserOrder from "./components/UserOrder";
import UserFooter from "./components/UserFooter";
-import {useUserData} from "@/hooks/useUserData";
import {View} from '@tarojs/components';
import './user.scss'
import IsDealer from "./components/IsDealer";
import {useThemeStyles} from "@/hooks/useTheme";
import UserGrid from "@/pages/user/components/UserGrid";
+import { useDidShow } from '@tarojs/taro'
function User() {
- const {refresh} = useUserData()
const userCardRef = useRef()
const themeStyles = useThemeStyles();
+ // TabBar 页在小程序里通常不会销毁;从“注册/申请”页返回时需要触发子组件重新初始化/拉取最新状态。
+ const [dealerViewKey, setDealerViewKey] = useState(0)
// 下拉刷新处理
const handleRefresh = async () => {
- await refresh()
- // 如果 UserCard 组件有自己的刷新方法,也可以调用
if (userCardRef.current?.handleRefresh) {
await userCardRef.current.handleRefresh()
}
+ setDealerViewKey(v => v + 1)
}
useEffect(() => {
}, []);
+ // 每次进入/切回个人中心都刷新一次统计(包含水票数量)
+ useDidShow(() => {
+ userCardRef.current?.reloadStats?.()
+ // 个人资料(头像/昵称)可能在其它页面被修改,这里确保返回时立刻刷新
+ userCardRef.current?.reloadUserInfo?.()
+ setDealerViewKey(v => v + 1)
+ })
+
return (
-
+
diff --git a/src/passport/agreement.tsx b/src/passport/agreement.tsx
index 5ce26d1..fbbae42 100644
--- a/src/passport/agreement.tsx
+++ b/src/passport/agreement.tsx
@@ -1,28 +1,47 @@
-import {useEffect, useState} from "react";
+import { useEffect, useState } from 'react'
import Taro from '@tarojs/taro'
-import {View, RichText} from '@tarojs/components'
+import { Loading } from '@nutui/nutui-react-taro'
+import { RichText, View } from '@tarojs/components'
+import { getByCode } from '@/api/cms/cmsArticle'
+import { wxParse } from '@/utils/common'
const Agreement = () => {
+ const [loading, setLoading] = useState(true)
+ const [content, setContent] = useState('')
- const [content, setContent] = useState('')
- const reload = () => {
- Taro.hideTabBar()
- setContent('' +
- '欢迎使用' +
- ' ' +
- '【WebSoft】' +
- '服务协议 ' +
- '
')
+ const reload = async () => {
+ try {
+ Taro.hideTabBar()
+ } catch (_) {
+ // ignore (e.g. H5 / unsupported env)
+ }
+
+ try {
+ const article = await getByCode('xieyi')
+ setContent(article?.content ? wxParse(article.content) : '暂无协议内容
')
+ } catch (e) {
+ // Keep UI usable even if CMS/API fails.
+ // eslint-disable-next-line no-console
+ console.error('load agreement failed', e)
+ setContent('协议内容加载失败
')
+ Taro.showToast({ title: '协议加载失败', icon: 'none' })
+ } finally {
+ setLoading(false)
+ }
}
useEffect(() => {
reload()
}, [])
+ if (loading) {
+ return 加载中
+ }
+
return (
<>
-
+
>
)
diff --git a/src/passport/register.config.ts b/src/passport/register.config.ts
index 77ed0bd..8018733 100644
--- a/src/passport/register.config.ts
+++ b/src/passport/register.config.ts
@@ -1,4 +1,5 @@
export default definePageConfig({
- navigationBarTitleText: '注册账号',
+ navigationBarTitleText: '注册/登录',
navigationBarTextStyle: 'black'
})
+
diff --git a/src/passport/register.tsx b/src/passport/register.tsx
index 553e0e0..2569e44 100644
--- a/src/passport/register.tsx
+++ b/src/passport/register.tsx
@@ -1,47 +1,295 @@
-import {useEffect, useState} from "react";
+import { useEffect, useMemo, useState } from 'react'
import Taro from '@tarojs/taro'
-import {Input, Radio, Button} from '@nutui/nutui-react-taro'
+import { Button, Radio } from '@nutui/nutui-react-taro'
+import { TenantId } from '@/config/app'
+import { getUserInfo, getWxOpenId } from '@/api/layout'
+import { saveStorageByLoginUser } from '@/utils/server'
+import {
+ getStoredInviteParams,
+ parseInviteParams,
+ saveInviteParams,
+ trackInviteSource,
+ checkAndHandleInviteRelation,
+} from '@/utils/invite'
+
+interface GetPhoneNumberDetail {
+ code?: string
+ encryptedData?: string
+ iv?: string
+ errMsg?: string
+}
+
+interface GetPhoneNumberEvent {
+ detail: GetPhoneNumberDetail
+}
+
+interface LoginResponse {
+ data: {
+ code?: number
+ message?: string
+ data?: {
+ access_token: string
+ user: any
+ }
+ }
+}
+
+async function getWeappLoginCode(): Promise {
+ try {
+ const res = await new Promise<{ code?: string }>((resolve, reject) => {
+ Taro.login({
+ success: (r) => resolve(r as any),
+ fail: (e) => reject(e),
+ })
+ })
+ return res?.code
+ } catch (_e) {
+ return undefined
+ }
+}
+
+async function ensureWxOpenIdSaved(opts: { user?: any; wxLoginCode?: string }) {
+ // JSAPI 微信支付必须有 openid;注册/登录后立刻补齐,避免后续创建支付单失败。
+ try {
+ if (Taro.getEnv() !== Taro.ENV_TYPE.WEAPP) return
+ } catch (_e) {
+ if (process.env.TARO_ENV !== 'weapp') return
+ }
+
+ if (opts.user?.openid) return
+
+ const code = opts.wxLoginCode || (await getWeappLoginCode())
+ if (!code) return
+
+ // 该接口一般会在服务端把 openid 绑定到当前登录用户;返回值并不一定包含 openid。
+ await getWxOpenId({ code })
+
+ // 同步本地 User(让后续页面/逻辑能直接读到 openid)
+ try {
+ const fresh = await getUserInfo()
+ if (fresh) Taro.setStorageSync('User', fresh)
+ } catch (_e) {
+ // ignore: openid 已在服务端绑定,本地不同步也不影响后端创建支付订单
+ }
+}
+
+function safeDecodeMaybeEncoded(input?: string): string {
+ if (!input) return ''
+ try {
+ // Taro 路由参数通常是 URL 编码过的字符串
+ return decodeURIComponent(input)
+ } catch (_e) {
+ return input
+ }
+}
+
+function isTabBarUrl(url: string) {
+ const pure = url.split('?')[0]
+ return (
+ pure === '/pages/index/index' ||
+ pure === '/pages/cart/cart' ||
+ pure === '/pages/user/user' ||
+ pure === '/pages/category/index'
+ )
+}
const Register = () => {
const [isAgree, setIsAgree] = useState(false)
- const reload = () => {
- Taro.hideTabBar()
- }
+ const [loading, setLoading] = useState(false)
+
+ // 短信验证码登录仅在非微信小程序端展示
+ const isWeapp = useMemo(() => {
+ try {
+ return Taro.getEnv() === Taro.ENV_TYPE.WEAPP
+ } catch (_e) {
+ return process.env.TARO_ENV === 'weapp'
+ }
+ }, [])
+
+ const router = Taro.getCurrentInstance().router
useEffect(() => {
- reload()
+ // 注册/登录页不需要展示 tabBar
+ Taro.hideTabBar()
}, [])
+ const redirectUrl = useMemo(() => {
+ const raw = (router?.params as any)?.redirect as string | undefined
+ const decoded = safeDecodeMaybeEncoded(raw)
+ if (!decoded) return ''
+ return decoded.startsWith('/') ? decoded : `/${decoded}`
+ }, [router?.params])
+
+ // 如果从分享/二维码直接进入注册页(携带 inviter/source/t),先暂存邀请信息
+ useEffect(() => {
+ try {
+ const inviteParams = parseInviteParams({ query: router?.params })
+ if (inviteParams?.inviter) {
+ saveInviteParams(inviteParams)
+ trackInviteSource(inviteParams.source || 'qrcode', parseInt(inviteParams.inviter, 10))
+ }
+ } catch (e) {
+ console.error('注册页处理邀请参数失败:', e)
+ }
+ }, [router?.params])
+
+ const navigateAfterLogin = async () => {
+ if (!redirectUrl) {
+ await Taro.reLaunch({ url: '/pages/index/index' })
+ return
+ }
+
+ if (isTabBarUrl(redirectUrl)) {
+ // switchTab 不支持携带 query,这里按纯路径跳转
+ await Taro.switchTab({ url: redirectUrl.split('?')[0] })
+ return
+ }
+
+ // 替换当前注册页,避免返回栈里再回到注册页
+ await Taro.redirectTo({ url: redirectUrl })
+ }
+
+ const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => {
+ if (!isAgree) {
+ Taro.showToast({ title: '请先勾选同意协议', icon: 'none' })
+ return
+ }
+ if (loading) return
+
+ const { code: phoneCode, encryptedData, iv, errMsg } = detail || {}
+ if (!phoneCode || (errMsg && errMsg.includes('fail'))) {
+ Taro.showToast({ title: '未授权手机号', icon: 'none' })
+ return
+ }
+
+ try {
+ setLoading(true)
+
+ // 获取存储的邀请参数(推荐人ID)
+ const inviteParams = getStoredInviteParams()
+ const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter, 10) : 0
+
+ // 获取小程序登录 code(用于后续绑定 openid)
+ const wxLoginCode = await getWeappLoginCode()
+
+ const res = (await Taro.request({
+ url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
+ method: 'POST',
+ data: {
+ code: phoneCode,
+ encryptedData,
+ iv,
+ notVerifyPhone: true,
+ refereeId: refereeId,
+ sceneType: 'save_referee',
+ tenantId: TenantId,
+ },
+ header: {
+ 'content-type': 'application/json',
+ TenantId,
+ },
+ })) as unknown as LoginResponse
+
+ if ((res as any)?.data?.code === 1) {
+ Taro.showToast({ title: res.data.message || '登录失败', icon: 'none' })
+ return
+ }
+
+ const token = res?.data?.data?.access_token
+ const user = res?.data?.data?.user
+ if (!token || !user?.userId) {
+ Taro.showToast({ title: '登录失败,请重试', icon: 'none' })
+ return
+ }
+
+ saveStorageByLoginUser(token, user)
+
+ // 注册/登录成功后,立即补齐 openid(JSAPI 支付必需)
+ try {
+ await ensureWxOpenIdSaved({ user, wxLoginCode })
+ } catch (e) {
+ console.error('注册页绑定 openid 失败:', e)
+ }
+
+ // 登录成功后尝试绑定推荐关系(如果有待处理 inviter,会自动处理并清理参数)
+ try {
+ await checkAndHandleInviteRelation()
+ } catch (e) {
+ console.error('注册页登录后处理邀请关系失败:', e)
+ }
+
+ Taro.showToast({ title: '登录成功', icon: 'success' })
+ setTimeout(() => {
+ navigateAfterLogin().catch((e) => console.error('登录后跳转失败:', e))
+ }, 800)
+ } catch (e: any) {
+ console.error('注册/登录失败:', e)
+ Taro.showToast({ title: e?.message || '登录失败', icon: 'none' })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const goSmsLogin = () => {
+ const inviteParams = getStoredInviteParams()
+ const inviter = inviteParams?.inviter
+ const source = inviteParams?.source
+ const t = inviteParams?.t
+
+ const params: Record = {}
+ if (redirectUrl) params.redirect = redirectUrl
+ // 兜底:把 inviter 带过去,避免“先点注册再进入”时丢失
+ if (inviter) params.inviter = inviter
+ if (source) params.source = source
+ if (t) params.t = t
+
+ const qs = Object.entries(params)
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
+ .join('&')
+ Taro.navigateTo({ url: `/passport/sms-login${qs ? `?${qs}` : ''}` })
+ }
+
return (
<>
-
-
免费试用14天,快速上手独立站
-
建站、选品、营销、支付、物流,全部搞定
-
- WebSoft为您提供独立站的解决方案,提供专业、高效、安全的运营服务。
+
+
注册/登录
+
+
+
+ 手机号一键注册/登录
+
+
+ {!isWeapp && (
+
+ 短信验证码注册/登录
+
+ )}
-
-
+
+
-
-
-
-
-
-
-
- 免费试用
-
-
-
-
>
)
}
+
export default Register
diff --git a/src/passport/sms-login.tsx b/src/passport/sms-login.tsx
index 62da43d..7b3dad1 100644
--- a/src/passport/sms-login.tsx
+++ b/src/passport/sms-login.tsx
@@ -3,6 +3,7 @@ import Taro from '@tarojs/taro'
import {Input, Button} from '@nutui/nutui-react-taro'
import {loginBySms, sendSmsCaptcha} from "@/api/passport/login";
import {LoginParam} from "@/api/passport/login/model";
+import {checkAndHandleInviteRelation, hasPendingInvite, parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite";
const SmsLogin = () => {
const [loading, setLoading] = useState
(false)
@@ -13,6 +14,46 @@ const SmsLogin = () => {
code: ''
})
+ const router = Taro.getCurrentInstance().router
+ const redirectParam = (router?.params as any)?.redirect as string | undefined
+
+ const safeDecodeMaybeEncoded = (input?: string) => {
+ if (!input) return ''
+ try {
+ return decodeURIComponent(input)
+ } catch (_e) {
+ return input
+ }
+ }
+
+ const redirectUrl = (() => {
+ const decoded = safeDecodeMaybeEncoded(redirectParam)
+ if (!decoded) return ''
+ return decoded.startsWith('/') ? decoded : `/${decoded}`
+ })()
+
+ const isTabBarUrl = (url: string) => {
+ const pure = url.split('?')[0]
+ return (
+ pure === '/pages/index/index' ||
+ pure === '/pages/cart/cart' ||
+ pure === '/pages/user/user' ||
+ pure === '/pages/category/index'
+ )
+ }
+
+ const navigateAfterLogin = async () => {
+ if (!redirectUrl) {
+ await Taro.reLaunch({ url: '/pages/index/index' })
+ return
+ }
+ if (isTabBarUrl(redirectUrl)) {
+ await Taro.switchTab({ url: redirectUrl.split('?')[0] })
+ return
+ }
+ await Taro.redirectTo({ url: redirectUrl })
+ }
+
const reload = () => {
Taro.hideTabBar()
}
@@ -21,6 +62,19 @@ const SmsLogin = () => {
reload()
}, [])
+ // 如果从分享/二维码链接进入短信登录页,先暂存邀请信息
+ useEffect(() => {
+ try {
+ const inviteParams = parseInviteParams({ query: router?.params })
+ if (inviteParams?.inviter) {
+ saveInviteParams(inviteParams)
+ trackInviteSource(inviteParams.source || 'share', parseInt(inviteParams.inviter, 10))
+ }
+ } catch (e) {
+ console.error('短信登录页处理邀请参数失败:', e)
+ }
+ }, [router?.params])
+
// 倒计时效果
useEffect(() => {
let timer: NodeJS.Timeout
@@ -131,6 +185,15 @@ const SmsLogin = () => {
code: formData.code
})
+ // 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定
+ if (hasPendingInvite()) {
+ try {
+ await checkAndHandleInviteRelation()
+ } catch (e) {
+ console.error('短信登录后处理邀请关系失败:', e)
+ }
+ }
+
Taro.showToast({
title: '登录成功',
icon: 'success'
@@ -138,8 +201,9 @@ const SmsLogin = () => {
// 延迟跳转到首页
setTimeout(() => {
- Taro.reLaunch({
- url: '/pages/index/index'
+ navigateAfterLogin().catch((e) => {
+ console.error('短信登录后跳转失败:', e)
+ Taro.reLaunch({ url: '/pages/index/index' })
})
}, 1500)
diff --git a/src/passport/unified-qr/index.tsx b/src/passport/unified-qr/index.tsx
index 9e87c7f..be5cda1 100644
--- a/src/passport/unified-qr/index.tsx
+++ b/src/passport/unified-qr/index.tsx
@@ -43,7 +43,7 @@ const UnifiedQRPage: React.FC = () => {
setTimeout(() => {
Taro.showModal({
title: '核销成功',
- content: '是否继续扫码核销其他礼品卡?',
+ content: '是否继续扫码核销其他水票/礼品卡?',
success: (res) => {
if (res.confirm) {
handleStartScan();
@@ -179,7 +179,7 @@ const UnifiedQRPage: React.FC = () => {
{scanType === ScanType.LOGIN ? '正在确认登录' :
- scanType === ScanType.VERIFICATION ? '正在核销礼品卡' : '正在处理'}
+ scanType === ScanType.VERIFICATION ? '正在核销' : '正在处理'}
>
)}
@@ -192,12 +192,29 @@ const UnifiedQRPage: React.FC = () => {
{result.type === ScanType.VERIFICATION && result.data && (
-
- 礼品卡:{result.data.goodsName || '未知商品'}
-
-
- 面值:¥{result.data.faceValue}
-
+ {result.data.businessType === 'gift' && result.data.gift && (
+ <>
+
+ 礼品:{result.data.gift.goodsName || result.data.gift.name || '未知'}
+
+
+ 面值:¥{result.data.gift.faceValue}
+
+ >
+ )}
+ {result.data.businessType === 'ticket' && result.data.ticket && (
+ <>
+
+ 水票:{result.data.ticket.templateName || '水票'}
+
+
+ 本次核销:{result.data.qty || 1} 次
+
+
+ 剩余可用:{result.data.ticket.availableQty ?? 0} 次
+
+ >
+ )}
)}
@@ -278,9 +295,14 @@ const UnifiedQRPage: React.FC = () => {
{record.success ? record.message : record.error}
- {record.success && record.type === ScanType.VERIFICATION && record.data && (
+ {record.success && record.type === ScanType.VERIFICATION && record.data?.businessType === 'gift' && record.data?.gift && (
- {record.data.goodsName} - ¥{record.data.faceValue}
+ {record.data.gift.goodsName || record.data.gift.name} - ¥{record.data.gift.faceValue}
+
+ )}
+ {record.success && record.type === ScanType.VERIFICATION && record.data?.businessType === 'ticket' && record.data?.ticket && (
+
+ {record.data.ticket.templateName || '水票'} - 本次核销 {record.data.qty || 1} 次
)}
@@ -304,7 +326,7 @@ const UnifiedQRPage: React.FC = () => {
• 登录二维码:自动确认网页端登录
- • 核销二维码:门店核销用户礼品卡
+ • 核销二维码:核销用户水票/礼品卡
• 系统会自动识别二维码类型并执行相应操作
diff --git a/src/rider/index.config.ts b/src/rider/index.config.ts
new file mode 100644
index 0000000..1293fcb
--- /dev/null
+++ b/src/rider/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '配送中心'
+})
diff --git a/src/rider/index.scss b/src/rider/index.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/rider/index.tsx b/src/rider/index.tsx
new file mode 100644
index 0000000..a77aef6
--- /dev/null
+++ b/src/rider/index.tsx
@@ -0,0 +1,304 @@
+import React from 'react'
+import {View, Text} from '@tarojs/components'
+import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
+import {
+ User,
+ Shopping,
+ Dongdong,
+ ArrowRight,
+ Purse,
+ People,
+ Scan
+} from '@nutui/icons-react-taro'
+import {useDealerUser} from '@/hooks/useDealerUser'
+import { useThemeStyles } from '@/hooks/useTheme'
+import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
+import Taro from '@tarojs/taro'
+
+const DealerIndex: React.FC = () => {
+ const {
+ dealerUser,
+ error,
+ refresh,
+ } = useDealerUser()
+
+ // 使用主题样式
+ const themeStyles = useThemeStyles()
+
+ // 导航到各个功能页面
+ const navigateToPage = (url: string) => {
+ Taro.navigateTo({url})
+ }
+
+ // 格式化金额
+ const formatMoney = (money?: string) => {
+ if (!money) return '0.00'
+ return parseFloat(money).toFixed(2)
+ }
+
+ // 格式化时间
+ const formatTime = (time?: string) => {
+ if (!time) return '-'
+ return new Date(time).toLocaleDateString()
+ }
+
+ // 获取用户主题
+ const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId)
+
+ // 获取渐变背景
+ const getGradientBackground = (themeColor?: string) => {
+ if (themeColor) {
+ const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30)
+ return gradientUtils.createGradient(themeColor, darkerColor)
+ }
+ return userTheme.background
+ }
+
+ console.log(getGradientBackground(),'getGradientBackground()')
+
+ if (error) {
+ return (
+
+
+ {error}
+
+
+ 重试
+
+
+ )
+ }
+
+ return (
+
+
+ {/*头部信息*/}
+ {dealerUser && (
+
+ {/* 装饰性背景元素 - 小程序兼容版本 */}
+
+
+
+
+ }
+ className="mr-4"
+ style={{
+ border: '2px solid rgba(255, 255, 255, 0.3)'
+ }}
+ />
+
+
+ {dealerUser?.realName || '分销商'}
+
+
+ ID: {dealerUser.userId}
+
+
+
+ 加入时间
+
+ {formatTime(dealerUser.createTime)}
+
+
+
+
+ )}
+
+ {/* 佣金统计卡片 */}
+ {dealerUser && (
+
+
+ 配送提成
+
+
+
+
+ {formatMoney(dealerUser.money)}
+
+ 本月配送佣金
+
+
+
+ {formatMoney(dealerUser.freezeMoney)}
+
+ 桶数
+
+
+
+ {formatMoney(dealerUser.totalMoney)}
+
+ 累计收入
+
+
+
+ )}
+
+ {/* 团队统计 */}
+ {dealerUser && (
+
+
+ 我的邀请
+ navigateToPage('/dealer/team/index')}
+ >
+ 查看详情
+
+
+
+
+
+
+ {dealerUser.firstNum || 0}
+
+ 一级成员
+
+
+
+ {dealerUser.secondNum || 0}
+
+ 二级成员
+
+
+
+ {dealerUser.thirdNum || 0}
+
+ 三级成员
+
+
+
+ )}
+
+ {/* 功能导航 */}
+
+ 配送工具
+
+
+ navigateToPage('/rider/orders/index')}>
+
+
+
+
+
+
+
+ navigateToPage('/rider/withdraw/index')}>
+
+
+
+
+
+
+
+ navigateToPage('/rider/team/index')}>
+
+
+
+
+
+
+
+ navigateToPage('/rider/qrcode/index')}>
+
+
+
+
+
+
+
+ navigateToPage('/rider/ticket/verification/index?auto=1')}>
+
+
+
+
+
+
+
+
+ {/* 第二行功能 */}
+ {/**/}
+ {/* navigateToPage('/dealer/invite-stats/index')}>*/}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+
+ {/* /!* 预留其他功能位置 *!/*/}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/**/}
+
+
+
+
+ {/* 底部安全区域 */}
+
+
+ )
+}
+
+export default DealerIndex
diff --git a/src/rider/orders/index.config.ts b/src/rider/orders/index.config.ts
new file mode 100644
index 0000000..4a1a611
--- /dev/null
+++ b/src/rider/orders/index.config.ts
@@ -0,0 +1,4 @@
+export default {
+ navigationBarTitleText: '送水订单',
+ navigationBarTextStyle: 'black'
+}
diff --git a/src/rider/orders/index.tsx b/src/rider/orders/index.tsx
new file mode 100644
index 0000000..5ad11eb
--- /dev/null
+++ b/src/rider/orders/index.tsx
@@ -0,0 +1,610 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import Taro, { useDidShow } from '@tarojs/taro'
+import {
+ Tabs,
+ TabPane,
+ Cell,
+ Space,
+ Button,
+ Dialog,
+ Radio,
+ RadioGroup,
+ Image,
+ Empty,
+ InfiniteLoading,
+ PullToRefresh,
+ Loading
+} from '@nutui/nutui-react-taro'
+import { View, Text } from '@tarojs/components'
+import dayjs from 'dayjs'
+import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder'
+import type { GltTicketOrder, GltTicketOrderParam } from '@/api/glt/gltTicketOrder/model'
+import { uploadFile } from '@/api/system/file'
+
+export default function RiderOrders() {
+ const PAGE_SIZE = 10
+
+ const riderId = useMemo(() => {
+ const v = Number(Taro.getStorageSync('UserId'))
+ return Number.isFinite(v) && v > 0 ? v : undefined
+ }, [])
+
+ const pageRef = useRef(1)
+ const listRef = useRef([])
+ const [tabIndex, setTabIndex] = useState(0)
+ const [list, setList] = useState([])
+ const [hasMore, setHasMore] = useState(true)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ const [deliverDialogVisible, setDeliverDialogVisible] = useState(false)
+ const [deliverSubmitting, setDeliverSubmitting] = useState(false)
+ const [deliverOrder, setDeliverOrder] = useState(null)
+ const [deliverImg, setDeliverImg] = useState(undefined)
+
+ type DeliverConfirmMode = 'photoComplete' | 'waitCustomerConfirm'
+ const [deliverConfirmMode, setDeliverConfirmMode] = useState('photoComplete')
+
+ const riderTabs = useMemo(
+ () => [
+ { index: 0, title: '全部' },
+ { index: 1, title: '待配送', deliveryStatus: 10 },
+ { index: 2, title: '配送中', deliveryStatus: 20 },
+ { index: 3, title: '待确认', deliveryStatus: 30 },
+ { index: 4, title: '已完成', deliveryStatus: 40 }
+ ],
+ []
+ )
+
+ const getOrderStatusText = (order: GltTicketOrder) => {
+ if (order.status === 1) return '已冻结'
+
+ const deliveryStatus = order.deliveryStatus
+ if (deliveryStatus === 40) return '已完成'
+ if (deliveryStatus === 30) return '待客户确认'
+ if (deliveryStatus === 20) return '配送中'
+ if (deliveryStatus === 10) return '待配送'
+
+ // 兼容:如果后端暂未下发 deliveryStatus,就用时间字段推断
+ if (order.receiveConfirmTime) return '已完成'
+ if (order.sendEndTime) return '待客户确认'
+ if (order.sendStartTime) return '配送中'
+ if (order.riderId) return '待配送'
+ return '待派单'
+ }
+
+ const getOrderStatusColor = (order: GltTicketOrder) => {
+ const text = getOrderStatusText(order)
+ if (text === '已完成') return 'text-green-600'
+ if (text === '待客户确认') return 'text-purple-600'
+ if (text === '配送中') return 'text-blue-600'
+ if (text === '待配送') return 'text-amber-600'
+ if (text === '已冻结') return 'text-orange-600'
+ return 'text-gray-500'
+ }
+
+ const canStartDeliver = (order: GltTicketOrder) => {
+ if (!order.id) return false
+ if (order.status === 1) return false
+ if (!riderId || order.riderId !== riderId) return false
+ if (order.deliveryStatus && order.deliveryStatus !== 10) return false
+ return !order.sendStartTime && !order.sendEndTime
+ }
+
+ const canConfirmDelivered = (order: GltTicketOrder) => {
+ if (!order.id) return false
+ if (order.status === 1) return false
+ if (!riderId || order.riderId !== riderId) return false
+ if (order.receiveConfirmTime) return false
+ if (order.deliveryStatus === 40) return false
+ if (order.sendEndTime) return false
+
+ // 只允许在“配送中”阶段确认送达
+ if (typeof order.deliveryStatus === 'number') return order.deliveryStatus === 20
+ return !!order.sendStartTime
+ }
+
+ const canCompleteByPhoto = (order: GltTicketOrder) => {
+ if (!order.id) return false
+ if (order.status === 1) return false
+ if (!riderId || order.riderId !== riderId) return false
+ if (order.receiveConfirmTime) return false
+ if (order.deliveryStatus === 40) return false
+ // 已送达但未完成:允许补传照片并直接完成
+ return !!order.sendEndTime
+ }
+
+ const filterByTab = useCallback(
+ (orders: GltTicketOrder[]) => {
+ if (tabIndex === 0) return orders
+
+ const current = riderTabs.find(t => t.index === tabIndex)
+ const status = current?.deliveryStatus
+ if (!status) return orders
+
+ // 如果后端已实现 deliveryStatus 筛选,这里基本不会再过滤;否则用兼容逻辑兜底。
+ return orders.filter(o => {
+ const ds = o.deliveryStatus
+ if (typeof ds === 'number') return ds === status
+ if (status === 10) return !!o.riderId && !o.sendStartTime && !o.sendEndTime
+ if (status === 20) return !!o.sendStartTime && !o.sendEndTime
+ if (status === 30) return !!o.sendEndTime && !o.receiveConfirmTime
+ if (status === 40) return !!o.receiveConfirmTime
+ return true
+ })
+ },
+ [riderTabs, tabIndex]
+ )
+
+ const reload = useCallback(
+ async (resetPage = false) => {
+ if (!riderId) return
+ if (loading) return
+ setLoading(true)
+ setError(null)
+
+ const currentPage = resetPage ? 1 : pageRef.current
+ const currentTab = riderTabs.find(t => t.index === tabIndex)
+ const params: GltTicketOrderParam = {
+ page: currentPage,
+ limit: PAGE_SIZE,
+ riderId,
+ deliveryStatus: currentTab?.deliveryStatus
+ }
+
+ try {
+ const res = await pageGltTicketOrder(params as any)
+ const incomingAll = (res?.list || []) as GltTicketOrder[]
+
+ // 兼容:后端若暂未实现 riderId 过滤,前端兜底过滤掉非本人的订单
+ const incoming = incomingAll.filter(o => o?.deleted !== 1 && o?.riderId === riderId)
+
+ const prev = resetPage ? [] : listRef.current
+ const next = resetPage ? incoming : prev.concat(incoming)
+ listRef.current = next
+ setList(next)
+
+ const total = typeof res?.count === 'number' ? res.count : undefined
+ const filteredOut = incomingAll.length - incoming.length
+ if (typeof total === 'number' && filteredOut === 0) {
+ setHasMore(next.length < total)
+ } else {
+ setHasMore(incomingAll.length >= PAGE_SIZE)
+ }
+
+ pageRef.current = currentPage + 1
+ } catch (e) {
+ console.error('加载配送订单失败:', e)
+ setError('加载失败,请重试')
+ setHasMore(false)
+ } finally {
+ setLoading(false)
+ }
+ },
+ [PAGE_SIZE, loading, riderId, riderTabs, tabIndex]
+ )
+
+ const reloadMore = useCallback(async () => {
+ if (loading || !hasMore) return
+ await reload(false)
+ }, [hasMore, loading, reload])
+
+ const openDeliverDialog = (order: GltTicketOrder, opts?: { mode?: DeliverConfirmMode }) => {
+ setDeliverOrder(order)
+ setDeliverImg(order.sendEndImg)
+ setDeliverConfirmMode(opts?.mode || (order.sendEndImg ? 'photoComplete' : 'waitCustomerConfirm'))
+ setDeliverDialogVisible(true)
+ }
+
+ const handleChooseDeliverImg = async () => {
+ try {
+ const file = await uploadFile()
+ setDeliverImg(file?.url)
+ } catch (e) {
+ console.error('上传送达照片失败:', e)
+ Taro.showToast({ title: '上传失败,请重试', icon: 'none' })
+ }
+ }
+
+ const handleStartDeliver = async (order: GltTicketOrder) => {
+ if (!order?.id) return
+ if (!canStartDeliver(order)) return
+ try {
+ await updateGltTicketOrder({
+ id: order.id,
+ deliveryStatus: 20,
+ sendStartTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
+ })
+ Taro.showToast({ title: '已开始配送', icon: 'success' })
+ pageRef.current = 1
+ listRef.current = []
+ setList([])
+ setHasMore(true)
+ await reload(true)
+ } catch (e) {
+ console.error('开始配送失败:', e)
+ Taro.showToast({ title: '开始配送失败', icon: 'none' })
+ }
+ }
+
+ const handleConfirmDelivered = async () => {
+ if (!deliverOrder?.id) return
+ if (deliverSubmitting) return
+ if (deliverConfirmMode === 'photoComplete' && !deliverImg) {
+ Taro.showToast({ title: '请先拍照/上传送达照片', icon: 'none' })
+ return
+ }
+ setDeliverSubmitting(true)
+ try {
+ const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
+ // 送达时间:首次“确认送达”写入;补传照片时不要覆盖原送达时间
+ const deliveredAt = deliverOrder.sendEndTime || now
+ // - waitCustomerConfirm:只标记“已送达”,进入待客户确认
+ // - photoComplete:拍照留档后可直接完成(是否允许由后端策略决定)
+ const payload: GltTicketOrder =
+ deliverConfirmMode === 'photoComplete'
+ ? {
+ id: deliverOrder.id,
+ deliveryStatus: 40,
+ sendEndTime: deliveredAt,
+ sendEndImg: deliverImg,
+ receiveConfirmTime: now,
+ receiveConfirmType: 20
+ }
+ : {
+ id: deliverOrder.id,
+ deliveryStatus: 30,
+ sendEndTime: deliveredAt,
+ sendEndImg: deliverImg
+ }
+
+ await updateGltTicketOrder(payload)
+
+ Taro.showToast({ title: '已确认送达', icon: 'success' })
+ setDeliverDialogVisible(false)
+ setDeliverOrder(null)
+ setDeliverImg(undefined)
+ setDeliverConfirmMode('photoComplete')
+ pageRef.current = 1
+ listRef.current = []
+ setList([])
+ setHasMore(true)
+ await reload(true)
+ } catch (e) {
+ console.error('确认送达失败:', e)
+ Taro.showToast({ title: '确认送达失败', icon: 'none' })
+ } finally {
+ setDeliverSubmitting(false)
+ }
+ }
+
+ useEffect(() => {
+ listRef.current = list
+ }, [list])
+
+ useDidShow(() => {
+ pageRef.current = 1
+ listRef.current = []
+ setList([])
+ setHasMore(true)
+ void reload(true)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ })
+
+ useEffect(() => {
+ pageRef.current = 1
+ listRef.current = []
+ setList([])
+ setHasMore(true)
+ void reload(true)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [tabIndex, riderId])
+
+ if (!riderId) {
+ return (
+
+ 请先登录
+
+ )
+ }
+
+ const displayList = filterByTab(list)
+
+ return (
+
+
+
+ setTabIndex(Number(paneKey))}
+ >
+ {riderTabs.map(t => (
+
+ ))}
+
+
+ {
+ pageRef.current = 1
+ listRef.current = []
+ setList([])
+ setHasMore(true)
+ await reload(true)
+ }}
+ headHeight={60}
+ >
+
+ {error ? (
+
+ {error}
+ reload(true)}>
+ 重新加载
+
+
+ ) : (
+
+
+ 加载中...
+
+ }
+ loadMoreText={
+ displayList.length === 0 ? (
+
+ ) : (
+ 没有更多了
+ )
+ }
+ >
+ {displayList.map(o => {
+ const timeText = o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'
+ const addr = o.address || (o.addressId ? `地址ID:${o.addressId}` : '-')
+ const remark = o.buyerRemarks || o.comments || ''
+ const qty = Number(o.totalNum || 0)
+
+ const flow1Done = !!o.riderId
+ const flow2Done = !!o.sendStartTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 20)
+ const flow3Done = !!o.sendEndTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 30)
+ const flow4Done = !!o.receiveConfirmTime || o.deliveryStatus === 40
+
+ const phoneToCall = o.phone
+ const storePhone = o.storePhone
+ const pickupName = o.warehouseName || o.storeName
+ const pickupAddr = o.warehouseAddress || o.storeAddress
+
+ return (
+
+
+
+
+ {o.userTicketId ? `票号#${o.userTicketId}` : '送水订单'}
+
+ {getOrderStatusText(o)}
+
+
+ 下单时间:{timeText}
+
+
+
+ 客户:
+ {o.nickname || '-'} {o.phone ? `(${o.phone})` : ''}
+
+
+ 收货地址:
+ {addr}
+
+ {!!remark && (
+
+ 买家留言:
+ {remark}
+
+ )}
+
+
+ 数量:
+ {Number.isFinite(qty) ? qty : '-'}
+ 金额:
+ ¥{o.price || '-'}
+
+
+
+ 配送时间:
+ {o.sendTime ? dayjs(o.sendTime).format('YYYY-MM-DD HH:mm') : '-'}
+
+
+
+ 取水点:
+ {pickupName || '-'}
+
+
+ 取水地址:
+ {pickupAddr || '-'}
+
+
+ {!!o.sendStartTime && (
+
+ 开始配送:
+ {dayjs(o.sendStartTime).format('YYYY-MM-DD HH:mm')}
+
+ )}
+ {!!o.sendEndTime && (
+
+ 送达时间:
+ {dayjs(o.sendEndTime).format('YYYY-MM-DD HH:mm')}
+
+ )}
+ {!!o.receiveConfirmTime && (
+
+ 完成时间:
+ {dayjs(o.receiveConfirmTime).format('YYYY-MM-DD HH:mm')}
+
+ )}
+
+ {o.sendEndImg ? (
+
+ 送达照片:
+
+
+
+
+ ) : null}
+
+
+ {/* 配送流程 */}
+
+ 流程:
+ 1 派单
+ {'>'}
+ 2 配送中
+ {'>'}
+ 3 送达留档
+ {'>'}
+ 4 完成
+
+
+
+
+ {!!phoneToCall && (
+ {
+ e.stopPropagation()
+ Taro.makePhoneCall({ phoneNumber: phoneToCall })
+ }}
+ >
+ 联系客户
+
+ )}
+ {!!addr && addr !== '-' && (
+ {
+ e.stopPropagation()
+ void Taro.setClipboardData({ data: addr })
+ Taro.showToast({ title: '地址已复制', icon: 'none' })
+ }}
+ >
+ 复制地址
+
+ )}
+ {!!storePhone && (
+ {
+ e.stopPropagation()
+ Taro.makePhoneCall({ phoneNumber: storePhone })
+ }}
+ >
+ 联系门店
+
+ )}
+ {canStartDeliver(o) && (
+ {
+ e.stopPropagation()
+ void handleStartDeliver(o)
+ }}
+ >
+ 开始配送
+
+ )}
+ {canConfirmDelivered(o) && (
+ {
+ e.stopPropagation()
+ openDeliverDialog(o, { mode: 'waitCustomerConfirm' })
+ }}
+ >
+ 确认送达
+
+ )}
+ {canCompleteByPhoto(o) && (
+ {
+ e.stopPropagation()
+ openDeliverDialog(o, { mode: 'photoComplete' })
+ }}
+ >
+ 补传照片完成
+
+ )}
+
+
+
+ |
+ )
+ })}
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/src/rider/ticket/verification/index.config.ts b/src/rider/ticket/verification/index.config.ts
new file mode 100644
index 0000000..afd29c5
--- /dev/null
+++ b/src/rider/ticket/verification/index.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '水票核销'
+})
+
diff --git a/src/rider/ticket/verification/index.tsx b/src/rider/ticket/verification/index.tsx
new file mode 100644
index 0000000..545e4ac
--- /dev/null
+++ b/src/rider/ticket/verification/index.tsx
@@ -0,0 +1,280 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react'
+import { View, Text } from '@tarojs/components'
+import Taro, { useDidShow, useRouter } from '@tarojs/taro'
+import { Button, Card, ConfigProvider } from '@nutui/nutui-react-taro'
+import { Scan, Success, Failure, Tips } from '@nutui/icons-react-taro'
+
+import { decryptQrData } from '@/api/shop/shopGift'
+import { getGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket'
+import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'
+import { isValidJSON } from '@/utils/jsonUtils'
+import { useUser } from '@/hooks/useUser'
+
+type TicketPayload = {
+ userTicketId: number
+ qty?: number
+ userId?: number
+ t?: number
+}
+
+type VerifyRecord = {
+ id: number
+ time: string
+ success: boolean
+ message: string
+ ticketName?: string
+ userInfo?: string
+ qty?: number
+}
+
+const RiderTicketVerificationPage: React.FC = () => {
+ const { hasRole, isAdmin } = useUser()
+ const router = useRouter()
+ const [loading, setLoading] = useState(false)
+ const [lastTicket, setLastTicket] = useState(null)
+ const [lastQty, setLastQty] = useState(1)
+ const [records, setRecords] = useState([])
+
+ const autoScanOnceRef = useRef(false)
+
+ const canVerify = useMemo(() => {
+ return (
+ hasRole('rider') ||
+ hasRole('store') ||
+ hasRole('staff') ||
+ hasRole('admin') ||
+ isAdmin()
+ )
+ }, [hasRole, isAdmin])
+
+ const autoScanEnabled = useMemo(() => {
+ const p: any = router?.params || {}
+ return p.auto === '1' || p.auto === 'true'
+ }, [router])
+
+ const addRecord = (rec: Omit) => {
+ const item: VerifyRecord = {
+ id: Date.now(),
+ time: new Date().toLocaleString(),
+ ...rec
+ }
+ setRecords(prev => [item, ...prev].slice(0, 10))
+ }
+
+ const parsePayload = (raw: string): TicketPayload => {
+ const trimmed = raw.trim()
+ if (!isValidJSON(trimmed)) throw new Error('无效的水票核销信息')
+ const payload = JSON.parse(trimmed) as TicketPayload
+ const userTicketId = Number(payload.userTicketId)
+ const qty = Math.max(1, Number(payload.qty || 1))
+ if (!Number.isFinite(userTicketId) || userTicketId <= 0) {
+ throw new Error('水票核销信息无效')
+ }
+ return { ...payload, userTicketId, qty }
+ }
+
+ const extractPayloadFromScanResult = async (scanResult: string): Promise => {
+ const trimmed = scanResult.trim()
+
+ // 1) 加密二维码:{ businessType, token, data }
+ if (isValidJSON(trimmed)) {
+ const json = JSON.parse(trimmed) as any
+ if (json?.businessType && json?.token && json?.data) {
+ if (json.businessType !== 'ticket') {
+ throw new Error('请扫描水票核销码')
+ }
+ const decrypted = await decryptQrData({
+ token: String(json.token),
+ encryptedData: String(json.data)
+ })
+ return parsePayload(String(decrypted || ''))
+ }
+
+ // 2) 明文 payload(内部调试/非加密二维码)
+ if (json?.userTicketId) {
+ return parsePayload(trimmed)
+ }
+ }
+
+ throw new Error('无效的水票核销码')
+ }
+
+ const verifyTicket = async (payload: TicketPayload) => {
+ const userTicketId = Number(payload.userTicketId)
+ const qty = Math.max(1, Number(payload.qty || 1))
+
+ const ticket = await getGltUserTicket(userTicketId)
+ if (!ticket) throw new Error('水票不存在')
+ if (ticket.status === 1) throw new Error('该水票已冻结')
+ const available = Number(ticket.availableQty || 0)
+ const used = Number(ticket.usedQty || 0)
+ if (available < qty) throw new Error('水票可用次数不足')
+
+ const lines: string[] = []
+ lines.push(`水票:${ticket.templateName || '水票'}`)
+ lines.push(`本次核销:${qty} 次`)
+ lines.push(`剩余可用:${available - qty} 次`)
+ if (ticket.phone) lines.push(`用户手机号:${ticket.phone}`)
+ if (ticket.nickname) lines.push(`用户昵称:${ticket.nickname}`)
+
+ const modalRes = await Taro.showModal({
+ title: '确认核销',
+ content: lines.join('\n')
+ })
+ if (!modalRes.confirm) return
+
+ await updateGltUserTicket({
+ ...ticket,
+ availableQty: available - qty,
+ usedQty: used + qty
+ })
+
+ setLastTicket({
+ ...ticket,
+ availableQty: available - qty,
+ usedQty: used + qty
+ })
+ setLastQty(qty)
+
+ addRecord({
+ success: true,
+ message: `核销成功(${qty}次)`,
+ ticketName: ticket.templateName || '水票',
+ userInfo: [ticket.nickname, ticket.phone].filter(Boolean).join(' / ') || undefined,
+ qty
+ })
+ Taro.showToast({ title: '核销成功', icon: 'success' })
+ }
+
+ const handleScan = async () => {
+ if (loading) return
+ if (!canVerify) {
+ Taro.showToast({ title: '您没有核销权限', icon: 'none' })
+ return
+ }
+
+ try {
+ setLoading(true)
+ const res = await Taro.scanCode({})
+ const scanResult = res?.result
+ if (!scanResult) throw new Error('未识别到二维码内容')
+
+ const payload = await extractPayloadFromScanResult(scanResult)
+ await verifyTicket(payload)
+ } catch (e: any) {
+ const msg = e?.message || '核销失败'
+ addRecord({ success: false, message: msg })
+ Taro.showToast({ title: msg, icon: 'none' })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // If navigated in "auto" mode, open scan on first show when user has permission.
+ useDidShow(() => {
+ // Reset the flag when user manually re-enters the page via navigation again.
+ // (This runs on every show; only the first show with auto enabled will trigger scan.)
+ if (!autoScanEnabled) autoScanOnceRef.current = false
+ })
+
+ useEffect(() => {
+ if (!autoScanEnabled) return
+ if (autoScanOnceRef.current) return
+ if (!canVerify) return
+ autoScanOnceRef.current = true
+ // Defer to ensure page is fully mounted before opening camera.
+ setTimeout(() => {
+ handleScan().catch(() => {})
+ }, 80)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [autoScanEnabled, canVerify])
+
+ return (
+
+
+
+
+
+ 水票核销
+
+
+ 扫描用户出示的“水票核销码”完成核销
+
+
+
+
+
+
+
+ }
+ onClick={handleScan}
+ >
+ 扫码核销
+
+
+
+ {lastTicket && (
+
+
+ 最近一次核销
+ 使用 {lastQty} 次
+
+
+
+ {lastTicket.templateName || '水票'}(剩余 {lastTicket.availableQty ?? 0} 次)
+
+
+
+ )}
+
+
+
+
+ 核销记录
+ 仅保留最近10条
+
+ {records.length === 0 ? (
+
+ 暂无记录
+
+ ) : (
+
+ {records.map(r => (
+
+
+
+ {r.success ? (
+
+ ) : (
+
+ )}
+ {r.message}
+
+
+
+ {r.time}
+ {r.ticketName ? ` · ${r.ticketName}` : ''}
+ {typeof r.qty === 'number' ? ` · ${r.qty}次` : ''}
+
+
+ {r.userInfo && (
+
+ {r.userInfo}
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+ )
+}
+
+export default RiderTicketVerificationPage
diff --git a/src/shop/category/components/GoodsList.scss b/src/shop/category/components/GoodsList.scss
index e69de29..de245e8 100644
--- a/src/shop/category/components/GoodsList.scss
+++ b/src/shop/category/components/GoodsList.scss
@@ -0,0 +1,122 @@
+
+.goods-grid {
+ margin-top: 18rpx;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 18rpx;
+}
+
+.goods-card {
+ border-radius: 22rpx;
+ overflow: hidden;
+ background: #ffffff;
+ box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
+}
+
+.goods-card__imgWrap {
+ padding: 18rpx 18rpx 0;
+}
+
+.goods-card__img {
+ width: 100%;
+ height: 280rpx;
+ border-radius: 18rpx;
+ background: #f4f4f4;
+}
+
+.goods-card__body {
+ padding: 18rpx 18rpx 20rpx;
+}
+
+.goods-card__title {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ font-size: 26rpx;
+ font-weight: 700;
+ color: #1c1c1c;
+ min-height: 72rpx;
+}
+
+.goods-card__meta {
+ margin-top: 10rpx;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ gap: 10rpx;
+}
+
+.goods-card__sold {
+ font-size: 22rpx;
+ color: #9a9a9a;
+ white-space: nowrap;
+}
+
+.goods-card__price {
+ display: flex;
+ align-items: baseline;
+ gap: 4rpx;
+ color: #27c86b;
+ white-space: nowrap;
+}
+
+.goods-card__priceUnit {
+ font-size: 22rpx;
+ font-weight: 800;
+}
+
+.goods-card__priceValue {
+ font-size: 36rpx;
+ font-weight: 900;
+}
+
+.goods-card__actions {
+ margin-top: 16rpx;
+ display: flex;
+ gap: 14rpx;
+}
+
+.goods-card__btn {
+ flex: 1;
+ height: 64rpx;
+ border-radius: 999rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.goods-card__btn--ghost {
+ border: 2rpx solid rgba(32, 194, 106, 0.7);
+ background: #ffffff;
+}
+
+.goods-card__btn--primary {
+ background: linear-gradient(90deg, #24d34c 0%, #6df09a 100%);
+}
+
+.goods-card__btnText {
+ font-size: 24rpx;
+ font-weight: 700;
+ color: #18b85a;
+ white-space: nowrap;
+}
+
+.goods-card__btnText--primary {
+ color: #ffffff;
+}
+
+.buy-btn{
+ height: 70px;
+ background: linear-gradient(to bottom, #1cd98a, #24ca94);
+ border-radius: 100px;
+ color: #ffffff;
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ .cart-icon{
+ background: linear-gradient(to bottom, #bbe094, #4ee265);
+ border-radius: 100px 0 0 100px;
+ height: 70px;
+ }
+}
diff --git a/src/shop/category/components/GoodsList.tsx b/src/shop/category/components/GoodsList.tsx
index f84ae9e..8066e7a 100644
--- a/src/shop/category/components/GoodsList.tsx
+++ b/src/shop/category/components/GoodsList.tsx
@@ -1,51 +1,57 @@
import {Image} from '@nutui/nutui-react-taro'
-import {Share} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
import './GoodsList.scss'
+import {ShopGoods} from "@/api/shop/shopGoods/model";
const GoodsList = (props: any) => {
return (
<>
-
-
- {props.data?.map((item: any, index: number) => {
- return (
-
- Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
-
-
- {item.name}
-
- {item.comments}
- 已售 {item.sales}
-
-
-
- ¥
- {item.price}
- 会员价
- ¥{item.salePrice}
-
-
-
- Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
-
- Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买
-
-
-
+
+
+
+ {props.data?.map((item: ShopGoods) => (
+
+
+
+ Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
+ }
+ />
+
+
+
+ {item.name}
+
+ 已购:{item.sales || 0}人
+
+ ¥
+ {item.buyingPrice}
+
+
+
+
+
+ Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
+ }
+ >
+ 立即购买
- )
- })}
+
+ ))}
>
diff --git a/src/shop/category/index.tsx b/src/shop/category/index.tsx
index 4f81fd7..74845c3 100644
--- a/src/shop/category/index.tsx
+++ b/src/shop/category/index.tsx
@@ -1,7 +1,7 @@
import Taro from '@tarojs/taro'
import GoodsList from './components/GoodsList'
import {useShareAppMessage} from "@tarojs/taro"
-import {Loading} from '@nutui/nutui-react-taro'
+import {Loading,Empty} from '@nutui/nutui-react-taro'
import {useEffect, useState} from "react"
import {useRouter} from '@tarojs/taro'
import './index.scss'
@@ -21,7 +21,7 @@ function Category() {
// 1.加载远程数据
const id = Number(params.id)
const nav = await getCmsNavigation(id)
- const shopGoods = await pageShopGoods({categoryId: id})
+ const shopGoods = await pageShopGoods({categoryId: id, status: 0})
// 2.处理业务逻辑
setCategoryId(id)
@@ -42,7 +42,7 @@ function Category() {
useShareAppMessage(() => {
return {
- title: `${nav?.categoryName}_时里院子市集`,
+ title: `${nav?.categoryName}_桂乐淘`,
path: `/shop/category/index?id=${categoryId}`,
success: function () {
console.log('分享成功');
@@ -59,6 +59,12 @@ function Category() {
)
}
+ if(list.length == 0){
+ return (
+
+ )
+ }
+
return (
<>
diff --git a/src/shop/gift/index.config.ts b/src/shop/gift/index.config.ts
new file mode 100644
index 0000000..5cf4860
--- /dev/null
+++ b/src/shop/gift/index.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '立即送水',
+ navigationBarTextStyle: 'black'
+})
diff --git a/src/shop/gift/index.tsx b/src/shop/gift/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/shop/goodsDetail/index.tsx b/src/shop/goodsDetail/index.tsx
index 37a6630..bdbf3d2 100644
--- a/src/shop/goodsDetail/index.tsx
+++ b/src/shop/goodsDetail/index.tsx
@@ -15,6 +15,10 @@ import SpecSelector from "@/components/SpecSelector";
import "./index.scss";
import {useCart} from "@/hooks/useCart";
import {useConfig} from "@/hooks/useConfig";
+import {parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite";
+import { ensureLoggedIn } from '@/utils/auth'
+import {getGltTicketTemplateByGoodsId} from "@/api/glt/gltTicketTemplate";
+import type {GltTicketTemplate} from "@/api/glt/gltTicketTemplate/model";
const GoodsDetail = () => {
const [statusBarHeight, setStatusBarHeight] = useState
(44);
@@ -30,6 +34,9 @@ const GoodsDetail = () => {
title: '',
content: ''
})
+ // 水票套票模板:存在时该商品不允许加入购物车(购物车无法支付此类商品)
+ const [ticketTemplate, setTicketTemplate] = useState(null)
+ const [ticketTemplateChecked, setTicketTemplateChecked] = useState(false)
// const [selectedSku, setSelectedSku] = useState(null);
const [loading, setLoading] = useState(false);
const router = Taro.getCurrentInstance().router;
@@ -39,17 +46,49 @@ const GoodsDetail = () => {
const {cartCount, addToCart} = useCart()
const {config} = useConfig()
+ // 如果从分享链接进入(携带 inviter/source/t),且当前未登录,则暂存邀请信息用于注册后绑定关系
+ useEffect(() => {
+ try {
+ const currentUserId = Taro.getStorageSync('UserId')
+ if (currentUserId) return
+
+ const inviteParams = parseInviteParams({query: router?.params})
+ if (inviteParams?.inviter) {
+ saveInviteParams(inviteParams)
+ trackInviteSource(inviteParams.source || 'share', parseInt(inviteParams.inviter))
+ }
+ } catch (e) {
+ // 邀请参数解析/存储失败不影响正常浏览商品
+ console.error('商品详情页处理邀请参数失败:', e)
+ }
+ // router 在 Taro 中可能不稳定;这里仅在 goodsId 变化时尝试处理一次即可
+ }, [goodsId])
+
// 处理加入购物车
- const handleAddToCart = () => {
+ const handleAddToCart = async () => {
if (!goods) return;
- if (!Taro.getStorageSync('UserId')) {
- return Taro.showToast({
- title: '请先登录',
- icon: 'none',
- duration: 2000
- });
+ // 水票套票商品:不允许加入购物车(购物车无法支付)
+ // 优先使用已加载的 ticketTemplate;若尚未加载则补一次查询
+ let tpl = ticketTemplate
+ let checked = ticketTemplateChecked
+ if (!tpl && goods?.goodsId) {
+ try {
+ tpl = await getGltTicketTemplateByGoodsId(Number(goods.goodsId))
+ setTicketTemplate(tpl)
+ setTicketTemplateChecked(true)
+ checked = true
+ } catch (_e) {
+ tpl = null
+ setTicketTemplateChecked(true)
+ checked = true
+ }
}
+ if (!checked || tpl) {
+ return
+ }
+
+ if (!ensureLoggedIn(`/shop/goodsDetail/index?id=${goods.goodsId}`)) return
// 如果有规格,显示规格选择器
if (specs.length > 0) {
@@ -71,13 +110,7 @@ const GoodsDetail = () => {
const handleBuyNow = () => {
if (!goods) return;
- if (!Taro.getStorageSync('UserId')) {
- return Taro.showToast({
- title: '请先登录',
- icon: 'none',
- duration: 2000
- });
- }
+ if (!ensureLoggedIn(`/shop/orderConfirm/index?goodsId=${goods.goodsId}`)) return
// 如果有规格,显示规格选择器
if (specs.length > 0) {
@@ -91,11 +124,30 @@ const GoodsDetail = () => {
};
// 规格选择确认回调
- const handleSpecConfirm = (sku: ShopGoodsSku, quantity: number, action?: 'cart' | 'buy') => {
+ const handleSpecConfirm = async (sku: ShopGoodsSku, quantity: number, action?: 'cart' | 'buy') => {
// setSelectedSku(sku);
setShowSpecSelector(false);
if (action === 'cart') {
+ // 水票套票商品:不允许加入购物车(购物车无法支付)
+ let tpl = ticketTemplate
+ let checked = ticketTemplateChecked
+ if (!tpl && goods?.goodsId) {
+ try {
+ tpl = await getGltTicketTemplateByGoodsId(Number(goods.goodsId))
+ setTicketTemplate(tpl)
+ setTicketTemplateChecked(true)
+ checked = true
+ } catch (_e) {
+ tpl = null
+ setTicketTemplateChecked(true)
+ checked = true
+ }
+ }
+ if (!checked || tpl) {
+ return
+ }
+
// 加入购物车
addToCart({
goodsId: goods!.goodsId!,
@@ -135,14 +187,19 @@ const GoodsDetail = () => {
}
useEffect(() => {
+ let alive = true
Taro.getSystemInfo({
success: (res) => {
+ if (!alive) return
setWindowWidth(res.windowWidth)
setStatusBarHeight(Number(res.statusBarHeight) + 5)
},
});
if (goodsId) {
setLoading(true);
+ // 切换商品时先重置套票模板,避免复用上一个商品状态
+ setTicketTemplate(null)
+ setTicketTemplateChecked(false)
// 加载商品详情
getShopGoods(Number(goodsId))
@@ -151,6 +208,7 @@ const GoodsDetail = () => {
if (res.content) {
res.content = wxParse(res.content);
}
+ if (!alive) return
setGoods(res);
if (res.files) {
const arr = JSON.parse(res.files);
@@ -161,12 +219,27 @@ const GoodsDetail = () => {
console.error("Failed to fetch goods detail:", error);
})
.finally(() => {
+ if (!alive) return
setLoading(false);
});
+ // 查询商品是否绑定水票模板(失败/无数据时不影响正常浏览)
+ getGltTicketTemplateByGoodsId(Number(goodsId))
+ .then((tpl) => {
+ if (!alive) return
+ setTicketTemplate(tpl)
+ setTicketTemplateChecked(true)
+ })
+ .catch((_e) => {
+ if (!alive) return
+ setTicketTemplate(null)
+ setTicketTemplateChecked(true)
+ })
+
// 加载商品规格
listShopGoodsSpec({goodsId: Number(goodsId)} as any)
.then((data) => {
+ if (!alive) return
setSpecs(data || []);
})
.catch((error) => {
@@ -176,19 +249,29 @@ const GoodsDetail = () => {
// 加载商品SKU
listShopGoodsSku({goodsId: Number(goodsId)} as any)
.then((data) => {
+ if (!alive) return
setSkus(data || []);
})
.catch((error) => {
console.error("Failed to fetch goods skus:", error);
});
}
+ return () => {
+ alive = false
+ }
}, [goodsId]);
// 分享给好友
useShareAppMessage(() => {
+ const inviter = Taro.getStorageSync('UserId')
+ const sharePath =
+ inviter
+ ? `/shop/goodsDetail/index?id=${goodsId}&inviter=${inviter}&source=goods_share&t=${Date.now()}`
+ : `/shop/goodsDetail/index?id=${goodsId}`
+
return {
title: goods?.name || '精选商品',
- path: `/shop/goodsDetail/index?id=${goodsId}`,
+ path: sharePath,
imageUrl: goods?.image ? `${goods.image}?x-oss-process=image/resize,w_500,h_400,m_fill` : undefined, // 分享图片,调整为5:4比例
success: function (res: any) {
console.log('分享成功', res);
@@ -213,6 +296,8 @@ const GoodsDetail = () => {
return 加载中...;
}
+ const showAddToCart = ticketTemplateChecked && !ticketTemplate
+
return (
{
¥
- {goods.price}
+ {goods.buyingPrice}
会员价
- ¥{goods.salePrice}
+ ¥{goods.salePrice}/{goods.unitName}
已售 {goods.sales}
@@ -371,10 +456,12 @@ const GoodsDetail = () => {
- handleAddToCart()}>加入购物车
-
- handleAddToCart()}>加入购物车
+
+ )}
+ handleBuyNow()}>立即购买
diff --git a/src/shop/orderConfirm/index.tsx b/src/shop/orderConfirm/index.tsx
index b48c621..c6765b0 100644
--- a/src/shop/orderConfirm/index.tsx
+++ b/src/shop/orderConfirm/index.tsx
@@ -1,4 +1,4 @@
-import {useEffect, useState} from "react";
+import {useEffect, useMemo, useState} from "react";
import {
Image,
Button,
@@ -9,6 +9,7 @@ import {
ActionSheet,
Popup,
InputNumber,
+ DatePicker,
ConfigProvider
} from '@nutui/nutui-react-taro'
import {Location, ArrowRight} from '@nutui/icons-react-taro'
@@ -27,6 +28,8 @@ import OrderConfirmSkeleton from "@/components/OrderConfirmSkeleton";
import CouponList from "@/components/CouponList";
import {CouponCardProps} from "@/components/CouponCard";
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
+import {getGltTicketTemplateByGoodsId} from "@/api/glt/gltTicketTemplate";
+import type {GltTicketTemplate} from "@/api/glt/gltTicketTemplate/model";
import {
transformCouponData,
calculateCouponDiscount,
@@ -36,7 +39,11 @@ import {
filterUsableCoupons,
filterUnusableCoupons
} from "@/utils/couponUtils";
-import navTo from "@/utils/common";
+import dayjs from 'dayjs'
+import type {ShopStore} from "@/api/shop/shopStore/model";
+import {getShopStore, listShopStore} from "@/api/shop/shopStore";
+import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
+import { ensureLoggedIn, isLoggedIn } from '@/utils/auth'
const OrderConfirm = () => {
@@ -50,6 +57,21 @@ const OrderConfirm = () => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [payLoading, setPayLoading] = useState(false)
+ // 配送时间(仅水票套票商品需要)
+ // 当日截单时间:超过该时间下单,最早配送日顺延到次日(避免 21:00 下单仍显示“当天配送”)
+ const DELIVERY_CUTOFF_HOUR = 21
+ const getMinSendDate = () => {
+ const now = dayjs()
+ const cutoff = now.hour(DELIVERY_CUTOFF_HOUR).minute(0).second(0).millisecond(0)
+ const startOfToday = now.startOf('day')
+ // >= 截单时间则最早只能选次日
+ return now.isSame(cutoff) || now.isAfter(cutoff) ? startOfToday.add(1, 'day') : startOfToday
+ }
+ const [sendTime, setSendTime] = useState(() => getMinSendDate().toDate())
+ const [sendTimePickerVisible, setSendTimePickerVisible] = useState(false)
+
+ // 水票套票活动(若存在则按规则限制最小购买量等)
+ const [ticketTemplate, setTicketTemplate] = useState(null)
// InputNumber 主题配置
const customTheme = {
@@ -67,9 +89,74 @@ const OrderConfirm = () => {
const [availableCoupons, setAvailableCoupons] = useState([])
const [couponLoading, setCouponLoading] = useState(false)
+ // 门店选择:用于在下单页展示当前“已选门店”,并允许用户切换(写入 SelectedStore Storage)
+ const [storePopupVisible, setStorePopupVisible] = useState(false)
+ const [stores, setStores] = useState([])
+ const [storeLoading, setStoreLoading] = useState(false)
+ const [selectedStore, setSelectedStore] = useState(getSelectedStoreFromStorage())
+
const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.goodsId;
+ // 页面级兜底:未登录直接进入下单页时,引导去注册/登录并回跳
+ useEffect(() => {
+ if (!goodsId) {
+ // 也可能是 orderData 模式;这里只做最小兜底
+ if (!ensureLoggedIn('/shop/orderConfirm/index')) return
+ return
+ }
+ if (!ensureLoggedIn(`/shop/orderConfirm/index?goodsId=${goodsId}`)) return
+ }, [goodsId])
+
+ const isTicketTemplateActive =
+ !!ticketTemplate &&
+ ticketTemplate.enabled !== false &&
+ ticketTemplate.status !== 1 &&
+ ticketTemplate.deleted !== 1
+ const hasTicketTemplate = !!ticketTemplate
+
+ // 套票活动最低购买量:优先取模板配置
+ const ticketMinBuyQty = (() => {
+ const n = Number(ticketTemplate?.minBuyQty)
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1
+ })()
+ const minBuyQty = isTicketTemplateActive ? ticketMinBuyQty : 1
+
+ const sendTimeText = useMemo(() => {
+ return dayjs(sendTime).format('YYYY-MM-DD')
+ }, [sendTime])
+
+ const getGiftTicketQty = (buyQty: number) => {
+ if (!isTicketTemplateActive) return 0
+ const multiplier = Number(ticketTemplate?.giftMultiplier || 0)
+ const startSend = Number(ticketTemplate?.startSendQty || 0)
+ if (multiplier > 0) return Math.max(0, buyQty) * multiplier
+ return Math.max(0, startSend)
+ }
+
+ const loadStores = async () => {
+ if (storeLoading) return
+ try {
+ setStoreLoading(true)
+ const list = await listShopStore()
+ setStores((list || []).filter(s => s?.isDelete !== 1))
+ } catch (e) {
+ console.error('获取门店列表失败:', e)
+ setStores([])
+ Taro.showToast({title: '获取门店列表失败', icon: 'none'})
+ } finally {
+ setStoreLoading(false)
+ }
+ }
+
+ // @ts-ignore
+ const openStorePopup = async () => {
+ setStorePopupVisible(true)
+ if (!stores.length) {
+ await loadStores()
+ }
+ }
+
// 计算商品总价
const getGoodsTotal = () => {
if (!goods) return 0
@@ -112,8 +199,9 @@ const OrderConfirm = () => {
// 处理数量变化
const handleQuantityChange = (value: string | number) => {
- const newQuantity = typeof value === 'string' ? parseInt(value) || 1 : value
- const finalQuantity = Math.max(1, Math.min(newQuantity, goods?.stock || 999))
+ const fallback = isTicketTemplateActive ? minBuyQty : 1
+ const newQuantity = typeof value === 'string' ? parseInt(value, 10) || fallback : value
+ const finalQuantity = Math.max(fallback, Math.min(newQuantity, goods?.stock || 999))
setQuantity(finalQuantity)
// 数量变化时,重新排序优惠券并检查当前选中的优惠券是否还可用
@@ -252,7 +340,7 @@ const OrderConfirm = () => {
}
// 加载用户优惠券
- const loadUserCoupons = async () => {
+ const loadUserCoupons = async (totalOverride?: number) => {
try {
setCouponLoading(true)
@@ -264,7 +352,7 @@ const OrderConfirm = () => {
const transformedCoupons = res.map(transformCouponData)
// 按优惠金额排序
- const total = getGoodsTotal()
+ const total = totalOverride ?? getGoodsTotal()
const sortedCoupons = sortCoupons(transformedCoupons, total)
const usableCoupons = filterUsableCoupons(sortedCoupons, total)
@@ -342,6 +430,7 @@ const OrderConfirm = () => {
* 统一支付入口
*/
const onPay = async (goods: ShopGoods) => {
+ let skipFinallyResetPayLoading = false
try {
setPayLoading(true)
@@ -362,6 +451,32 @@ const OrderConfirm = () => {
return;
}
+ // 水票套票商品:保存配送时间到 ShopOrder.sendStartTime
+ if (hasTicketTemplate && !sendTime) {
+ Taro.showToast({ title: '请选择配送时间', icon: 'none' })
+ return
+ }
+ if (hasTicketTemplate) {
+ const min = getMinSendDate()
+ if (dayjs(sendTime).isBefore(min, 'day')) {
+ setSendTime(min.toDate())
+ Taro.showToast({
+ title: `已过当日${DELIVERY_CUTOFF_HOUR}点截单,最早配送:${min.format('YYYY-MM-DD')}`,
+ icon: 'none'
+ })
+ return
+ }
+ }
+
+ // 水票套票活动:最小购买量校验
+ if (isTicketTemplateActive && quantity < minBuyQty) {
+ Taro.showToast({
+ title: `最低购买量:${minBuyQty}桶`,
+ icon: 'none'
+ })
+ return
+ }
+
// 库存校验
if (goods.stock !== undefined && quantity > goods.stock) {
Taro.showToast({
@@ -415,6 +530,9 @@ const OrderConfirm = () => {
comments: goods.name,
deliveryType: 0,
buyerRemarks: orderRemark,
+ sendStartTime: hasTicketTemplate
+ ? dayjs(sendTime).startOf('day').format('YYYY-MM-DD HH:mm:ss')
+ : undefined,
couponId: parseInt(String(bestCoupon.id), 10)
}
);
@@ -437,9 +555,12 @@ const OrderConfirm = () => {
quantity,
address.id,
{
- comments: '时里院子市集',
+ comments: '桂乐淘',
deliveryType: 0,
buyerRemarks: orderRemark,
+ sendStartTime: hasTicketTemplate
+ ? dayjs(sendTime).startOf('day').format('YYYY-MM-DD HH:mm:ss')
+ : undefined,
// 🔧 确保 couponId 是正确的数字类型,且不传递 undefined
couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined
}
@@ -482,38 +603,70 @@ const OrderConfirm = () => {
// icon: 'success'
// })
} catch (error: any) {
- return navTo('/user/order/order?statusFilter=0', true)
- // console.error('支付失败:', error)
+ const message = String(error?.message || '')
+ const isUserCancelPay =
+ message.includes('用户取消支付') ||
+ message.includes('取消支付') ||
+ message.toLowerCase().includes('requestpayment:fail cancel') ||
+ message.toLowerCase().includes('cancel')
- // 只处理PaymentHandler未处理的错误
- // if (!error.handled) {
- // let errorMessage = '支付失败,请重试';
- //
- // // 根据错误类型提供具体提示
- // if (error.message?.includes('余额不足')) {
- // errorMessage = '账户余额不足,请充值后重试';
- // } else if (error.message?.includes('优惠券')) {
- // errorMessage = '优惠券使用失败,请重新选择';
- // } else if (error.message?.includes('库存')) {
- // errorMessage = '商品库存不足,请减少购买数量';
- // } else if (error.message?.includes('地址')) {
- // errorMessage = '收货地址信息有误,请重新选择';
- // } else if (error.message) {
- // errorMessage = error.message;
- // }
- // Taro.showToast({
- // title: errorMessage,
- // icon: 'error'
- // })
- // console.log('跳去未付款的订单列表页面')
- // }
+ // 用户取消支付:跳转到待付款列表,方便继续支付
+ if (isUserCancelPay) {
+ skipFinallyResetPayLoading = true
+ setPayLoading(false)
+ const url = '/user/order/order?statusFilter=0'
+ try {
+ await Taro.redirectTo({ url })
+ } catch (_e) {
+ try {
+ await Taro.navigateTo({ url })
+ } catch (_e2) {
+ // ignore
+ }
+ }
+ return
+ }
+
+ const isOutOfDeliveryRange =
+ message.includes('不在配送范围') ||
+ message.includes('配送范围') ||
+ message.includes('电子围栏') ||
+ message.includes('围栏')
+
+ // “配送范围”类错误给出更友好的解释,并提供快捷入口去更换收货地址
+ if (isOutOfDeliveryRange) {
+ try {
+ const res = await Taro.showModal({
+ title: '暂不支持配送',
+ content: '当前收货地址超出配送范围。您可以更换收货地址后再下单,或联系门店确认配送范围。',
+ confirmText: '更换地址',
+ cancelText: '我知道了'
+ })
+ if (res?.confirm) {
+ Taro.navigateTo({ url: '/user/address/index' })
+ }
+ } catch (_e) {
+ // ignore
+ }
+ return
+ }
+
+ // 兜底:仅在 PaymentHandler 未弹过提示时再提示一次
+ if (!error?.handled) {
+ Taro.showToast({ title: message || '支付失败,请重试', icon: 'none' })
+ }
} finally {
- setPayLoading(false)
+ if (!skipFinallyResetPayLoading) {
+ setPayLoading(false)
+ }
}
};
// 统一的数据加载函数
const loadAllData = async () => {
+ // 未登录时不发起接口请求;页面会被登录兜底逻辑引导走注册/登录页
+ if (!isLoggedIn()) return
+
try {
setLoading(true)
setError('')
@@ -530,10 +683,42 @@ const OrderConfirm = () => {
])
// 设置商品信息
- if (goodsRes) {
- setGoods(goodsRes)
+ // 查询当前商品是否存在水票套票活动(失败/无数据时不影响正常下单)
+ let tpl: GltTicketTemplate | null = null
+ if (goodsId) {
+ try {
+ tpl = await getGltTicketTemplateByGoodsId(Number(goodsId))
+ } catch (e) {
+ tpl = null
+ }
}
+ const tplActive =
+ !!tpl &&
+ tpl.enabled !== false &&
+ tpl.status !== 1 &&
+ tpl.deleted !== 1
+
+ const tplMinBuyQty = (() => {
+ const n = Number(tpl?.minBuyQty)
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1
+ })()
+
+ // 设置商品信息(若存在套票模板,则默认 canBuyNumber 使用模板最小购买量)
+ if (goodsRes) {
+ const patchedGoods: ShopGoods = { ...goodsRes }
+ if (tplActive && ((patchedGoods.canBuyNumber ?? 0) === 0)) {
+ patchedGoods.canBuyNumber = tplMinBuyQty
+ }
+ setGoods(patchedGoods)
+
+ // 设置默认购买数量:优先使用 canBuyNumber,否则使用 1
+ const initQty = (patchedGoods.canBuyNumber ?? 0) > 0 ? (patchedGoods.canBuyNumber as number) : 1
+ setQuantity(initQty)
+ }
+
+ setTicketTemplate(tpl)
+
// 设置默认收货地址
if (addressRes && addressRes.length > 0) {
setAddress(addressRes[0])
@@ -548,9 +733,16 @@ const OrderConfirm = () => {
setPayment(paymentRes[0])
}
- // 加载优惠券(在商品信息加载完成后)
+ // 加载优惠券:使用“初始数量”对应的总价做推荐,避免默认数量变化导致推荐不准
if (goodsRes) {
- await loadUserCoupons()
+ const initQty = (() => {
+ const n = Number(goodsRes?.canBuyNumber)
+ if (Number.isFinite(n) && n > 0) return Math.floor(n)
+ if (tplActive) return tplMinBuyQty
+ return 1
+ })()
+ const total = parseFloat(goodsRes.price || '0') * initQty
+ await loadUserCoupons(total)
}
} catch (err) {
console.error('加载数据失败:', err)
@@ -561,10 +753,17 @@ const OrderConfirm = () => {
}
useDidShow(() => {
+ // 返回/切换到该页面时,刷新一下当前已选门店
+ if (!isLoggedIn()) return
+ setSelectedStore(getSelectedStoreFromStorage())
loadAllData()
})
useEffect(() => {
+ // 切换商品时重置配送时间,避免沿用上一次选择
+ if (!isLoggedIn()) return
+ setSendTime(getMinSendDate().toDate())
+ setSendTimePickerVisible(false)
loadAllData()
}, [goodsId]);
@@ -623,6 +822,48 @@ const OrderConfirm = () => {
)}
+ {hasTicketTemplate && (
+
+
+ {sendTimeText}
+
+ |
+ )}
+ onClick={() => {
+ // 若页面停留跨过截单时间,打开选择器前再校正一次最早可选日期
+ const min = getMinSendDate()
+ if (dayjs(sendTime).isBefore(min, 'day')) {
+ setSendTime(min.toDate())
+ }
+ setSendTimePickerVisible(true)
+ }}
+ />
+
+ )}
+
+ {/**/}
+ {/* */}
+ {/* */}
+ {/* 门店*/}
+ {/* */}
+ {/* )}*/}
+ {/* extra={(*/}
+ {/* */}
+ {/* */}
+ {/* {selectedStore?.name || '请选择门店'}*/}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* )}*/}
+ {/* onClick={openStorePopup}*/}
+ {/* />*/}
+ {/* | */}
+
@@ -634,24 +875,32 @@ const OrderConfirm = () => {
{goods.name}
- 80g/袋
+ {/*80g/袋*/}
¥{goods.price}
-
-
-
+
+
+
{goods.stock !== undefined && (
库存 {goods.stock} 件
)}
+ {isTicketTemplateActive && (
+
+ 最低购买量:{minBuyQty}桶
+ 赠送水票:{getGiftTicketQty(quantity)}张
+
+ )}
@@ -736,6 +985,20 @@ const OrderConfirm = () => {
)}/>
|
+ {ticketTemplate && (
+
+
+ 注意事项:
+ 最低起送量≥{ticketTemplate.startSendQty}桶;
+ 配送范围要在电子围栏内;
+ 上楼费暂不收取,收费另行通知。
+ |
+ )}/>
+
+ )}
+
+
{/* 支付方式选择 */}
{
+ {/* 门店选择弹窗 */}
+ setStorePopupVisible(false)}
+ >
+
+
+ 选择门店
+ setStorePopupVisible(false)}
+ >
+ 关闭
+
+
+
+ {storeLoading ? (
+
+ 加载中...
+
+ ) : (
+
+ {stores.map((s) => {
+ const isActive = !!selectedStore?.id && selectedStore.id === s.id
+ return (
+ {s.name || `门店${s.id}`} | }
+ description={s.address || ''}
+ onClick={async () => {
+ let storeToSave: ShopStore = s
+ if (s?.id) {
+ try {
+ const full = await getShopStore(s.id)
+ if (full) storeToSave = full
+ } catch (_e) {
+ // keep base item
+ }
+ }
+ setSelectedStore(storeToSave)
+ saveSelectedStoreToStorage(storeToSave)
+ setStorePopupVisible(false)
+ Taro.showToast({title: '门店已切换', icon: 'success'})
+ }}
+ />
+ )
+ })}
+ {!stores.length && (
+ 暂无门店数据} />
+ )}
+
+ )}
+
+
+
+ setSendTimePickerVisible(false)}
+ onCancel={() => setSendTimePickerVisible(false)}
+ onConfirm={(_options, selectedValue) => {
+ const [y, m, d] = (selectedValue || []).map(v => Number(v))
+ const next = new Date(y, (m || 1) - 1, d || 1, 0, 0, 0)
+ setSendTime(next)
+ setSendTimePickerVisible(false)
+ }}
+ />
+
@@ -836,6 +1173,7 @@ const OrderConfirm = () => {
type="success"
size="large"
loading={payLoading}
+ disabled={isTicketTemplateActive && quantity < minBuyQty}
onClick={() => onPay(goods)}
>
{payLoading ? '支付中...' : '立即付款'}
diff --git a/src/shop/orderConfirmCart/index.tsx b/src/shop/orderConfirmCart/index.tsx
index b9cb9dd..63acb18 100644
--- a/src/shop/orderConfirmCart/index.tsx
+++ b/src/shop/orderConfirmCart/index.tsx
@@ -2,8 +2,6 @@ import {useEffect, useState} from "react";
import {Image, Button, Cell, CellGroup, Input, Space} from '@nutui/nutui-react-taro'
import {Location, ArrowRight} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
-import {ShopGoods} from "@/api/shop/shopGoods/model";
-import {getShopGoods} from "@/api/shop/shopGoods";
import {View} from '@tarojs/components';
import {listShopUserAddress} from "@/api/shop/shopUserAddress";
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
@@ -12,14 +10,12 @@ import {useCart, CartItem} from "@/hooks/useCart";
import Gap from "@/components/Gap";
import {Payment} from "@/api/system/payment/model";
import {PaymentHandler, PaymentType, buildCartOrder} from "@/utils/payment";
+import { ensureLoggedIn } from '@/utils/auth'
const OrderConfirm = () => {
- const [goods, setGoods] = useState (null);
const [address, setAddress] = useState()
- const [payment, setPayment] = useState()
+ const [payment] = useState()
const [checkoutItems, setCheckoutItems] = useState([]);
- const router = Taro.getCurrentInstance().router;
- const goodsId = router?.params?.goodsId;
const {
cartItems,
@@ -27,13 +23,18 @@ const OrderConfirm = () => {
} = useCart();
const reload = async () => {
- const address = await listShopUserAddress({isDefault: true});
- if (address.length > 0) {
- console.log(address, '111')
- setAddress(address[0])
+ const addressList = await listShopUserAddress({isDefault: true});
+ if (addressList.length > 0) {
+ setAddress(addressList[0])
}
}
+ // 页面级兜底:防止未登录时进入结算页导致接口报错/仅提示“请先登录”
+ useEffect(() => {
+ // redirect 到当前结算页,登录成功后返回继续支付
+ if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
+ }, [])
+
// 加载结算商品数据
const loadCheckoutItems = () => {
try {
@@ -57,6 +58,8 @@ const OrderConfirm = () => {
* 统一支付入口
*/
const onPay = async () => {
+ if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
+
// 基础校验
if (!address) {
Taro.showToast({
@@ -77,7 +80,7 @@ const OrderConfirm = () => {
// 构建订单数据
const orderData = buildCartOrder(
checkoutItems.map(item => ({
- goodsId: item.goodsId!,
+ goodsId: item.goodsId,
quantity: item.quantity || 1
})),
address.id,
@@ -102,16 +105,11 @@ const OrderConfirm = () => {
};
useEffect(() => {
- if (goodsId) {
- getShopGoods(Number(goodsId)).then(res => {
- setGoods(res);
- }).catch(error => {
- console.error("Failed to fetch goods detail:", error);
- });
- }
+ if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
+
reload().then();
loadCheckoutItems();
- }, [goodsId, cartItems]);
+ }, [cartItems]);
// 计算总价
const getTotalPrice = () => {
@@ -157,19 +155,19 @@ const OrderConfirm = () => {
- {checkoutItems.map((goods, _) => (
-
+ {checkoutItems.map((item) => (
+ |
-
- {goods.name}
- 80g/袋
+ {item.name}
+ {/*80g/袋*/}
- ¥{goods.price}
- x {goods.quantity}
+ ¥{item.price}
+ x {item.quantity}
diff --git a/src/shop/orderDetail/index.tsx b/src/shop/orderDetail/index.tsx
index 7bb5b00..dca8ed1 100644
--- a/src/shop/orderDetail/index.tsx
+++ b/src/shop/orderDetail/index.tsx
@@ -1,5 +1,5 @@
import {useEffect, useState} from "react";
-import {Cell, CellGroup, Image, Space, Button} from '@nutui/nutui-react-taro'
+import {Cell, CellGroup, Image, Space, Button, Dialog} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import {ShopOrder} from "@/api/shop/shopOrder/model";
@@ -10,20 +10,34 @@ import dayjs from "dayjs";
import PaymentCountdown from "@/components/PaymentCountdown";
import './index.scss'
+// 申请退款:支付成功后仅允许在指定时间窗内发起(前端展示层限制,后端仍应校验)
+const isWithinRefundWindow = (payTime?: string, windowMinutes: number = 60): boolean => {
+ if (!payTime) return false;
+ const raw = String(payTime).trim();
+ const t = /^\d+$/.test(raw)
+ ? dayjs(Number(raw) < 1e12 ? Number(raw) * 1000 : Number(raw)) // 兼容秒/毫秒时间戳
+ : dayjs(raw);
+ if (!t.isValid()) return false;
+ return dayjs().diff(t, 'minute') <= windowMinutes;
+};
+
const OrderDetail = () => {
const [order, setOrder] = useState(null);
const [orderGoodsList, setOrderGoodsList] = useState([]);
+ const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
const router = Taro.getCurrentInstance().router;
const orderId = router?.params?.orderId;
// 处理支付超时
const handlePaymentExpired = async () => {
if (!order) return;
+ if (!order.orderId) return;
try {
// 自动取消过期订单
await updateShopOrder({
- ...order,
+ // 只传最小字段,避免误取消/误走售后流程
+ orderId: order.orderId,
orderStatus: 2 // 已取消
});
@@ -44,29 +58,62 @@ const OrderDetail = () => {
const handleApplyRefund = async () => {
if (order) {
try {
- // 更新订单状态为"退款申请中"
+ const confirm = await Taro.showModal({
+ title: '申请退款',
+ content: '确认要申请退款吗?',
+ confirmText: '确认',
+ cancelText: '取消'
+ })
+ if (!confirm?.confirm) return
+
+ Taro.showLoading({ title: '提交中...' })
+
+ // 退款相关操作使用退款接口:PUT /api/shop/shop-order/refund
await updateShopOrder({
orderId: order.orderId,
- orderStatus: 4 // 退款申请中
- });
+ refundMoney: order.payPrice || order.totalPrice,
+ orderStatus: 7
+ })
- // 更新本地状态
- setOrder(prev => prev ? {...prev, orderStatus: 4} : null);
+ // 乐观更新本地状态
+ setOrder(prev => prev ? { ...prev, orderStatus: 7 } : null)
- // 跳转到退款申请页面
- Taro.navigateTo({
- url: `/user/order/refund/index?orderId=${order.orderId}&orderNo=${order.orderNo}`
- });
+ Taro.showToast({ title: '退款申请已提交', icon: 'success' })
} catch (error) {
- console.error('更新订单状态失败:', error);
+ console.error('申请退款失败:', error);
Taro.showToast({
title: '操作失败,请重试',
icon: 'none'
});
+ } finally {
+ try {
+ Taro.hideLoading()
+ } catch (_e) {
+ // ignore
+ }
}
}
};
+ // 确认收货(客户)
+ const handleConfirmReceive = async () => {
+ if (!order?.orderId) return
+ try {
+ setConfirmReceiveDialogVisible(false)
+ await updateShopOrder({
+ orderId: order.orderId,
+ deliveryStatus: order.deliveryStatus, // 10未发货 20已发货 30部分发货
+ orderStatus: 1 // 已完成
+ })
+ Taro.showToast({title: '确认收货成功', icon: 'success'})
+ setOrder(prev => (prev ? {...prev, orderStatus: 1} : prev))
+ } catch (e) {
+ console.error('确认收货失败:', e)
+ Taro.showToast({title: '确认收货失败', icon: 'none'})
+ setConfirmReceiveDialogVisible(true)
+ }
+ }
+
const getOrderStatusText = (order: ShopOrder) => {
// 优先检查订单状态
if (order.orderStatus === 2) return '已取消';
@@ -81,8 +128,15 @@ const OrderDetail = () => {
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return '待发货';
- if (order.deliveryStatus === 20) return '待收货';
- if (order.deliveryStatus === 30) return '已收货';
+ if (order.deliveryStatus === 20) {
+ // 若订单有配送员,则以配送员送达时间作为“可确认收货”的依据
+ if (order.riderId) {
+ if (order.sendEndTime && order.orderStatus !== 1) return '待确认收货';
+ return '配送中';
+ }
+ return '待收货';
+ }
+ if (order.deliveryStatus === 30) return '部分发货';
// 最后检查订单完成状态
if (order.orderStatus === 1) return '已完成';
@@ -133,12 +187,22 @@ const OrderDetail = () => {
return 加载中... ;
}
+ const currentUserId = Number(Taro.getStorageSync('UserId'))
+ const isOwner = !!currentUserId && currentUserId === order.userId
+ const canConfirmReceive =
+ isOwner &&
+ order.payStatus &&
+ order.orderStatus !== 1 &&
+ order.deliveryStatus === 20 &&
+ (!order.riderId || !!order.sendEndTime)
+
return (
{/* 支付倒计时显示 - 详情页实时更新 */}
{!order.payStatus && order.orderStatus !== 2 && (
{
{!order.payStatus && console.log('取消订单')}>取消订单}
{!order.payStatus && console.log('立即支付')}>立即支付}
- {order.orderStatus === 1 && 申请退款}
- {order.deliveryStatus === 20 &&
- console.log('确认收货')}>确认收货}
+ {order.orderStatus === 1 && order.payStatus && isWithinRefundWindow(order.payTime, 60) && (
+ 申请退款
+ )}
+ {canConfirmReceive && (
+ setConfirmReceiveDialogVisible(true)}>
+ 确认收货
+
+ )}
+
+
);
};
diff --git a/src/store/index.config.ts b/src/store/index.config.ts
new file mode 100644
index 0000000..5ec6c4d
--- /dev/null
+++ b/src/store/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '门店中心'
+})
diff --git a/src/store/index.scss b/src/store/index.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/store/index.tsx b/src/store/index.tsx
new file mode 100644
index 0000000..948f192
--- /dev/null
+++ b/src/store/index.tsx
@@ -0,0 +1,282 @@
+import React, {useCallback, useState} from 'react'
+import {View, Text} from '@tarojs/components'
+import {Avatar, Button, ConfigProvider, Grid} from '@nutui/nutui-react-taro'
+import {Location, Scan, Shop, Shopping, User} from '@nutui/icons-react-taro'
+import Taro, {useDidShow} from '@tarojs/taro'
+import {useThemeStyles} from '@/hooks/useTheme'
+import {useUser} from '@/hooks/useUser'
+import {getSelectedStoreFromStorage} from '@/utils/storeSelection'
+import {listShopStoreUser} from '@/api/shop/shopStoreUser'
+import {getShopStore} from '@/api/shop/shopStore'
+import type {ShopStore as ShopStoreModel} from '@/api/shop/shopStore/model'
+import { goToRegister } from '@/utils/auth'
+
+const StoreIndex: React.FC = () => {
+ const themeStyles = useThemeStyles()
+ const {isLoggedIn, loading: userLoading, getAvatarUrl, getDisplayName, getRoleName, hasRole} = useUser()
+
+ const [boundStoreId, setBoundStoreId] = useState (undefined)
+ const [selectedStore, setSelectedStore] = useState(getSelectedStoreFromStorage())
+ const [store, setStore] = useState(selectedStore)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ const storeId = boundStoreId || selectedStore?.id
+
+ const parseStoreCoords = (s: ShopStoreModel): {lng: number; lat: number} | null => {
+ const raw = (s.lngAndLat || s.location || '').trim()
+ if (!raw) return null
+
+ const parts = raw.split(/[,\s]+/).filter(Boolean)
+ if (parts.length < 2) return null
+
+ const a = parseFloat(parts[0])
+ const b = parseFloat(parts[1])
+ if (Number.isNaN(a) || Number.isNaN(b)) return null
+
+ // 常见格式是 "lng,lat";这里做一个简单兜底
+ const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90
+ const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180
+ if (looksLikeLngLat) return {lng: a, lat: b}
+ if (looksLikeLatLng) return {lng: b, lat: a}
+ return null
+ }
+
+ const navigateToPage = (url: string) => {
+ if (!isLoggedIn) {
+ goToRegister({ redirect: '/store/index' })
+ return
+ }
+ Taro.navigateTo({url})
+ }
+
+ const refresh = useCallback(async () => {
+ setError(null)
+ setLoading(true)
+ try {
+ const latestSelectedStore = getSelectedStoreFromStorage()
+ setSelectedStore(latestSelectedStore)
+
+ const userIdRaw = Number(Taro.getStorageSync('UserId'))
+ const userId = Number.isFinite(userIdRaw) && userIdRaw > 0 ? userIdRaw : undefined
+
+ let foundStoreId: number | undefined = undefined
+ if (userId) {
+ // 优先按“店员绑定关系”确定门店归属
+ try {
+ const list = await listShopStoreUser({userId})
+ const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId)
+ foundStoreId = first?.storeId
+ setBoundStoreId(foundStoreId)
+ } catch {
+ // fallback to SelectedStore
+ foundStoreId = undefined
+ setBoundStoreId(undefined)
+ }
+ } else {
+ foundStoreId = undefined
+ setBoundStoreId(undefined)
+ }
+
+ const nextStoreId = (foundStoreId || latestSelectedStore?.id)
+ if (!nextStoreId) {
+ setStore(latestSelectedStore)
+ return
+ }
+
+ // 获取门店详情(用于展示门店名称/地址/仓库等)
+ const full = await getShopStore(nextStoreId)
+ setStore(full || (latestSelectedStore?.id === nextStoreId ? latestSelectedStore : ({id: nextStoreId} as ShopStoreModel)))
+ } catch (e: any) {
+ const msg = e?.message || '获取门店信息失败'
+ setError(msg)
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ // 返回/切换到该页面时,同步最新的已选门店与绑定门店
+ useDidShow(() => {
+ refresh().catch(() => {})
+ })
+
+ const openStoreLocation = () => {
+ if (!store?.id) {
+ return Taro.showToast({title: '请先选择门店', icon: 'none'})
+ }
+ const coords = parseStoreCoords(store)
+ if (!coords) {
+ return Taro.showToast({title: '门店未配置定位', icon: 'none'})
+ }
+ Taro.openLocation({
+ latitude: coords.lat,
+ longitude: coords.lng,
+ name: store.name || '门店',
+ address: store.address || ''
+ })
+ }
+
+ if (!isLoggedIn && !userLoading) {
+ return (
+
+
+ 请先登录后再进入门店中心
+
+ goToRegister({ redirect: '/store/index' })}>
+ 去注册/登录
+
+
+
+
+ )
+ }
+
+ return (
+
+ {/* 头部信息 */}
+
+
+
+
+
+
+ }
+ className="mr-4"
+ style={{border: '2px solid rgba(255, 255, 255, 0.3)'}}
+ />
+
+
+ {getDisplayName()}
+
+
+ {hasRole('store') ? '门店' : hasRole('rider') ? '配送员' : getRoleName()}
+
+
+
+ 刷新
+
+
+
+
+ {/* 门店信息 */}
+
+
+ 当前门店
+ Taro.switchTab({url: '/pages/index/index'})}
+ >
+ 切换门店
+
+
+
+ {!storeId ? (
+
+
+ 未选择门店,请先去首页选择门店。
+
+
+ Taro.switchTab({url: '/pages/index/index'})}>
+ 去首页选择门店
+
+
+
+ ) : (
+
+
+ {store?.name || `门店ID: ${storeId}`}
+
+ {!!store?.address && (
+
+ {store.address}
+
+ )}
+ {!!store?.warehouseName && (
+
+ 默认仓库:{store.warehouseName}
+
+ )}
+ {!!error && (
+
+ {error}
+
+ )}
+
+ )}
+
+
+ {/* 功能入口 */}
+
+ 门店工具
+
+
+ navigateToPage('/store/orders/index')}>
+
+
+
+
+
+
+
+ navigateToPage('/user/store/verification')}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Taro.switchTab({url: '/pages/index/index'})}>
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default StoreIndex
diff --git a/src/store/orders/index.config.ts b/src/store/orders/index.config.ts
new file mode 100644
index 0000000..4de19d9
--- /dev/null
+++ b/src/store/orders/index.config.ts
@@ -0,0 +1,4 @@
+export default {
+ navigationBarTitleText: '门店订单',
+ navigationBarTextStyle: 'black'
+}
diff --git a/src/store/orders/index.tsx b/src/store/orders/index.tsx
new file mode 100644
index 0000000..bcd5e8f
--- /dev/null
+++ b/src/store/orders/index.tsx
@@ -0,0 +1,83 @@
+import {useEffect, useMemo, useState} from 'react'
+import Taro from '@tarojs/taro'
+import {Button} from '@nutui/nutui-react-taro'
+import {View, Text} from '@tarojs/components'
+import OrderList from '@/user/order/components/OrderList'
+import {getSelectedStoreFromStorage} from '@/utils/storeSelection'
+import {listShopStoreUser} from '@/api/shop/shopStoreUser'
+
+export default function StoreOrders() {
+ const [boundStoreId, setBoundStoreId] = useState(undefined)
+
+ const isLoggedIn = useMemo(() => {
+ return !!Taro.getStorageSync('access_token') && !!Taro.getStorageSync('UserId')
+ }, [])
+
+ const selectedStore = useMemo(() => getSelectedStoreFromStorage(), [])
+ const storeId = boundStoreId || selectedStore?.id
+
+ useEffect(() => {
+ }, [])
+
+ useEffect(() => {
+ // 优先按“店员绑定关系”确定门店归属:门店看到的是自己的订单
+ const userId = Number(Taro.getStorageSync('UserId'))
+ if (!Number.isFinite(userId) || userId <= 0) return
+ listShopStoreUser({userId}).then(list => {
+ const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId)
+ if (first?.storeId) setBoundStoreId(first.storeId)
+ }).catch(() => {
+ // fallback to SelectedStore
+ })
+ }, [])
+
+ if (!isLoggedIn) {
+ return (
+
+
+ 请先登录
+
+ Taro.navigateTo({url: '/passport/login'})}>
+ 去登录
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ 当前门店:
+
+ {boundStoreId
+ ? (selectedStore?.id === boundStoreId ? (selectedStore?.name || `门店ID: ${boundStoreId}`) : `门店ID: ${boundStoreId}`)
+ : (selectedStore?.name || '未选择门店')}
+
+
+
+ {!storeId ? (
+
+
+ 请先在首页左上角选择门店,再查看门店订单。
+
+
+ Taro.switchTab({url: '/pages/index/index'})}
+ >
+ 去首页选择门店
+
+
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/src/types/giftCard.ts b/src/types/giftCard.ts
index 9178651..3736e25 100644
--- a/src/types/giftCard.ts
+++ b/src/types/giftCard.ts
@@ -4,7 +4,7 @@
/** 礼品卡类型枚举 */
export enum GiftCardType {
- /** 实物礼品卡 */
+ /** 礼品劵 */
PHYSICAL = 10,
/** 虚拟礼品卡 */
VIRTUAL = 20,
diff --git a/src/user/address/add.tsx b/src/user/address/add.tsx
index 9ec01f5..dba8324 100644
--- a/src/user/address/add.tsx
+++ b/src/user/address/add.tsx
@@ -1,6 +1,6 @@
import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
-import {Button, Loading, CellGroup, Input, TextArea, Form} from '@nutui/nutui-react-taro'
+import {Button, Loading, CellGroup, Cell, Input, TextArea, Form} from '@nutui/nutui-react-taro'
import {Scan, ArrowRight} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
@@ -9,6 +9,34 @@ import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
import {getShopUserAddress, listShopUserAddress, updateShopUserAddress, addShopUserAddress} from "@/api/shop/shopUserAddress";
import RegionData from '@/api/json/regions-data.json';
import FixedButton from "@/components/FixedButton";
+import { parseLngLatFromText } from "@/utils/geofence";
+
+type SelectedLocation = { lng: string; lat: string; name?: string; address?: string }
+
+const isLocationDenied = (e: any) => {
+ const msg = String(e?.errMsg || e?.message || e || '')
+ return (
+ msg.includes('auth deny') ||
+ msg.includes('authorize') ||
+ msg.includes('permission') ||
+ msg.includes('denied') ||
+ msg.includes('scope.userLocation')
+ )
+}
+
+const isUserCancel = (e: any) => {
+ const msg = String(e?.errMsg || e?.message || e || '')
+ return msg.includes('cancel')
+}
+
+const hasValidLngLat = (addr?: Partial | null) => {
+ if (!addr) return false
+ const p = parseLngLatFromText(`${(addr as any)?.lng ?? ''},${(addr as any)?.lat ?? ''}`)
+ if (!p) return false
+ // Treat "0,0" as missing in this app (typically used as placeholder by backends).
+ if (p.lng === 0 && p.lat === 0) return false
+ return true
+}
const AddUserAddress = () => {
const {params} = useRouter();
@@ -18,16 +46,73 @@ const AddUserAddress = () => {
const [visible, setVisible] = useState(false)
const [FormData, setFormData] = useState({})
const [inputText, setInputText] = useState('')
+ const [selectedLocation, setSelectedLocation] = useState(null)
+ const [regionLocked, setRegionLocked] = useState(false)
const formRef = useRef(null)
+ const wxDraftRef = useRef | null>(null)
+ const wxDraftPatchedRef = useRef(false)
// 判断是编辑还是新增模式
const isEditMode = !!params.id
const addressId = params.id ? Number(params.id) : undefined
+ const fromWx = params.fromWx === '1' || params.fromWx === 'true'
+ const skipDefaultCheck =
+ fromWx || params.skipDefaultCheck === '1' || params.skipDefaultCheck === 'true'
const reload = async () => {
// 整理地区数据
setRegionData()
+ // 新增模式:若存在“默认地址”但缺少经纬度,则强制引导到编辑页完善定位
+ // 目的:确保业务依赖默认地址坐标(选店/配送范围)时不会因为缺失坐标而失败
+ if (!isEditMode && !skipDefaultCheck) {
+ try {
+ const defaultList = await listShopUserAddress({ isDefault: true })
+ const defaultAddr = defaultList?.[0]
+ if (defaultAddr && !hasValidLngLat(defaultAddr)) {
+ await Taro.showModal({
+ title: '需要完善定位',
+ content: '默认收货地址缺少定位信息,请先进入编辑页面选择定位并保存后再继续。',
+ confirmText: '去完善',
+ showCancel: false
+ })
+ if (defaultAddr.id) {
+ Taro.navigateTo({ url: `/user/address/add?id=${defaultAddr.id}` })
+ } else {
+ Taro.navigateTo({ url: '/user/address/index' })
+ }
+ return
+ }
+ } catch (_e) {
+ // ignore: 新增页不阻塞渲染
+ }
+ }
+
+ // 微信地址导入:先用微信返回的字段预填表单,让用户手动选择定位后再保存
+ if (!isEditMode && fromWx && !wxDraftPatchedRef.current) {
+ try {
+ const draft = Taro.getStorageSync('WxAddressDraft')
+ if (draft) {
+ wxDraftPatchedRef.current = true
+ wxDraftRef.current = draft as any
+ Taro.removeStorageSync('WxAddressDraft')
+
+ setFormData(prev => ({
+ ...prev,
+ ...(draft as any)
+ }))
+
+ const p = String((draft as any)?.province || '').trim()
+ const c = String((draft as any)?.city || '').trim()
+ const r = String((draft as any)?.region || '').trim()
+ const regionText = [p, c, r].filter(Boolean).join(' ')
+ if (regionText) setText(regionText)
+ }
+ } catch (_e) {
+ // ignore
+ }
+ }
+
// 如果是编辑模式,加载地址数据
if (isEditMode && addressId) {
try {
@@ -35,6 +120,13 @@ const AddUserAddress = () => {
setFormData(address)
// 设置所在地区
setText(`${address.province} ${address.city} ${address.region}`)
+ // 回显已保存的经纬度(编辑模式)
+ if (hasValidLngLat(address)) {
+ setSelectedLocation({ lng: String(address.lng), lat: String(address.lat) })
+ setRegionLocked(true)
+ } else {
+ setRegionLocked(false)
+ }
} catch (error) {
console.error('加载地址失败:', error)
Taro.showToast({
@@ -86,30 +178,39 @@ const AddUserAddress = () => {
const result = parseAddressText(inputText);
// 更新表单数据
- const newFormData = {
+ const newFormData: any = {
...FormData,
name: result.name || FormData.name,
phone: result.phone || FormData.phone,
- address: result.address || FormData.address,
- province: result.province || FormData.province,
- city: result.city || FormData.city,
- region: result.region || FormData.region
+ address: result.address || FormData.address
};
+ if (!regionLocked) {
+ newFormData.province = result.province || FormData.province
+ newFormData.city = result.city || FormData.city
+ newFormData.region = result.region || FormData.region
+ }
+
setFormData(newFormData);
// 更新地区显示文本
- if (result.province && result.city && result.region) {
+ if (!regionLocked && result.province && result.city && result.region) {
setText(`${result.province} ${result.city} ${result.region}`);
}
// 更新表单字段值
if (formRef.current) {
- formRef.current.setFieldsValue(newFormData);
+ const patch: any = {
+ name: newFormData.name,
+ phone: newFormData.phone,
+ address: newFormData.address
+ }
+ if (!regionLocked && newFormData.region) patch.region = newFormData.region
+ formRef.current.setFieldsValue(patch);
}
Taro.showToast({
- title: '识别成功',
+ title: regionLocked ? '识别成功(所在地区以定位为准)' : '识别成功',
icon: 'success'
});
@@ -210,15 +311,153 @@ const AddUserAddress = () => {
return null;
};
+ // 选择定位:打开地图让用户选点,保存经纬度到表单数据
+ const chooseGeoLocation = async () => {
+ const applyChosenLocation = (res: any) => {
+ if (!res) return
+ if (res.latitude === undefined || res.longitude === undefined) {
+ Taro.showToast({ title: '定位信息获取失败', icon: 'none' })
+ return
+ }
+
+ const next: SelectedLocation = {
+ lng: String(res.longitude),
+ lat: String(res.latitude),
+ name: res.name,
+ address: res.address
+ }
+
+ // 尝试从地图返回的 address 文本解析省市区(best-effort)
+ const regionResult = res?.provinceName || res?.cityName || res?.adName
+ ? {
+ province: String(res.provinceName || ''),
+ city: String(res.cityName || ''),
+ region: String(res.adName || '')
+ }
+ : parseRegion(String(res.address || ''))
+
+ const province = String(regionResult?.province || '').trim()
+ const city = String(regionResult?.city || '').trim()
+ const region = String(regionResult?.region || '').trim()
+ if (!province || !city || !region) {
+ Taro.showToast({ title: '定位未识别到所在地区,请重新选择定位', icon: 'none' })
+ return
+ }
+
+ setSelectedLocation(next)
+ setRegionLocked(true)
+
+ // 将地图选点的地址同步到“收货地址”(不额外拼接省市区字段,省市区由独立字段保存)
+ const nextDetailAddress = (() => {
+ const rawAddr = String(res.address || '').trim()
+ const name = String(res.name || '').trim()
+
+ // 选择定位返回的 address 往往包含省市区,这里尽量剥离掉,避免和表单的省市区字段重复
+ let detail = rawAddr
+ for (const part of [province, city, region]) {
+ if (part) detail = detail.replace(part, '')
+ }
+ detail = detail.replace(/[,,]+/g, ' ').replace(/\s+/g, ' ').trim()
+
+ const base = detail || rawAddr
+ if (!base && !name) return ''
+ if (!base) return name
+ if (!name) return base
+ return base.includes(name) ? base : `${base} ${name}`
+ })()
+
+ setFormData(prev => ({
+ ...prev,
+ lng: next.lng,
+ lat: next.lat,
+ address: nextDetailAddress || prev.address,
+ province,
+ city,
+ region
+ }))
+
+ setText(`${province} ${city} ${region}`)
+
+ // 更新表单展示值(Form initialValues 不会跟随 FormData 变化)
+ if (formRef.current) {
+ const patch: any = {}
+ if (nextDetailAddress) patch.address = nextDetailAddress
+ patch.region = region
+ formRef.current.setFieldsValue(patch)
+ }
+ }
+
+ try {
+ const initLat = selectedLocation?.lat ? Number(selectedLocation.lat) : undefined
+ const initLng = selectedLocation?.lng ? Number(selectedLocation.lng) : undefined
+ const latitude = typeof initLat === 'number' && Number.isFinite(initLat) ? initLat : undefined
+ const longitude = typeof initLng === 'number' && Number.isFinite(initLng) ? initLng : undefined
+ const res = await Taro.chooseLocation({
+ latitude,
+ longitude
+ })
+ applyChosenLocation(res)
+ } catch (e: any) {
+ console.warn('选择定位失败:', e)
+ if (isUserCancel(e)) return
+ if (isLocationDenied(e)) {
+ try {
+ const modal = await Taro.showModal({
+ title: '需要定位权限',
+ content: '选择定位需要开启定位权限,请在设置中开启后重试。',
+ confirmText: '去设置'
+ })
+ if (modal.confirm) {
+ await Taro.openSetting()
+ // 权限可能刚被开启:重试一次
+ const res = await Taro.chooseLocation({})
+ applyChosenLocation(res)
+ }
+ } catch (_e) {
+ // ignore
+ }
+ return
+ }
+ try {
+ await Taro.showToast({ title: '打开地图失败,请重试', icon: 'none' })
+ } catch (_e) {
+ // ignore
+ }
+ }
+ }
+
+ const openRegionPicker = () => {
+ if (regionLocked) {
+ Taro.showToast({ title: '所在地区已由定位确定,修改请重新选择定位', icon: 'none' })
+ return
+ }
+ setVisible(true)
+ }
+
// 提交表单
const submitSucceed = async (values: any) => {
+ const loc =
+ selectedLocation ||
+ (hasValidLngLat(FormData) ? { lng: String(FormData.lng), lat: String(FormData.lat) } : null)
+ if (!loc) {
+ Taro.showToast({ title: '请选择定位', icon: 'none' })
+ return
+ }
+ if (!FormData.province || !FormData.city || !FormData.region) {
+ Taro.showToast({ title: '请先选择定位以自动填写所在地区', icon: 'none' })
+ return
+ }
+
try {
// 准备提交的数据
const submitData = {
...values,
+ country: FormData.country,
province: FormData.province,
city: FormData.city,
region: FormData.region,
+ lng: loc.lng,
+ lat: loc.lat,
isDefault: true // 新增或编辑的地址都设为默认地址
};
@@ -271,13 +510,40 @@ const AddUserAddress = () => {
useEffect(() => {
// 动态设置页面标题
Taro.setNavigationBarTitle({
- title: isEditMode ? '编辑收货地址' : '新增收货地址'
+ title: isEditMode ? '编辑收货地址' : (fromWx ? '完善收货地址' : '新增收货地址')
});
reload().then(() => {
setLoading(false)
})
- }, [isEditMode]);
+ }, [fromWx, isEditMode]);
+
+ useEffect(() => {
+ if (!regionLocked) return
+ if (!visible) return
+ setVisible(false)
+ }, [regionLocked, visible])
+
+ // NutUI Form 的 initialValues 在首次渲染后不再响应更新;微信导入时做一次 setFieldsValue 兜底回填。
+ useEffect(() => {
+ if (loading) return
+ if (isEditMode) return
+ const draft = wxDraftRef.current
+ if (!draft) return
+ if (!formRef.current?.setFieldsValue) return
+ try {
+ formRef.current.setFieldsValue({
+ name: (draft as any)?.name,
+ phone: (draft as any)?.phone,
+ address: (draft as any)?.address,
+ region: (draft as any)?.region
+ })
+ } catch (_e) {
+ // ignore
+ } finally {
+ wxDraftRef.current = null
+ }
+ }, [fromWx, isEditMode, loading])
if (loading) {
return 加载中
@@ -294,7 +560,7 @@ const AddUserAddress = () => {
onFinishFailed={(errors) => submitFailed(errors)}
>
- {
>
识别
-
+
-
+
-
+
{
rules={[{message: '请输入您的所在地区'}]}
required
>
-
+
+
+
+
+ {selectedLocation?.name || (selectedLocation ? '已选择' : '请选择')}
+
+
+
+ )}
+ onClick={chooseGeoLocation}
+ />
+ |
{
options={optionsDemo1}
title="选择地址"
onChange={(value, _) => {
+ if (regionLocked) {
+ Taro.showToast({ title: '所在地区已由定位确定,修改请重新选择定位', icon: 'none' })
+ return
+ }
setFormData({
...FormData,
province: `${value[0]}`,
@@ -365,14 +671,14 @@ const AddUserAddress = () => {
/>
{/* 底部浮动按钮 */}
- {
// 触发表单提交
if (formRef.current) {
formRef.current.submit();
}
- }}
+ }}
/>
>
);
diff --git a/src/user/address/index.config.ts b/src/user/address/index.config.ts
index e1884c6..672b958 100644
--- a/src/user/address/index.config.ts
+++ b/src/user/address/index.config.ts
@@ -1,4 +1,4 @@
export default definePageConfig({
- navigationBarTitleText: '地址管理',
+ navigationBarTitleText: '配送管理',
navigationBarTextStyle: 'black'
})
diff --git a/src/user/address/index.tsx b/src/user/address/index.tsx
index f9328db..52c6f34 100644
--- a/src/user/address/index.tsx
+++ b/src/user/address/index.tsx
@@ -1,16 +1,58 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
-import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Divider} from '@nutui/nutui-react-taro'
-import {Dongdong, ArrowRight, CheckNormal, Checked} from '@nutui/icons-react-taro'
+import {Button, Cell, Space, Empty, ConfigProvider, Divider} from '@nutui/nutui-react-taro'
+import {CheckNormal, Checked} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
import {listShopUserAddress, removeShopUserAddress, updateShopUserAddress} from "@/api/shop/shopUserAddress";
import FixedButton from "@/components/FixedButton";
+import dayjs from "dayjs";
const Address = () => {
const [list, setList] = useState([])
const [address, setAddress] = useState()
+ const safeNavigateBack = async () => {
+ try {
+ const pages = (Taro as any).getCurrentPages?.() || []
+ if (Array.isArray(pages) && pages.length > 1) {
+ await Taro.navigateBack()
+ return true
+ }
+ } catch (_e) {
+ // ignore
+ }
+ return false
+ }
+
+ const parseTime = (raw?: unknown) => {
+ if (raw === undefined || raw === null || raw === '') return null;
+ // 兼容秒/毫秒时间戳
+ if (typeof raw === 'number' || (typeof raw === 'string' && /^\d+$/.test(raw))) {
+ const n = Number(raw);
+ return dayjs(Number.isFinite(n) ? (n < 1e12 ? n * 1000 : n) : raw as any);
+ }
+ return dayjs(raw as any);
+ }
+
+ const canModifyOncePerMonth = (item: ShopUserAddress) => {
+ const lastUpdate = parseTime(item.updateTime);
+ if (!lastUpdate || !lastUpdate.isValid()) return { ok: true as const };
+
+ // 若 updateTime 与 createTime 基本一致,则视为“未修改过”,不做限制
+ const createdAt = parseTime(item.createTime);
+ if (createdAt && createdAt.isValid() && Math.abs(lastUpdate.diff(createdAt, 'minute')) < 1) {
+ return { ok: true as const };
+ }
+
+ const nextAllowed = lastUpdate.add(1, 'month');
+ const now = dayjs();
+ if (now.isBefore(nextAllowed)) {
+ return { ok: false as const, nextAllowed: nextAllowed.format('YYYY-MM-DD HH:mm') };
+ }
+ return { ok: true as const };
+ }
+
const reload = () => {
listShopUserAddress({
userId: Taro.getStorageSync('UserId')
@@ -29,6 +71,8 @@ const Address = () => {
}
const onDefault = async (item: ShopUserAddress) => {
+ if (item.isDefault) return
+
if (address) {
await updateShopUserAddress({
...address,
@@ -36,14 +80,18 @@ const Address = () => {
})
}
await updateShopUserAddress({
- id: item.id,
- isDefault: true
+ ...item,
+ isDefault: true,
})
Taro.showToast({
title: '设置成功',
icon: 'success'
});
- reload();
+ // 设置默认地址通常是“选择地址”的动作:成功后返回上一页,体验更顺滑
+ setTimeout(async () => {
+ const backed = await safeNavigateBack()
+ if (!backed) reload()
+ }, 400)
}
const onDel = async (id?: number) => {
@@ -56,6 +104,12 @@ const Address = () => {
}
const selectAddress = async (item: ShopUserAddress) => {
+ if (item.isDefault) {
+ const backed = await safeNavigateBack()
+ if (!backed) reload()
+ return
+ }
+
if (address) {
await updateShopUserAddress({
...address,
@@ -63,11 +117,12 @@ const Address = () => {
})
}
await updateShopUserAddress({
- id: item.id,
- isDefault: true
+ ...item,
+ isDefault: true,
})
- setTimeout(() => {
- Taro.navigateBack()
+ setTimeout(async () => {
+ const backed = await safeNavigateBack()
+ if (!backed) reload()
}, 500)
}
@@ -89,8 +144,8 @@ const Address = () => {
/>
Taro.navigateTo({url: '/user/address/add'})}>新增地址
- Taro.navigateTo({url: '/user/address/wxAddress'})}>获取微信地址
+ {/* Taro.navigateTo({url: '/user/address/wxAddress'})}>获取微信地址*/}
@@ -99,19 +154,19 @@ const Address = () => {
return (
<>
-
- Taro.navigateTo({url: '/user/address/wxAddress'})}
- >
-
- |
-
+ {/**/}
+ {/* Taro.navigateTo({url: '/user/address/wxAddress'})}*/}
+ {/* >*/}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* 获取微信地址 */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* | */}
+ {/**/}
{list.map((item, _) => (
selectAddress(item)}>
@@ -137,7 +192,17 @@ const Address = () => {
Taro.navigateTo({url: '/user/address/add?id=' + item.id})}>
+ onClick={() => {
+ const { ok, nextAllowed } = canModifyOncePerMonth(item);
+ if (!ok) {
+ Taro.showToast({
+ title: `一个月只能修改一次${nextAllowed ? ',' + nextAllowed + ' 后可再次修改' : ''}`,
+ icon: 'none',
+ });
+ return;
+ }
+ Taro.navigateTo({url: '/user/address/add?id=' + item.id})
+ }}>
修改
>
diff --git a/src/user/address/wxAddress.tsx b/src/user/address/wxAddress.tsx
index 30fed0a..1eb0486 100644
--- a/src/user/address/wxAddress.tsx
+++ b/src/user/address/wxAddress.tsx
@@ -1,17 +1,17 @@
import {useEffect} from "react";
import Taro from '@tarojs/taro'
-import {addShopUserAddress} from "@/api/shop/shopUserAddress";
const WxAddress = () => {
/**
* 从微信API获取用户收货地址
- * 调用微信原生地址选择界面,获取成功后保存到服务器并刷新列表
+ * 调用微信原生地址选择界面,获取成功后跳转到“新增收货地址”页面,让用户选择定位后再保存
*/
const getWeChatAddress = () => {
Taro.chooseAddress()
- .then(res => {
- // 格式化微信返回的地址数据为后端所需格式
- const addressData = {
+ .then(async res => {
+ // 仅填充微信地址信息,不要用“当前定位”覆盖经纬度(会造成经纬度与地址不匹配)。
+ // 选择后跳转到“新增/编辑收货地址”页面,让用户手动选择地图定位后再保存。
+ const addressDraft = {
name: res.userName,
phone: res.telNumber,
country: res.nationalCode || '中国',
@@ -19,38 +19,32 @@ const WxAddress = () => {
city: res.cityName,
region: res.countyName,
address: res.detailInfo,
- postalCode: res.postalCode,
- isDefault: false
+ isDefault: false,
}
- console.log(res, 'addrs..')
- // 调用保存地址的API(假设存在该接口)
- addShopUserAddress(addressData)
- .then((msg) => {
- console.log(msg)
- Taro.showToast({
- title: `${msg}`,
- icon: 'none'
- })
- setTimeout(() => {
- // 保存成功后返回
- Taro.navigateBack()
- }, 1000)
- })
- .catch(error => {
- console.error('保存地址失败:', error)
- Taro.showToast({title: '保存地址失败', icon: 'error'})
- })
+ Taro.setStorageSync('WxAddressDraft', addressDraft)
+ // 用 redirectTo 替换当前页面,避免保存后 navigateBack 回到空白的 wxAddress 页面。
+ await Taro.redirectTo({ url: '/user/address/add?fromWx=1&skipDefaultCheck=1' })
})
.catch(err => {
console.error('获取微信地址失败:', err)
+ // 用户取消选择地址:直接返回上一页
+ if (String(err?.errMsg || '').includes('cancel')) {
+ setTimeout(() => Taro.navigateBack(), 200)
+ return
+ }
// 处理用户拒绝授权的情况
- if (err.errMsg.includes('auth deny')) {
+ if (String(err?.errMsg || '').includes('auth deny')) {
Taro.showModal({
title: '授权失败',
content: '请在设置中允许获取地址权限',
showCancel: false
})
+ setTimeout(() => Taro.navigateBack(), 300)
+ return
}
+
+ Taro.showToast({ title: '获取微信地址失败', icon: 'none' })
+ setTimeout(() => Taro.navigateBack(), 300)
})
}
diff --git a/src/user/gift/detail.tsx b/src/user/gift/detail.tsx
index e2ab775..b94841e 100644
--- a/src/user/gift/detail.tsx
+++ b/src/user/gift/detail.tsx
@@ -43,10 +43,10 @@ const GiftCardDetail = () => {
// 获取礼品卡类型文本
const getGiftTypeText = (type?: number) => {
switch (type) {
- case 10: return '实物礼品卡'
+ case 10: return '礼品劵'
case 20: return '虚拟礼品卡'
case 30: return '服务礼品卡'
- default: return '礼品卡'
+ default: return '水票'
}
}
diff --git a/src/user/gift/index.config.ts b/src/user/gift/index.config.ts
index c3474f2..521bff3 100644
--- a/src/user/gift/index.config.ts
+++ b/src/user/gift/index.config.ts
@@ -1,5 +1,5 @@
export default definePageConfig({
- navigationBarTitleText: '我的礼品卡',
+ navigationBarTitleText: '我的水票',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff'
})
diff --git a/src/user/gift/index.tsx b/src/user/gift/index.tsx
index aaae782..d7f5fab 100644
--- a/src/user/gift/index.tsx
+++ b/src/user/gift/index.tsx
@@ -1,7 +1,7 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Empty, ConfigProvider,SearchBar, InfiniteLoading, Loading, PullToRefresh, Tabs, TabPane} from '@nutui/nutui-react-taro'
-import {Gift, Retweet, Board, QrCode} from '@nutui/icons-react-taro'
+import {Gift, Retweet, QrCode} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {ShopGift} from "@/api/shop/shopGift/model";
import {getUserGifts} from "@/api/shop/shopGift";
@@ -24,7 +24,7 @@ const GiftCardManage = () => {
// sortOrder: 'desc' as 'asc' | 'desc'
// })
- // 获取礼品卡状态过滤条件
+ // 获取水票状态过滤条件
const getStatusFilter = () => {
switch (String(activeTab)) {
case '0': // 未使用
@@ -52,7 +52,7 @@ const GiftCardManage = () => {
}
}
- // 根据状态过滤条件加载礼品卡
+ // 根据状态过滤条件加载水票
const loadGiftsByStatus = async (statusFilter: any) => {
setLoading(true)
try {
@@ -72,9 +72,9 @@ const GiftCardManage = () => {
setHasMore(false)
}
} catch (error) {
- console.error('获取礼品卡失败:', error)
+ console.error('获取水票失败:', error)
Taro.showToast({
- title: '获取礼品卡失败',
+ title: '获取水票失败',
icon: 'error'
})
} finally {
@@ -125,9 +125,9 @@ const GiftCardManage = () => {
// setTotal(0)
}
} catch (error) {
- console.error('获取礼品卡失败:', error)
+ console.error('获取水票失败:', error)
Taro.showToast({
- title: '获取礼品卡失败',
+ title: '获取水票失败',
icon: 'error'
});
} finally {
@@ -162,11 +162,11 @@ const GiftCardManage = () => {
loadGiftsByStatus(statusFilter)
}
- // 转换礼品卡数据为GiftCard组件所需格式
+ // 转换水票数据为GiftCard组件所需格式
const transformGiftData = (gift: ShopGift): GiftCardProps => {
return {
id: gift.id || 0,
- name: gift.name || '礼品卡', // 礼品卡名称
+ name: gift.name || '水票', // 水票名称
goodsName: gift.goodsName, // 商品名称(新增字段)
description: gift.description || gift.instructions, // 使用说明作为描述
code: gift.code,
@@ -180,23 +180,23 @@ const GiftCardManage = () => {
contactInfo: gift.contactInfo,
// 添加商品信息
goodsInfo: {
- // 如果有商品名称或商品ID,说明是关联商品的礼品卡
+ // 如果有商品名称或商品ID,说明是关联商品的水票
...((gift.goodsName || gift.goodsId) && {
- specification: `礼品卡面值:¥${gift.faceValue}`,
+ specification: `水票面值:¥${gift.faceValue}`,
category: getTypeText(gift.type),
tags: [
getTypeText(gift.type),
gift.status === 0 ? '未使用' : gift.status === 1 ? '已使用' : '失效',
- ...(gift.goodsName ? ['商品礼品卡'] : [])
+ ...(gift.goodsName ? ['商品水票'] : [])
].filter(Boolean),
instructions: gift.instructions ? [gift.instructions] : [
'请在有效期内使用',
'出示兑换码即可使用',
'不可兑换现金',
- ...(gift.goodsName ? ['此礼品卡关联具体商品'] : [])
+ ...(gift.goodsName ? ['此水票关联具体商品'] : [])
],
notices: [
- '礼品卡一经使用不可退换',
+ '水票一经使用不可退换',
'请妥善保管兑换码',
'如有疑问请联系客服',
...(gift.goodsName ? ['商品以实际为准'] : [])
@@ -213,34 +213,34 @@ const GiftCardManage = () => {
}
}
- // 获取礼品卡类型文本
+ // 获取水票类型文本
const getTypeText = (type?: number): string => {
switch (type) {
- case 10: return '实物礼品卡'
- case 20: return '虚拟礼品卡'
- case 30: return '服务礼品卡'
- default: return '礼品卡'
+ case 10: return '实物水票'
+ case 20: return '虚拟水票'
+ case 30: return '服务水票'
+ default: return '水票'
}
}
- // 根据礼品卡类型获取主题色
+ // 根据水票类型获取主题色
const getThemeByType = (type?: number): 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple' => {
switch (type) {
- case 10: return 'gold' // 实物礼品卡 - 金色
- case 20: return 'blue' // 虚拟礼品卡 - 蓝色
- case 30: return 'green' // 服务礼品卡 - 绿色
+ case 10: return 'gold' // 实物水票 - 金色
+ case 20: return 'blue' // 虚拟水票 - 蓝色
+ case 30: return 'green' // 服务水票 - 绿色
default: return 'purple' // 默认使用紫色主题,更美观
}
}
- // 使用礼品卡
+ // 使用水票
const handleUseGift = (gift: ShopGift) => {
Taro.showModal({
- title: '使用礼品卡',
+ title: '使用水票',
content: `确定要使用"${gift.name}"吗?`,
success: (res) => {
if (res.confirm) {
- // 跳转到礼品卡使用页面
+ // 跳转到水票使用页面
Taro.navigateTo({
url: `/user/gift/use?id=${gift.id}`
})
@@ -249,35 +249,35 @@ const GiftCardManage = () => {
})
}
- // 礼品卡点击事件
+ // 水票点击事件
const handleGiftClick = (gift: GiftCardProps, index: number) => {
console.log(gift.code)
const originalGift = list[index]
if (originalGift) {
- // 显示礼品卡详情
+ // 显示水票详情
handleGiftDetail(originalGift)
}
}
- // 显示礼品卡详情
+ // 显示水票详情
const handleGiftDetail = (gift: ShopGift) => {
- // 跳转到礼品卡详情页
+ // 跳转到水票详情页
Taro.navigateTo({
url: `/user/gift/detail?id=${gift.id}`
})
}
- // 加载礼品卡统计数据
+ // 加载水票统计数据
// const loadGiftStats = async () => {
// try {
- // // 并行获取各状态的礼品卡数量
+ // // 并行获取各状态的水票数量
// const [availableRes, usedRes, expiredRes] = await Promise.all([
// getUserGifts({ page: 1, limit: 1, useStatus: 0 }),
// getUserGifts({ page: 1, limit: 1, useStatus: 1 }),
// getUserGifts({ page: 1, limit: 1, useStatus: 2 })
// ])
//
- // // 计算总价值(仅可用礼品卡)
+ // // 计算总价值(仅可用水票)
// const availableGifts = await getUserGifts({ page: 1, limit: 100, useStatus: 0 })
// const totalValue = availableGifts?.list?.reduce((sum, gift) => {
// return sum + parseFloat(gift.faceValue || '0')
@@ -290,7 +290,7 @@ const GiftCardManage = () => {
// totalValue
// })
// } catch (error) {
- // console.error('获取礼品卡统计失败:', error)
+ // console.error('获取水票统计失败:', error)
// }
// }
@@ -300,21 +300,21 @@ const GiftCardManage = () => {
// available: '0',
// used: '1',
// expired: '2',
- // total: '0' // 总价值点击跳转到可用礼品卡
+ // total: '0' // 总价值点击跳转到可用水票
// }
// if (tabMap[type]) {
// handleTabChange(tabMap[type])
// }
// }
- // 兑换礼品卡
+ // 兑换水票
const handleRedeemGift = () => {
Taro.navigateTo({
url: '/user/gift/redeem'
})
}
- // 扫码兑换礼品卡
+ // 扫码兑换水票
const handleScanRedeem = () => {
Taro.scanCode({
success: (res) => {
@@ -377,14 +377,14 @@ const GiftCardManage = () => {
>
扫码
- }
- onClick={() => setShowGuide(true)}
- >
- 帮助
-
+ {/*}*/}
+ {/* onClick={() => setShowGuide(true)}*/}
+ {/*>*/}
+ {/* 帮助*/}
+ {/**/}
@@ -400,7 +400,7 @@ const GiftCardManage = () => {
- {/* 礼品卡列表 */}
+ {/* 水票列表 */}
{
@@ -450,14 +450,14 @@ const GiftCardManage = () => {
- 暂无未使用礼品卡
+ 暂无未使用水票
- 兑换礼品卡
+ 兑换水票
{
const transformGiftData = (gift: ShopGift) => {
return {
id: gift.id || 0,
- name: gift.name || '礼品卡',
+ name: gift.name || '水票',
description: gift.description,
code: gift.code,
goodsImage: gift.goodsImage,
diff --git a/src/user/gift/use.tsx b/src/user/gift/use.tsx
index 6cbc976..2aec304 100644
--- a/src/user/gift/use.tsx
+++ b/src/user/gift/use.tsx
@@ -121,7 +121,7 @@ const GiftCardUse = () => {
const transformGiftData = (gift: ShopGift) => {
return {
id: gift.id || 0,
- name: gift.name || '礼品卡',
+ name: gift.name || '水票',
description: gift.description,
code: gift.code,
goodsImage: gift.goodsImage,
diff --git a/src/user/order/components/OrderList.tsx b/src/user/order/components/OrderList.tsx
index 4ec73fe..485a328 100644
--- a/src/user/order/components/OrderList.tsx
+++ b/src/user/order/components/OrderList.tsx
@@ -1,17 +1,21 @@
-import {Avatar, Cell, Space, Empty, Tabs, Button, TabPane, Image, Dialog} from '@nutui/nutui-react-taro'
-import {useEffect, useState, useCallback, CSSProperties} from "react";
+import {Avatar, Cell, Space, Empty, Tabs, Button, TabPane, Image, Dialog, PullToRefresh, InfiniteLoading} from '@nutui/nutui-react-taro'
+import {useEffect, useState, useCallback, useRef, CSSProperties} from "react";
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro';
-import {InfiniteLoading} from '@nutui/nutui-react-taro'
import dayjs from "dayjs";
-import {pageShopOrder, updateShopOrder, createOrder} from "@/api/shop/shopOrder";
-import {ShopOrder, ShopOrderParam} from "@/api/shop/shopOrder/model";
+import {
+ pageShopOrder,
+ updateShopOrder,
+ createOrder,
+ getShopOrder,
+ prepayShopOrder
+} from "@/api/shop/shopOrder";
+import {OrderCreateRequest, ShopOrder, ShopOrderParam} from "@/api/shop/shopOrder/model";
import {listShopOrderGoods} from "@/api/shop/shopOrderGoods";
-import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
import {copyText} from "@/utils/common";
import PaymentCountdown from "@/components/PaymentCountdown";
import {PaymentType} from "@/utils/payment";
-import {goTo} from "@/utils/navigation";
+import {ErrorType, RequestError} from "@/utils/request";
// 判断订单是否支付已过期
const isPaymentExpired = (createTime: string, timeoutHours: number = 24): boolean => {
@@ -22,6 +26,17 @@ const isPaymentExpired = (createTime: string, timeoutHours: number = 24): boolea
return now.isAfter(expireTime);
};
+// 申请退款:支付成功后仅允许在指定时间窗内发起(前端展示层限制,后端仍应校验)
+const isWithinRefundWindow = (payTime?: string, windowMinutes: number = 60): boolean => {
+ if (!payTime) return false;
+ const raw = String(payTime).trim();
+ const t = /^\d+$/.test(raw)
+ ? dayjs(Number(raw) < 1e12 ? Number(raw) * 1000 : Number(raw)) // 兼容秒/毫秒时间戳
+ : dayjs(raw);
+ if (!t.isValid()) return false;
+ return dayjs().diff(t, 'minute') <= windowMinutes;
+};
+
const getInfiniteUlStyle = (showSearch: boolean = false): CSSProperties => ({
marginTop: showSearch ? '0' : '0', // 如果显示搜索框,增加更多的上边距
height: showSearch ? '75vh' : '84vh', // 相应调整高度
@@ -78,22 +93,30 @@ const tabs = [
}
]
-// 扩展订单接口,包含商品信息
-interface OrderWithGoods extends ShopOrder {
- orderGoods?: ShopOrderGoods[];
-}
-
interface OrderListProps {
onReload?: () => void;
searchParams?: ShopOrderParam;
showSearch?: boolean;
onSearchParamsChange?: (params: ShopOrderParam) => void; // 新增:通知父组件参数变化
+ // 订单视图模式:用户/门店/骑手
+ mode?: 'user' | 'store' | 'rider';
+ // 固定过滤条件(例如 storeId / riderId),会合并到每次请求里
+ baseParams?: ShopOrderParam;
+ // 只读模式:隐藏“支付/取消/确认收货/退款”等用户操作按钮
+ readOnly?: boolean;
+ // 是否自动取消“支付已过期”的待支付订单(仅 user 模式生效)
+ autoCancelExpired?: boolean;
+ // 支付超时时间(小时),默认 24 小时
+ paymentTimeoutHours?: number;
}
function OrderList(props: OrderListProps) {
- const [list, setList] = useState([])
- const [page, setPage] = useState(1)
+ const [list, setList] = useState([])
+ const pageRef = useRef(1)
const [hasMore, setHasMore] = useState(true)
+ const [payingOrderId, setPayingOrderId] = useState(null)
+ const autoCanceledOrderIdsRef = useRef>(new Set())
+ const autoCancelRunningRef = useRef(false)
// 根据传入的statusFilter设置初始tab索引
const getInitialTabIndex = () => {
if (props.searchParams?.statusFilter !== undefined) {
@@ -113,61 +136,107 @@ function OrderList(props: OrderListProps) {
const [orderToCancel, setOrderToCancel] = useState(null)
const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
const [orderToConfirmReceive, setOrderToConfirmReceive] = useState(null)
+ const isReadOnly = props.readOnly || props.mode === 'store' || props.mode === 'rider'
+
+ const toNum = (v: any): number | undefined => {
+ if (v === null || v === undefined || v === '') return undefined;
+ const n = Number(v);
+ return Number.isFinite(n) ? n : undefined;
+ };
+
+ const parseTime = (raw: any): dayjs.Dayjs | null => {
+ const text = String(raw ?? '').trim();
+ if (!text) return null;
+ const t = /^\d+$/.test(text)
+ ? dayjs(Number(text) < 1e12 ? Number(text) * 1000 : Number(text))
+ : dayjs(text);
+ return t.isValid() ? t : null;
+ };
+
+ const isOrderPaymentExpiredSafe = (order: ShopOrder, timeoutHours: number) => {
+ if (order.payStatus) return false;
+ if (toNum(order.orderStatus) === 2) return false;
+
+ const expiration = parseTime(order.expirationTime);
+ if (expiration) return dayjs().isAfter(expiration);
+
+ if (order.createTime) return isPaymentExpired(order.createTime, timeoutHours);
+ return false;
+ };
+
+ // “已完成”应以订单状态为准;不要用商品ID等字段推断完成态,否则会造成 Tab(待发货/待收货) 与状态文案不同步
+ const isOrderCompleted = (order: ShopOrder) => toNum(order.orderStatus) === 1;
// 获取订单状态文本
const getOrderStatusText = (order: ShopOrder) => {
+ const orderStatus = toNum(order.orderStatus);
+ const deliveryStatus = toNum(order.deliveryStatus);
// 优先检查订单状态
- if (order.orderStatus === 2) return '已取消';
- if (order.orderStatus === 4) return '退款申请中';
- if (order.orderStatus === 5) return '退款被拒绝';
- if (order.orderStatus === 6) return '退款成功';
- if (order.orderStatus === 7) return '客户端申请退款';
+ if (orderStatus === 2) return '已取消';
+ if (orderStatus === 4) return '退款申请中';
+ if (orderStatus === 5) return '退款被拒绝';
+ if (orderStatus === 6) return '退款成功';
+ if (orderStatus === 7) return '客户端申请退款';
+ if (isOrderCompleted(order)) return '已完成';
// 检查支付状态 (payStatus为boolean类型,false/0表示未付款,true/1表示已付款)
if (!order.payStatus) return '等待买家付款';
// 已付款后检查发货状态
- if (order.deliveryStatus === 10) return '待发货';
- if (order.deliveryStatus === 20) return '待收货';
- if (order.deliveryStatus === 30) return '已完成';
+ if (deliveryStatus === 10) return '待发货';
+ if (deliveryStatus === 20) {
+ // 若订单没有配送员,沿用原“待收货”语义
+ if (!order.riderId || Number(order.riderId) === 0) return '待收货';
+ // 配送员确认送达后(sendEndTime有值),才进入“待确认收货”
+ if (order.sendEndTime && !isOrderCompleted(order)) return '待确认收货';
+ return '配送中';
+ }
+ if (deliveryStatus === 30) return '部分发货';
- // 最后检查订单完成状态
- if (order.orderStatus === 1) return '已完成';
- if (order.orderStatus === 0) return '未使用';
+ if (orderStatus === 0) return '未使用';
return '未知状态';
};
// 获取订单状态颜色
const getOrderStatusColor = (order: ShopOrder) => {
+ const orderStatus = toNum(order.orderStatus);
+ const deliveryStatus = toNum(order.deliveryStatus);
// 优先检查订单状态
- if (order.orderStatus === 2) return 'text-gray-500'; // 已取消
- if (order.orderStatus === 4) return 'text-orange-500'; // 退款申请中
- if (order.orderStatus === 5) return 'text-red-500'; // 退款被拒绝
- if (order.orderStatus === 6) return 'text-green-500'; // 退款成功
- if (order.orderStatus === 7) return 'text-orange-500'; // 客户端申请退款
+ if (orderStatus === 2) return 'text-gray-500'; // 已取消
+ if (orderStatus === 4) return 'text-orange-500'; // 退款申请中
+ if (orderStatus === 5) return 'text-red-500'; // 退款被拒绝
+ if (orderStatus === 6) return 'text-green-500'; // 退款成功
+ if (orderStatus === 7) return 'text-orange-500'; // 客户端申请退款
+ if (isOrderCompleted(order)) return 'text-green-600'; // 已完成
// 检查支付状态
if (!order.payStatus) return 'text-orange-500'; // 等待买家付款
// 已付款后检查发货状态
- if (order.deliveryStatus === 10) return 'text-blue-500'; // 待发货
- if (order.deliveryStatus === 20) return 'text-purple-500'; // 待收货
- if (order.deliveryStatus === 30) return 'text-green-500'; // 已收货
+ if (deliveryStatus === 10) return 'text-blue-500'; // 待发货
+ if (deliveryStatus === 20) {
+ if (!order.riderId || Number(order.riderId) === 0) return 'text-purple-500'; // 待收货
+ if (order.sendEndTime && !isOrderCompleted(order)) return 'text-purple-500'; // 待确认收货
+ return 'text-blue-500'; // 配送中
+ }
+ if (deliveryStatus === 30) return 'text-blue-500'; // 部分发货
- // 最后检查订单完成状态
- if (order.orderStatus === 1) return 'text-green-600'; // 已完成
- if (order.orderStatus === 0) return 'text-gray-500'; // 未使用
+ if (orderStatus === 0) return 'text-gray-500'; // 未使用
return 'text-gray-600'; // 默认颜色
};
// 使用后端统一的 statusFilter 进行筛选
const getOrderStatusParams = (index: string | number) => {
- let params: ShopOrderParam = {};
- // 添加用户ID过滤
- params.userId = Taro.getStorageSync('UserId');
+ let params: ShopOrderParam = {
+ ...(props.baseParams || {})
+ };
+ // 默认是用户视图:添加 userId 过滤;门店/骑手视图由 baseParams 控制
+ if (!props.mode || props.mode === 'user') {
+ params.userId = Taro.getStorageSync('UserId');
+ }
// 获取当前tab的statusFilter配置
const currentTab = tabs.find(tab => tab.index === Number(index));
@@ -183,17 +252,19 @@ function OrderList(props: OrderListProps) {
const reload = useCallback(async (resetPage = false, targetPage?: number) => {
setLoading(true);
setError(null); // 清除之前的错误
- const currentPage = resetPage ? 1 : (targetPage || page);
+ const currentPage = resetPage ? 1 : (targetPage || pageRef.current);
const statusParams = getOrderStatusParams(tapIndex);
// 合并搜索条件,tab的statusFilter优先级更高
const searchConditions: any = {
page: currentPage,
- userId: statusParams.userId, // 用户ID
+ ...statusParams,
...props.searchParams, // 搜索关键词等其他条件
};
- // statusFilter总是添加到搜索条件中(包括-1表示全部)
- if (statusParams.statusFilter !== undefined) {
+ // Tabs 的 statusFilter 优先级最高;全部(-1)时不传该参数(后端按“无筛选”处理)
+ if (statusParams.statusFilter === undefined || statusParams.statusFilter === -1) {
+ delete searchConditions.statusFilter;
+ } else {
searchConditions.statusFilter = statusParams.statusFilter;
}
console.log('订单筛选条件:', {
@@ -203,52 +274,86 @@ function OrderList(props: OrderListProps) {
finalStatusFilter: searchConditions.statusFilter
});
- try {
- const res = await pageShopOrder(searchConditions);
- let newList: OrderWithGoods[];
+ try {
+ const timeoutHours = typeof props.paymentTimeoutHours === 'number' ? props.paymentTimeoutHours : 24;
+ const canAutoCancelExpired =
+ !!props.autoCancelExpired &&
+ (!props.mode || props.mode === 'user') &&
+ !props.readOnly;
+ const isPendingPayList = statusParams.statusFilter === 0;
- if (res?.list && res?.list.length > 0) {
- // 批量获取订单商品信息,限制并发数量
- const batchSize = 3; // 限制并发数量为3
- const ordersWithGoods: OrderWithGoods[] = [];
+ const fetchOrders = async () => pageShopOrder(searchConditions);
- for (let i = 0; i < res.list.length; i += batchSize) {
- const batch = res.list.slice(i, i + batchSize);
- const batchResults = await Promise.all(
- batch.map(async (order) => {
- try {
- const orderGoods = await listShopOrderGoods({orderId: order.orderId});
- return {
- ...order,
- orderGoods: orderGoods || []
- };
- } catch (error) {
- console.error('获取订单商品失败:', error);
- return {
- ...order,
- orderGoods: []
- };
- }
- })
- );
- ordersWithGoods.push(...batchResults);
+ let res = await fetchOrders();
+ let incoming = (res?.list || []) as ShopOrder[];
+ let rawIncomingLength = incoming.length;
+
+ // 自动取消“支付已过期”的待支付订单(避免用户看到一堆不可支付的过期单)
+ if (canAutoCancelExpired && incoming.length && !autoCancelRunningRef.current) {
+ const expiredToCancel = incoming
+ .filter(o => !!o?.orderId)
+ .filter(o => !autoCanceledOrderIdsRef.current.has(o.orderId as number))
+ .filter(o => isOrderPaymentExpiredSafe(o, timeoutHours));
+
+ if (expiredToCancel.length) {
+ autoCancelRunningRef.current = true;
+ const justCanceled = new Set();
+ try {
+ // 单次最多处理 20 笔,避免接口风暴
+ for (const order of expiredToCancel.slice(0, 20)) {
+ try {
+ await updateShopOrder({ orderId: order.orderId, orderStatus: 2 });
+ autoCanceledOrderIdsRef.current.add(order.orderId as number);
+ justCanceled.add(order.orderId as number);
+ } catch (e) {
+ console.warn('自动取消过期订单失败:', order?.orderId, e);
+ }
+ }
+ } finally {
+ autoCancelRunningRef.current = false;
+ }
+
+ if (justCanceled.size > 0) {
+ if (resetPage) {
+ // resetPage 时重新拉取一次,确保列表状态与服务端一致
+ res = await fetchOrders();
+ incoming = (res?.list || []) as ShopOrder[];
+ rawIncomingLength = incoming.length;
+ Taro.showToast({ title: '已自动取消过期订单', icon: 'none' });
+ } else {
+ // loadMore 时不重新拉取,避免破坏滚动;仅在本地列表中做最小同步
+ if (isPendingPayList) {
+ incoming = incoming.filter(o => !justCanceled.has(o.orderId as number));
+ } else {
+ incoming = incoming.map(o => (
+ justCanceled.has(o.orderId as number) ? { ...o, orderStatus: 2 } : o
+ ));
+ }
+ }
+ }
+ }
+ }
+
+ if (rawIncomingLength > 0) {
+ // 订单分页接口已返回 orderGoods:列表直接使用该字段
+ // 使用函数式更新避免依赖 list
+ if (incoming.length > 0) {
+ setList(prevList => (resetPage ? incoming : (prevList || []).concat(incoming)));
+ } else {
+ // 本页数据全部被自动取消过滤掉:不清空历史列表,仅保持现状
+ setList(prevList => (resetPage ? [] : prevList));
}
- // 使用函数式更新避免依赖 list
- setList(prevList => {
- const newList = resetPage ? ordersWithGoods : (prevList || []).concat(ordersWithGoods);
- return newList;
- });
-
- // 正确判断是否还有更多数据
- const hasMoreData = res.list.length >= 10; // 假设每页10条数据
+ // 正确判断是否还有更多数据(以服务端返回条数为准)
+ const hasMoreData = rawIncomingLength >= 10; // 假设每页10条数据
setHasMore(hasMoreData);
} else {
- setList(prevList => resetPage ? [] : prevList);
+ // 服务端已无更多数据
+ setList(prevList => (resetPage ? [] : prevList));
setHasMore(false);
}
- setPage(currentPage);
+ pageRef.current = currentPage;
setLoading(false);
} catch (error) {
console.error('加载订单失败:', error);
@@ -260,14 +365,14 @@ function OrderList(props: OrderListProps) {
icon: 'none'
});
}
- }, [tapIndex, page, props.searchParams]); // 移除 list 依赖
+ }, [tapIndex, props.searchParams, props.baseParams, props.mode, props.readOnly, props.autoCancelExpired, props.paymentTimeoutHours]); // 移除 list/page 依赖,避免useEffect触发循环
const reloadMore = useCallback(async () => {
if (loading || !hasMore) return; // 防止重复加载
- const nextPage = page + 1;
- setPage(nextPage);
+ const nextPage = pageRef.current + 1;
+ pageRef.current = nextPage;
await reload(false, nextPage);
- }, [loading, hasMore, page, reload]);
+ }, [loading, hasMore, reload]);
// 确认收货 - 显示确认对话框
const confirmReceive = (order: ShopOrder) => {
@@ -284,7 +389,7 @@ function OrderList(props: OrderListProps) {
await updateShopOrder({
...orderToConfirmReceive,
- deliveryStatus: 20, // 已收货
+ deliveryStatus: orderToConfirmReceive.deliveryStatus, // 10未发货 20已发货 30部分发货(收货由orderStatus控制)
orderStatus: 1 // 已完成
});
@@ -316,83 +421,20 @@ function OrderList(props: OrderListProps) {
};
// 申请退款 (待发货状态)
- const applyRefund = async (order: ShopOrder) => {
- try {
- // 更新订单状态为"退款申请中"
- await updateShopOrder({
- orderId: order.orderId,
- orderStatus: 4 // 退款申请中
- });
-
- // 更新本地状态
- setDataSource(prev => prev.map(item =>
- item.orderId === order.orderId ? {...item, orderStatus: 4} : item
- ));
-
- // 跳转到退款申请页面
- Taro.navigateTo({
- url: `/user/order/refund/index?orderId=${order.orderId}&orderNo=${order.orderNo}`
- });
- } catch (error) {
- console.error('更新订单状态失败:', error);
- Taro.showToast({
- title: '操作失败,请重试',
- icon: 'none'
- });
- }
+ const applyRefund = (order: ShopOrder) => {
+ // 跳转到退款申请页面(订单状态在选择退款原因后再更新)
+ Taro.navigateTo({
+ url: `/user/order/refund/index?orderId=${order.orderId}&orderNo=${order.orderNo}`
+ });
};
// 查看物流 (待收货状态)
- const viewLogistics = (order: ShopOrder) => {
- // 跳转到物流查询页面
- Taro.navigateTo({
- url: `/user/order/logistics/index?orderId=${order.orderId}&orderNo=${order.orderNo}&expressNo=${order.transactionId || ''}&expressCompany=SF`
- });
- };
-
- // 再次购买 (已完成状态)
- const buyAgain = (order: ShopOrder) => {
- console.log('再次购买:', order);
- goTo(`/shop/orderConfirm/index?goodsId=${order.orderGoods[0].goodsId}`)
- // Taro.showToast({
- // title: '再次购买功能开发中',
- // icon: 'none'
- // });
- };
-
- // 评价商品 (已完成状态)
- const evaluateGoods = (order: ShopOrder) => {
- // 跳转到评价页面
- Taro.navigateTo({
- url: `/user/order/evaluate/index?orderId=${order.orderId}&orderNo=${order.orderNo}`
- });
- };
-
- // 查看进度 (退款/售后状态)
- const viewProgress = (order: ShopOrder) => {
- // 根据订单状态确定售后类型
- let afterSaleType = 'refund' // 默认退款
-
- if (order.orderStatus === 4) {
- afterSaleType = 'refund' // 退款申请中
- } else if (order.orderStatus === 7) {
- afterSaleType = 'return' // 退货申请中
- }
-
- // 跳转到售后进度页面
- Taro.navigateTo({
- url: `/user/order/progress/index?orderId=${order.orderId}&orderNo=${order.orderNo}&type=${afterSaleType}`
- });
- };
-
- // 撤销申请 (退款/售后状态)
- const cancelApplication = (order: ShopOrder) => {
- console.log('撤销申请:', order);
- Taro.showToast({
- title: '撤销申请功能开发中',
- icon: 'none'
- });
- };
+ // const viewLogistics = (order: ShopOrder) => {
+ // // 跳转到物流查询页面
+ // Taro.navigateTo({
+ // url: `/user/order/logistics/index?orderId=${order.orderId}&orderNo=${order.orderNo}&expressNo=${order.transactionId || ''}&expressCompany=SF`
+ // });
+ // };
// 取消订单
const cancelOrder = (order: ShopOrder) => {
@@ -403,13 +445,23 @@ function OrderList(props: OrderListProps) {
// 确认取消订单
const handleConfirmCancel = async () => {
if (!orderToCancel) return;
+ if (!orderToCancel.orderId) {
+ Taro.showToast({
+ title: '订单信息错误',
+ icon: 'error'
+ });
+ setOrderToCancel(null);
+ setCancelDialogVisible(false);
+ return;
+ }
try {
setCancelDialogVisible(false);
// 更新订单状态为已取消,而不是删除订单
await updateShopOrder({
- ...orderToCancel,
+ // 只传最小字段,避免误取消/误走售后流程
+ orderId: orderToCancel.orderId,
orderStatus: 2 // 已取消
});
@@ -439,7 +491,7 @@ function OrderList(props: OrderListProps) {
// 立即支付
const payOrder = async (order: ShopOrder) => {
try {
- if (!order.orderId || !order.orderNo) {
+ if (!order.orderId) {
Taro.showToast({
title: '订单信息错误',
icon: 'error'
@@ -447,8 +499,40 @@ function OrderList(props: OrderListProps) {
return;
}
- // 检查订单是否已过期
- if (order.createTime && isPaymentExpired(order.createTime)) {
+ if (payingOrderId === order.orderId) {
+ return;
+ }
+ setPayingOrderId(order.orderId);
+
+ // 尽量以服务端最新状态为准,避免“已取消/已支付”但列表未刷新导致误发起支付
+ let latestOrder: ShopOrder | null = null;
+ try {
+ latestOrder = await getShopOrder(order.orderId);
+ } catch (_e) {
+ // 忽略:网络波动时继续使用列表数据兜底
+ }
+ const effectiveOrder = latestOrder ? { ...order, ...latestOrder } : order;
+
+ if (effectiveOrder.payStatus) {
+ Taro.showToast({
+ title: '订单已支付',
+ icon: 'none'
+ });
+ // 同步刷新一次,避免列表显示旧状态
+ void reload(true);
+ return;
+ }
+ if (effectiveOrder.orderStatus === 2) {
+ Taro.showToast({
+ title: '订单已取消,无法支付',
+ icon: 'error'
+ });
+ void reload(true);
+ return;
+ }
+
+ // 检查订单是否已过期(以最新 createTime 为准)
+ if (effectiveOrder.createTime && isPaymentExpired(effectiveOrder.createTime)) {
Taro.showToast({
title: '订单已过期,无法支付',
icon: 'error'
@@ -456,46 +540,69 @@ function OrderList(props: OrderListProps) {
return;
}
- // 检查订单状态
- if (order.payStatus) {
+ Taro.showLoading({title: '发起支付...'});
+
+ // 构建商品数据:优先使用订单分页接口返回的 orderGoods;缺失时再补拉一次,避免goodsItems为空导致后端拒绝/再次支付失败
+ let orderGoods = effectiveOrder.orderGoods || [];
+ if (!orderGoods.length) {
+ try {
+ orderGoods = (await listShopOrderGoods({orderId: effectiveOrder.orderId})) || [];
+ } catch (e) {
+ // 继续走下面的校验提示
+ console.error('补拉订单商品失败:', e);
+ }
+ }
+
+ const goodsItems = orderGoods
+ .filter(g => !!(g as any).goodsId || !!(g as any).itemId)
+ .map(goods => ({
+ goodsId: (goods.goodsId ?? (goods as any).itemId) as number,
+ quantity: ((goods as any).quantity ?? goods.totalNum ?? 1) as number,
+ // 若后端按SKU计算价格/库存,补齐SKU/规格信息更安全
+ skuId: (goods as any).skuId ?? (goods as any).sku_id,
+ specInfo: (goods as any).specInfo ?? (goods as any).spec
+ }));
+
+ if (!goodsItems.length) {
Taro.showToast({
- title: '订单已支付',
+ title: '订单商品信息缺失,请稍后重试',
icon: 'none'
});
return;
}
- if (order.orderStatus === 2) {
- Taro.showToast({
- title: '订单已取消,无法支付',
- icon: 'error'
+ // 优先:对“已创建但未支付”的订单走“重新发起支付”接口(不应重复创建订单)
+ // 若后端未提供该接口,则降级为重新创建订单(此时不传 orderNo,避免出现“相同订单号重复订单”)
+ let result: any;
+ let usedFallbackCreate = false;
+ try {
+ result = await prepayShopOrder({
+ orderId: effectiveOrder.orderId!,
+ payType: PaymentType.WECHAT
});
- return;
+ } catch (e) {
+ // 订单状态等业务错误:直接提示,不要降级“重新创建订单”导致产生多笔订单
+ if (e instanceof RequestError && e.type === ErrorType.BUSINESS_ERROR) {
+ throw e;
+ }
+ usedFallbackCreate = true;
+ const orderData: OrderCreateRequest = {
+ goodsItems,
+ addressId: effectiveOrder.addressId,
+ payType: PaymentType.WECHAT,
+ couponId: effectiveOrder.couponId,
+ deliveryType: effectiveOrder.deliveryType,
+ selfTakeMerchantId: effectiveOrder.selfTakeMerchantId,
+ comments: effectiveOrder.comments,
+ title: effectiveOrder.title,
+ storeId: effectiveOrder.storeId,
+ storeName: effectiveOrder.storeName,
+ riderId: effectiveOrder.riderId,
+ warehouseId: effectiveOrder.warehouseId
+ };
+ result = await createOrder(orderData);
}
- Taro.showLoading({title: '发起支付...'});
-
- // 构建商品数据
- const goodsItems = order.orderGoods?.map(goods => ({
- goodsId: goods.goodsId,
- quantity: goods.totalNum || 1
- })) || [];
-
- // 对于已存在的订单,我们需要重新发起支付
- // 构建支付请求数据,包含完整的商品信息
- const paymentData = {
- orderId: order.orderId,
- orderNo: order.orderNo,
- goodsItems: goodsItems,
- addressId: order.addressId,
- payType: PaymentType.WECHAT
- };
-
- console.log('重新支付数据:', paymentData);
-
- // 直接调用createOrder API进行重新支付
- const result = await createOrder(paymentData as any);
-
if (!result) {
throw new Error('支付发起失败');
}
@@ -506,13 +613,26 @@ function OrderList(props: OrderListProps) {
}
// 调用微信支付
- await Taro.requestPayment({
- timeStamp: result.timeStamp,
- nonceStr: result.nonceStr,
- package: result.package,
- signType: (result.signType || 'MD5') as 'MD5' | 'HMAC-SHA256',
- paySign: result.paySign,
- });
+ try {
+ await Taro.requestPayment({
+ timeStamp: result.timeStamp,
+ nonceStr: result.nonceStr,
+ package: result.package,
+ signType: (result.signType || 'MD5') as 'MD5' | 'HMAC-SHA256',
+ paySign: result.paySign,
+ });
+ } catch (payError: any) {
+ const msg: string = payError?.errMsg || payError?.message || '';
+ if (msg.includes('cancel')) {
+ // 用户主动取消,不当作“失败”强提示
+ Taro.showToast({
+ title: '已取消支付',
+ icon: 'none'
+ });
+ return;
+ }
+ throw payError;
+ }
// 支付成功
Taro.showToast({
@@ -520,6 +640,18 @@ function OrderList(props: OrderListProps) {
icon: 'success'
});
+ // 若因后端不支持“重新发起支付”而降级“重新创建订单”,则原订单会遗留为待支付,支付成功后自动将其标记为已取消,避免列表堆积
+ if (usedFallbackCreate && effectiveOrder.orderId && !effectiveOrder.payStatus && effectiveOrder.orderStatus !== 2) {
+ try {
+ await updateShopOrder({
+ orderId: effectiveOrder.orderId,
+ orderStatus: 2
+ });
+ } catch (e) {
+ console.warn('自动取消旧待支付订单失败:', e);
+ }
+ }
+
// 重新加载订单列表
void reload(true);
props.onReload?.();
@@ -533,13 +665,14 @@ function OrderList(props: OrderListProps) {
console.error('支付失败:', error);
let errorMessage = '支付失败,请重试';
- if (error.message) {
- if (error.message.includes('cancel')) {
+ const rawMsg: string = error?.errMsg || error?.message || '';
+ if (rawMsg) {
+ if (rawMsg.includes('cancel')) {
errorMessage = '用户取消支付';
- } else if (error.message.includes('余额不足')) {
+ } else if (rawMsg.includes('余额不足')) {
errorMessage = '账户余额不足';
} else {
- errorMessage = error.message;
+ errorMessage = rawMsg;
}
}
@@ -549,13 +682,13 @@ function OrderList(props: OrderListProps) {
});
} finally {
Taro.hideLoading();
+ setPayingOrderId(null);
}
};
-
useEffect(() => {
- void reload(true); // 首次加载或tab切换时重置页码
- }, [tapIndex]); // 只监听tapIndex变化,避免reload依赖循环
+ void reload(true); // 首次加载、tab切换或搜索条件变化时重置页码
+ }, [reload]);
// 监听外部statusFilter变化,同步更新tab索引
useEffect(() => {
@@ -624,203 +757,205 @@ function OrderList(props: OrderListProps) {
})
}
-
- {error ? (
-
- {error}
- reload(true)}
+ {
+ setHasMore(true)
+ await reload(true)
+ props.onReload?.()
+ }}
+ headHeight={60}
+ >
+
+ {error ? (
+
+ {error}
+ reload(true)}
+ >
+ 重新加载
+
+
+ ) : (
+ {
+
+ }}
+ onScrollToUpper={() => {
+
+ }}
+ loadingText={
+ <>
+ 加载中
+ >
+ }
+ loadMoreText={
+ list.length === 0 ? (
+
+ ) : (
+
+ 没有更多了
+
+ )
+ }
>
- 重新加载
-
-
- ) : (
- {
- }}
- onScrollToUpper={() => {
-
- }}
- loadingText={
- <>
- 加载中
- >
- }
- loadMoreText={
- list.length === 0 ? (
-
- ) : (
-
- 没有更多了
-
- )
- }
- >
-
- {/* 订单列表 */}
- {list.length > 0 && list
- ?.filter((item) => {
- // 如果是待付款标签页(tapIndex === 1),过滤掉支付已过期的订单
- if (tapIndex === 1 && !item.payStatus && item.orderStatus !== 2 && item.createTime) {
- return !isPaymentExpired(item.createTime);
- }
- return true;
- })
- ?.map((item, index) => {
- return (
- Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
-
-
-
- {
- e.stopPropagation();
- copyText(`${item.orderNo}`)
- }}>{item.orderNo}
+ {/* 订单列表 */}
+ {list.length > 0 && list
+ ?.filter((item) => {
+ const orderStatus = toNum(item.orderStatus);
+ // “待收货”不展示退款中的/已退款订单,这些订单统一放到“退货/售后”
+ if (tapIndex === 3 && (orderStatus === 4 || orderStatus === 6)) {
+ return false;
+ }
+ // “退货/售后”只展示售后相关状态
+ if (tapIndex === 5) {
+ return orderStatus === 4 || orderStatus === 5 || orderStatus === 6 || orderStatus === 7;
+ }
+ return true;
+ })
+ ?.map((item, index) => {
+ const orderStatus = toNum(item.orderStatus);
+ const deliveryStatus = toNum(item.deliveryStatus);
+ return (
+ | Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
+
+
+
+ {
+ e.stopPropagation();
+ copyText(`${item.orderNo}`)
+ }}>{item.orderNo}
+
+ {/* 右侧显示合并的状态和倒计时 */}
+
+ {!item.payStatus && orderStatus !== 2 ? (
+
+ ) : (
+ getOrderStatusText(item)
+ )}
+
- {/* 右侧显示合并的状态和倒计时 */}
-
- {!item.payStatus && item.orderStatus !== 2 ? (
-
+ {dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}
+
+ {/* 商品信息 */}
+
+ {item.orderGoods && item.orderGoods.length > 0 ? (
+ item.orderGoods.map((goods, goodsIndex) => (
+
+
+
+
+ {goods.goodsName || (goods as any).goodsTitle || (goods as any).title || item.title || '订单商品'}
+
+ {(goods.spec || (goods as any).specInfo) && (
+ 规格:{goods.spec || (goods as any).specInfo}
+ )}
+ 数量:{(goods as any).quantity ?? goods.totalNum}
+
+ x
+ ¥{goods.price || (goods as any).payPrice}
+
+ ))
) : (
- getOrderStatusText(item)
+
+
+
+ {item.title || '订单商品'}
+ {item.totalNum}件商品
+
+
)}
-
- {dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}
- {/* 商品信息 */}
-
- {item.orderGoods && item.orderGoods.length > 0 ? (
- item.orderGoods.map((goods, goodsIndex) => (
-
-
-
- {goods.goodsName}
- {goods.spec && 规格:{goods.spec}}
- 数量:{goods.totalNum}
-
- ¥{goods.price}
-
- ))
- ) : (
-
-
-
- {item.title || '订单商品'}
- {item.totalNum}件商品
-
-
- )}
-
+ 实付金额:¥{item.payPrice}
- 实付金额:¥{item.payPrice}
+ {/* 操作按钮 */}
+ {!isReadOnly && (
+
+ {/* 待付款状态:显示取消订单和立即支付 */}
+ {(!item.payStatus) && orderStatus !== 2 && (
+
+ {
+ e.stopPropagation();
+ void cancelOrder(item);
+ }}>取消
+ {(!item.createTime || !isPaymentExpired(item.createTime, 24)) && (
+ {
+ e.stopPropagation();
+ void payOrder(item);
+ }}>继续支付
+ )}
+
+ )}
- {/* 操作按钮 */}
-
- {/* 待付款状态:显示取消订单和立即支付 */}
- {(!item.payStatus) && item.orderStatus !== 2 && (
-
- {
- e.stopPropagation();
- void cancelOrder(item);
- }}>取消订单
- {
- e.stopPropagation();
- void payOrder(item);
- }}>立即支付
-
- )}
-
- {/* 待发货状态:显示申请退款 */}
- {item.payStatus && item.deliveryStatus === 10 && item.orderStatus !== 2 && item.orderStatus !== 4 && (
- {
- e.stopPropagation();
- applyRefund(item);
- }}>申请退款
- )}
-
- {/* 待收货状态:显示查看物流和确认收货 */}
- {item.deliveryStatus === 20 && item.orderStatus !== 2 && (
-
- {
- e.stopPropagation();
- viewLogistics(item);
- }}>查看物流
- {
- e.stopPropagation();
- confirmReceive(item);
- }}>确认收货
-
- )}
-
- {/* 已完成状态:显示再次购买、评价商品、申请退款 */}
- {item.orderStatus === 1 && (
-
- {
- e.stopPropagation();
- buyAgain(item);
- }}>再次购买
- {/* {*/}
- {/* e.stopPropagation();*/}
- {/* evaluateGoods(item);*/}
- {/*}}>评价商品*/}
+ {/* 待发货状态:显示申请退款 */}
+ {item.payStatus && isWithinRefundWindow(item.payTime, 60) && deliveryStatus === 10 && orderStatus !== 2 && orderStatus !== 4 && orderStatus !== 6 && orderStatus !== 7 && !isOrderCompleted(item) && (
{
e.stopPropagation();
applyRefund(item);
}}>申请退款
-
- )}
+ )}
- {/* 退款/售后状态:显示查看进度和撤销申请 */}
- {(item.orderStatus === 4 || item.orderStatus === 7) && (
-
- {/* {*/}
- {/* e.stopPropagation();*/}
- {/* viewProgress(item);*/}
- {/*}}>查看进度*/}
-
- )}
+ {/* 待收货状态:显示查看物流和确认收货 */}
+ {deliveryStatus === 20 && (!item.riderId || Number(item.riderId) === 0 || !!item.sendEndTime) && orderStatus !== 2 && orderStatus !== 6 && !isOrderCompleted(item) && (
+
+ {/* {*/}
+ {/* e.stopPropagation();*/}
+ {/* viewLogistics(item);*/}
+ {/*}}>查看物流*/}
+ {
+ e.stopPropagation();
+ confirmReceive(item);
+ }}>确认收货
+
+ )}
- {/* 退款成功状态:显示再次购买 */}
- {item.orderStatus === 6 && (
- {
- e.stopPropagation();
- buyAgain(item);
- }}>再次购买
+ {/* 退款/售后状态:显示查看进度和撤销申请 */}
+ {(orderStatus === 4 || orderStatus === 7) && (
+
+ {/* {*/}
+ {/* e.stopPropagation();*/}
+ {/* viewProgress(item);*/}
+ {/*}}>查看进度*/}
+
+ )}
+
+
)}
-
- |
- )
- })}
- |
- )}
-
+ |
+ )
+ })}
+
+ )}
+
+
{/* 取消订单确认对话框 */}
| | |