Compare commits
93 Commits
master
...
ab61aa9ee0
| Author | SHA1 | Date | |
|---|---|---|---|
| ab61aa9ee0 | |||
| 64d30e1b62 | |||
| a8eb9e11be | |||
| 338dc421db | |||
| 6f1e0a6a2b | |||
| 8b5609255a | |||
| 31d47f0a0b | |||
| 68d5848d3d | |||
| e40120138b | |||
| ef26a207b0 | |||
| 78ac461ef9 | |||
| f9dcaa9ce9 | |||
| d86cdad470 | |||
| 3d94125c5e | |||
| 63d0d64a1f | |||
| 5840bea66b | |||
| 929f173b95 | |||
| 049b2396c3 | |||
| fb06816e37 | |||
| 939d7b3ec2 | |||
| d6eb6d5e05 | |||
| 694b5e1e7a | |||
| 00d3ffaeee | |||
| e22cfe4646 | |||
| 52ef8d4199 | |||
| 93418912dc | |||
| 3535cf3a92 | |||
| b22ff991f0 | |||
| fc3b32215e | |||
| 4951c3202d | |||
| 50ac79d055 | |||
| 8d2188b928 | |||
| a1e1487d42 | |||
| 37c2f030f2 | |||
| 231723e960 | |||
| 94ed969d2d | |||
| 1ce6381248 | |||
| 7fb74e9977 | |||
| f8672dec34 | |||
| 6c83f6c082 | |||
| 5581493772 | |||
| 80653f7ac2 | |||
| 6e0a5aa1fe | |||
| 50ffd2c9da | |||
| 9e780e369c | |||
| 8751be5fb4 | |||
| f15933fc82 | |||
| f20c8b0961 | |||
| 25177d724e | |||
| 661e7574ef | |||
| 56d933ddf8 | |||
| c0954564a6 | |||
| 5bddf6e438 | |||
| 6197dbdb84 | |||
| 96d1bb959e | |||
| a1e5bf1c05 | |||
| 2a3b661478 | |||
| 6d33708601 | |||
| 8c7698a198 | |||
| 24354a38c5 | |||
| 5dc70a1c3c | |||
| 5e90c48b8b | |||
| 526c821a67 | |||
| 8679b26f74 | |||
| fcbaa970d0 | |||
| 5e36f243ef | |||
| afe8f93c32 | |||
| 174f9458e2 | |||
| f96918bf86 | |||
| a3c952d092 | |||
| cb17e48b03 | |||
| 945bf9af8d | |||
| dea40268fe | |||
| a2e34466d5 | |||
| 3d82a0f194 | |||
| f8e689e250 | |||
| e07fd4091e | |||
| 47d2eee486 | |||
| 3b98dfa150 | |||
| 3a68955f1c | |||
| b9c03be394 | |||
| 3a42eaf853 | |||
| f5c6d52b78 | |||
| 7227ec6d84 | |||
| ed5ef3fb19 | |||
| ed02db5a8d | |||
| a4938fbe31 | |||
| aff888794f | |||
| 0d6eb331c8 | |||
| 415e05cc4e | |||
| 0542b93dc7 | |||
| 0770eb1699 | |||
| 039af32fc3 |
@@ -1 +1 @@
|
|||||||
{"projectName":"trae_template-10550_mhk8"}
|
{"projectName":"trae_template-10584_mhk8"}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { API_BASE_URL } from './env'
|
import { API_BASE_URL } from './env'
|
||||||
|
|
||||||
// 租户ID - 请根据实际情况修改
|
// 租户ID - 请根据实际情况修改
|
||||||
export const TenantId = '10550';
|
export const TenantId = '10584';
|
||||||
|
// 租户名称
|
||||||
|
export const TenantName = '桂乐淘';
|
||||||
// 接口地址 - 请根据实际情况修改
|
// 接口地址 - 请根据实际情况修改
|
||||||
export const BaseUrl = API_BASE_URL;
|
export const BaseUrl = API_BASE_URL;
|
||||||
// 当前版本
|
// 当前版本
|
||||||
|
|||||||
@@ -2,20 +2,21 @@
|
|||||||
export const ENV_CONFIG = {
|
export const ENV_CONFIG = {
|
||||||
// 开发环境
|
// 开发环境
|
||||||
development: {
|
development: {
|
||||||
API_BASE_URL: 'http://127.0.0.1:9200/api',
|
// API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||||
// API_BASE_URL: 'https://cms-api.websoft.top/api',
|
API_BASE_URL: 'https://glt-api.websoft.top/api',
|
||||||
APP_NAME: '开发环境',
|
APP_NAME: '开发环境',
|
||||||
DEBUG: 'true',
|
DEBUG: 'true',
|
||||||
},
|
},
|
||||||
// 生产环境
|
// 生产环境
|
||||||
production: {
|
production: {
|
||||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
API_BASE_URL: 'https://glt-api.websoft.top/api',
|
||||||
APP_NAME: '时里院子市集',
|
APP_NAME: '桂乐淘',
|
||||||
DEBUG: 'false',
|
DEBUG: 'false',
|
||||||
},
|
},
|
||||||
// 测试环境
|
// 测试环境
|
||||||
test: {
|
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: '测试环境',
|
APP_NAME: '测试环境',
|
||||||
DEBUG: 'true',
|
DEBUG: 'true',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?:
|
|||||||
success: function () {
|
success: function () {
|
||||||
if (code) {
|
if (code) {
|
||||||
Taro.request({
|
Taro.request({
|
||||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
code,
|
code,
|
||||||
|
|||||||
41
docs/水票配送订单-后端提示词.md
Normal file
41
docs/水票配送订单-后端提示词.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 水票配送订单:后端提示词(可直接发给后端)
|
||||||
|
|
||||||
|
## 1) 订单查询(配送员端)
|
||||||
|
请在 `GET /glt/glt-ticket-order/page` 支持以下筛选,并保证权限隔离:
|
||||||
|
- `riderId`:只返回该配送员的订单(必要)
|
||||||
|
- `deliveryStatus`:10待配送、20配送中、30待客户确认、40已完成(必要)
|
||||||
|
- 排序:建议 `sendTime asc` + `createTime desc`(或给前端一个可控排序字段)
|
||||||
|
|
||||||
|
## 2) 配送流程字段(建议后端落库并回传)
|
||||||
|
订单表建议确保有以下字段(当前前端已按这些字段做流程判断/展示):
|
||||||
|
- `riderId/riderName/riderPhone`:配送员信息
|
||||||
|
- `deliveryStatus`:10/20/30/40
|
||||||
|
- `sendStartTime`:配送员点击“开始配送”的时间
|
||||||
|
- `sendEndTime`:配送员点击“确认送达”的时间
|
||||||
|
- `sendEndImg`:送达拍照留档图片 URL(可选/必填由后端策略决定)
|
||||||
|
- `receiveConfirmTime`:客户确认收货时间
|
||||||
|
- `receiveConfirmType`:10客户手动确认、20配送照片自动确认、30超时自动确认
|
||||||
|
|
||||||
|
## 3) 状态流转与校验(强烈建议在后端做)
|
||||||
|
请在更新订单时做状态机校验,避免前端绕过流程:
|
||||||
|
- `10 -> 20`:仅允许订单属于当前配送员,且未开始/未送达
|
||||||
|
- `20 -> 30`:配送员确认送达(可带 `sendEndImg`)
|
||||||
|
- `20/30 -> 40`:完成;来源可能是
|
||||||
|
- 客户手动确认(写 `receiveConfirmTime` + `receiveConfirmType=10`)
|
||||||
|
- 配送照片直接完成(写 `receiveConfirmTime` + `receiveConfirmType=20`,并要求 `sendEndImg`)
|
||||||
|
- 超时自动确认(写 `receiveConfirmTime` + `receiveConfirmType=30`,建议由定时任务执行)
|
||||||
|
|
||||||
|
## 4) 建议新增/明确的接口能力
|
||||||
|
为了避免并发抢单/越权更新,建议新增更语义化的接口(或在 update 内做等价校验):
|
||||||
|
- 接单(抢单/派单):`POST /glt/glt-ticket-order/{id}/accept`
|
||||||
|
- 后端原子校验:仅当 `riderId is null` 才能写入当前 rider 信息
|
||||||
|
- 开始配送:`POST /glt/glt-ticket-order/{id}/start`(写 `sendStartTime` + `deliveryStatus=20`)
|
||||||
|
- 确认送达:`POST /glt/glt-ticket-order/{id}/delivered`(写 `sendEndTime` + `deliveryStatus=30` + 可选 `sendEndImg`)
|
||||||
|
- 客户确认收货:`POST /glt/glt-ticket-order/{id}/confirm-receive`
|
||||||
|
- 校验:只能本人 `userId` 操作,且必须已送达
|
||||||
|
|
||||||
|
## 5) 为了“导航到收货地址/取货点”的字段补充(建议)
|
||||||
|
当前仅有 `address` 字符串,无法在小程序内 `openLocation` 精准导航;建议补充:
|
||||||
|
- 收货地址:`receiverName`、`receiverPhone`、`province/city/district/detail`、`latitude/longitude`
|
||||||
|
- 取货点(门店/仓库):`storeLatitude/storeLongitude` 或 `warehouseLatitude/warehouseLongitude`
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "template-10550",
|
"name": "template-10584",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "WebSoft Inc.",
|
"description": "WebSoft Inc.",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"miniprogramRoot": "dist/",
|
"miniprogramRoot": "dist/",
|
||||||
"projectname": "template-10550",
|
"projectname": "template-10584",
|
||||||
"description": "时里院子市集",
|
"description": "桂乐淘",
|
||||||
"appid": "wx5170f9f17a813877",
|
"appid": "wxad831ba00ad6a026",
|
||||||
"setting": {
|
"setting": {
|
||||||
"urlCheck": true,
|
"urlCheck": true,
|
||||||
"es6": false,
|
"es6": false,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"miniprogramRoot": "./",
|
"miniprogramRoot": "./",
|
||||||
"projectname": "mp-react",
|
"projectname": "mp-react",
|
||||||
"description": "时里院子市集",
|
"description": "桂乐淘",
|
||||||
"appid": "touristappid",
|
"appid": "touristappid",
|
||||||
"setting": {
|
"setting": {
|
||||||
"urlCheck": true,
|
"urlCheck": true,
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ function UserCard() {
|
|||||||
success: function () {
|
success: function () {
|
||||||
if (code) {
|
if (code) {
|
||||||
Taro.request({
|
Taro.request({
|
||||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
code,
|
code,
|
||||||
@@ -237,7 +237,7 @@ function UserCard() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={'item flex justify-center flex-col items-center'}
|
<div className={'item flex justify-center flex-col items-center'}
|
||||||
onClick={() => navTo('/user/gift/index', true)}>
|
onClick={() => navTo('/user/gift/index', true)}>
|
||||||
<span className={'text-sm text-gray-500'}>礼品卡</span>
|
<span className={'text-sm text-gray-500'}>水票</span>
|
||||||
<span className={'text-xl'}>{giftCount}</span>
|
<span className={'text-xl'}>{giftCount}</span>
|
||||||
</div>
|
</div>
|
||||||
{/*<div className={'item flex justify-center flex-col items-center'}>*/}
|
{/*<div className={'item flex justify-center flex-col items-center'}>*/}
|
||||||
|
|||||||
101
src/api/glt/gltTicketOrder/index.ts
Normal file
101
src/api/glt/gltTicketOrder/index.ts
Normal file
@@ -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<ApiResult<PageResult<GltTicketOrder>>>(
|
||||||
|
'/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<ApiResult<GltTicketOrder[]>>(
|
||||||
|
'/glt/glt-ticket-order',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加送水订单
|
||||||
|
*/
|
||||||
|
export async function addGltTicketOrder(data: GltTicketOrder) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-order',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改送水订单
|
||||||
|
*/
|
||||||
|
export async function updateGltTicketOrder(data: GltTicketOrder) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-order',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除送水订单
|
||||||
|
*/
|
||||||
|
export async function removeGltTicketOrder(id?: number) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-order/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除送水订单
|
||||||
|
*/
|
||||||
|
export async function removeBatchGltTicketOrder(data: (number | undefined)[]) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-order/batch',
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询送水订单
|
||||||
|
*/
|
||||||
|
export async function getGltTicketOrder(id: number) {
|
||||||
|
const res = await request.get<ApiResult<GltTicketOrder>>(
|
||||||
|
'/glt/glt-ticket-order/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
94
src/api/glt/gltTicketOrder/model/index.ts
Normal file
94
src/api/glt/gltTicketOrder/model/index.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
118
src/api/glt/gltTicketTemplate/index.ts
Normal file
118
src/api/glt/gltTicketTemplate/index.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import type { GltTicketTemplate, GltTicketTemplateParam } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询水票
|
||||||
|
*/
|
||||||
|
export async function pageGltTicketTemplate(params: GltTicketTemplateParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<GltTicketTemplate>>>(
|
||||||
|
'/glt/glt-ticket-template/page',
|
||||||
|
{
|
||||||
|
params
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询水票列表
|
||||||
|
*/
|
||||||
|
export async function listGltTicketTemplate(params?: GltTicketTemplateParam) {
|
||||||
|
const res = await request.get<ApiResult<GltTicketTemplate[]>>(
|
||||||
|
'/glt/glt-ticket-template',
|
||||||
|
{
|
||||||
|
params
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加水票
|
||||||
|
*/
|
||||||
|
export async function addGltTicketTemplate(data: GltTicketTemplate) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-template',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改水票
|
||||||
|
*/
|
||||||
|
export async function updateGltTicketTemplate(data: GltTicketTemplate) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-template',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除水票
|
||||||
|
*/
|
||||||
|
export async function removeGltTicketTemplate(id?: number) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-template/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除水票
|
||||||
|
*/
|
||||||
|
export async function removeBatchGltTicketTemplate(data: (number | undefined)[]) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-template/batch',
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询水票
|
||||||
|
*/
|
||||||
|
export async function getGltTicketTemplate(id: number) {
|
||||||
|
const res = await request.get<ApiResult<GltTicketTemplate>>(
|
||||||
|
'/glt/glt-ticket-template/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据商品ID查询水票模板
|
||||||
|
*/
|
||||||
|
export async function getGltTicketTemplateByGoodsId(id: number) {
|
||||||
|
const res = await request.get<ApiResult<GltTicketTemplate>>(
|
||||||
|
'/glt/glt-ticket-template/getByGoodsId/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
55
src/api/glt/gltTicketTemplate/model/index.ts
Normal file
55
src/api/glt/gltTicketTemplate/model/index.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
170
src/api/glt/gltUserTicket/index.ts
Normal file
170
src/api/glt/gltUserTicket/index.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import type { GltUserTicket, GltUserTicketParam } from './model';
|
||||||
|
|
||||||
|
function normalizeTotal(input: unknown): number {
|
||||||
|
if (typeof input === 'number' && Number.isFinite(input)) return input;
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
const n = Number(input);
|
||||||
|
if (Number.isFinite(n)) return n;
|
||||||
|
}
|
||||||
|
if (input && typeof input === 'object') {
|
||||||
|
const obj: any = input;
|
||||||
|
// Common shapes from different backends.
|
||||||
|
for (const key of ['total', 'count', 'value', 'num', 'ticketTotal', 'totalQty']) {
|
||||||
|
const v = obj?.[key];
|
||||||
|
const n = normalizeTotal(v);
|
||||||
|
if (n) return n;
|
||||||
|
}
|
||||||
|
// Sometimes nested: { data: { total: ... } } / { data: 12 }
|
||||||
|
if ('data' in obj) {
|
||||||
|
const n = normalizeTotal(obj.data);
|
||||||
|
if (n) return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询我的水票
|
||||||
|
*/
|
||||||
|
export async function pageGltUserTicket(params: GltUserTicketParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<GltUserTicket>>>(
|
||||||
|
'/glt/glt-user-ticket/page',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询我的水票列表
|
||||||
|
*/
|
||||||
|
export async function listGltUserTicket(params?: GltUserTicketParam) {
|
||||||
|
const res = await request.get<ApiResult<GltUserTicket[]>>(
|
||||||
|
'/glt/glt-user-ticket',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加我的水票
|
||||||
|
*/
|
||||||
|
export async function addGltUserTicket(data: GltUserTicket) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改我的水票
|
||||||
|
*/
|
||||||
|
export async function updateGltUserTicket(data: GltUserTicket) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除我的水票
|
||||||
|
*/
|
||||||
|
export async function removeGltUserTicket(id?: number) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除我的水票
|
||||||
|
*/
|
||||||
|
export async function removeBatchGltUserTicket(data: (number | undefined)[]) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket/batch',
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询我的水票
|
||||||
|
*/
|
||||||
|
export async function getGltUserTicket(id: number) {
|
||||||
|
const res = await request.get<ApiResult<GltUserTicket>>(
|
||||||
|
'/glt/glt-user-ticket/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的水票总数
|
||||||
|
*/
|
||||||
|
export async function getMyGltUserTicketTotal(userId?: number) {
|
||||||
|
const params = userId ? { userId } : undefined
|
||||||
|
|
||||||
|
const extract = (res: any) => {
|
||||||
|
// Some backends may return a raw number instead of ApiResult.
|
||||||
|
if (typeof res === 'number' || typeof res === 'string') return normalizeTotal(res)
|
||||||
|
if (res && typeof res === 'object' && 'code' in res) {
|
||||||
|
const apiRes = res as ApiResult<unknown>
|
||||||
|
if (apiRes.code === 0) return normalizeTotal(apiRes.data)
|
||||||
|
throw new Error(apiRes.message)
|
||||||
|
}
|
||||||
|
return normalizeTotal(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try both the configured BaseUrl host and the auth-server host.
|
||||||
|
// If the first one returns 0, keep trying; some tenants deploy GLT on a different host.
|
||||||
|
const urls = [
|
||||||
|
'/glt/glt-user-ticket/my-total'
|
||||||
|
]
|
||||||
|
|
||||||
|
let lastError: unknown
|
||||||
|
let firstTotal: number | undefined
|
||||||
|
for (const url of urls) {
|
||||||
|
try {
|
||||||
|
const res = await request.get<any>(url, params)
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('[getMyGltUserTicketTotal] response:', { url, res })
|
||||||
|
}
|
||||||
|
const total = extract(res)
|
||||||
|
if (firstTotal === undefined) firstTotal = total
|
||||||
|
if (total) return total
|
||||||
|
} catch (e) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.warn('[getMyGltUserTicketTotal] failed:', { url, error: e })
|
||||||
|
}
|
||||||
|
lastError = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstTotal !== undefined) return firstTotal
|
||||||
|
return Promise.reject(lastError instanceof Error ? lastError : new Error('获取水票总数失败'))
|
||||||
|
}
|
||||||
66
src/api/glt/gltUserTicket/model/index.ts
Normal file
66
src/api/glt/gltUserTicket/model/index.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { PageParam } from '@/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的水票
|
||||||
|
*/
|
||||||
|
export interface GltUserTicket {
|
||||||
|
//
|
||||||
|
id?: number;
|
||||||
|
// 模板ID
|
||||||
|
templateId?: number;
|
||||||
|
// 模板名称
|
||||||
|
templateName?: string;
|
||||||
|
// 商品ID
|
||||||
|
goodsId?: number;
|
||||||
|
// 订单ID
|
||||||
|
orderId?: number;
|
||||||
|
// 订单编号
|
||||||
|
orderNo?: string;
|
||||||
|
// 订单商品ID
|
||||||
|
orderGoodsId?: number;
|
||||||
|
// 总数量
|
||||||
|
totalQty?: number;
|
||||||
|
// 可用数量
|
||||||
|
availableQty?: number;
|
||||||
|
// 冻结数量
|
||||||
|
frozenQty?: number;
|
||||||
|
// 已使用数量
|
||||||
|
usedQty?: number;
|
||||||
|
// 已释放数量
|
||||||
|
releasedQty?: number;
|
||||||
|
// 用户ID
|
||||||
|
userId?: number;
|
||||||
|
// 用户昵称
|
||||||
|
nickname?: string;
|
||||||
|
// 用户头像
|
||||||
|
avatar?: string;
|
||||||
|
// 用户手机号
|
||||||
|
phone?: string;
|
||||||
|
// 排序(数字越小越靠前)
|
||||||
|
sortNumber?: number;
|
||||||
|
// 备注
|
||||||
|
comments?: string;
|
||||||
|
// 状态, 0正常, 1冻结
|
||||||
|
status?: number;
|
||||||
|
// 是否删除, 0否, 1是
|
||||||
|
deleted?: number;
|
||||||
|
// 租户id
|
||||||
|
tenantId?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 修改时间
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的水票搜索条件
|
||||||
|
*/
|
||||||
|
export interface GltUserTicketParam extends PageParam {
|
||||||
|
id?: number;
|
||||||
|
templateId?: number;
|
||||||
|
userId?: number;
|
||||||
|
phone?: string;
|
||||||
|
keywords?: string;
|
||||||
|
// 状态过滤:0正常,1冻结
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
101
src/api/glt/gltUserTicketLog/index.ts
Normal file
101
src/api/glt/gltUserTicketLog/index.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import type { GltUserTicketLog, GltUserTicketLogParam } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询消费日志
|
||||||
|
*/
|
||||||
|
export async function pageGltUserTicketLog(params: GltUserTicketLogParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<GltUserTicketLog>>>(
|
||||||
|
'/glt/glt-user-ticket-log/page',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询消费日志列表
|
||||||
|
*/
|
||||||
|
export async function listGltUserTicketLog(params?: GltUserTicketLogParam) {
|
||||||
|
const res = await request.get<ApiResult<GltUserTicketLog[]>>(
|
||||||
|
'/glt/glt-user-ticket-log',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加消费日志
|
||||||
|
*/
|
||||||
|
export async function addGltUserTicketLog(data: GltUserTicketLog) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket-log',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改消费日志
|
||||||
|
*/
|
||||||
|
export async function updateGltUserTicketLog(data: GltUserTicketLog) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket-log',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消费日志
|
||||||
|
*/
|
||||||
|
export async function removeGltUserTicketLog(id?: number) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket-log/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除消费日志
|
||||||
|
*/
|
||||||
|
export async function removeBatchGltUserTicketLog(data: (number | undefined)[]) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket-log/batch',
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询消费日志
|
||||||
|
*/
|
||||||
|
export async function getGltUserTicketLog(id: number) {
|
||||||
|
const res = await request.get<ApiResult<GltUserTicketLog>>(
|
||||||
|
'/glt/glt-user-ticket-log/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
54
src/api/glt/gltUserTicketLog/model/index.ts
Normal file
54
src/api/glt/gltUserTicketLog/model/index.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { PageParam } from '@/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消费日志
|
||||||
|
*/
|
||||||
|
export interface GltUserTicketLog {
|
||||||
|
//
|
||||||
|
id?: number;
|
||||||
|
// 用户水票ID
|
||||||
|
userTicketId?: number;
|
||||||
|
// 变更类型
|
||||||
|
changeType?: number;
|
||||||
|
// 可更改
|
||||||
|
changeAvailable?: number;
|
||||||
|
// 更改冻结状态
|
||||||
|
changeFrozen?: number;
|
||||||
|
// 已使用更改
|
||||||
|
changeUsed?: number;
|
||||||
|
// 可用后
|
||||||
|
availableAfter?: number;
|
||||||
|
// 冻结后
|
||||||
|
frozenAfter?: number;
|
||||||
|
// 使用后
|
||||||
|
usedAfter?: number;
|
||||||
|
// 订单ID
|
||||||
|
orderId?: number;
|
||||||
|
// 订单编号
|
||||||
|
orderNo?: string;
|
||||||
|
// 用户ID
|
||||||
|
userId?: number;
|
||||||
|
// 排序(数字越小越靠前)
|
||||||
|
sortNumber?: number;
|
||||||
|
// 备注
|
||||||
|
comments?: string;
|
||||||
|
// 状态, 0正常, 1冻结
|
||||||
|
status?: number;
|
||||||
|
// 是否删除, 0否, 1是
|
||||||
|
deleted?: number;
|
||||||
|
// 租户id
|
||||||
|
tenantId?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 修改时间
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消费日志搜索条件
|
||||||
|
*/
|
||||||
|
export interface GltUserTicketLogParam extends PageParam {
|
||||||
|
id?: number;
|
||||||
|
keywords?: string;
|
||||||
|
userId?: number;
|
||||||
|
}
|
||||||
105
src/api/glt/gltUserTicketRelease/index.ts
Normal file
105
src/api/glt/gltUserTicketRelease/index.ts
Normal file
@@ -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<ApiResult<PageResult<GltUserTicketRelease>>>(
|
||||||
|
'/glt/glt-user-ticket-release/page',
|
||||||
|
{
|
||||||
|
params
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询水票释放列表
|
||||||
|
*/
|
||||||
|
export async function listGltUserTicketRelease(params?: GltUserTicketReleaseParam) {
|
||||||
|
const res = await request.get<ApiResult<GltUserTicketRelease[]>>(
|
||||||
|
'/glt/glt-user-ticket-release',
|
||||||
|
{
|
||||||
|
params
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加水票释放
|
||||||
|
*/
|
||||||
|
export async function addGltUserTicketRelease(data: GltUserTicketRelease) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket-release',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改水票释放
|
||||||
|
*/
|
||||||
|
export async function updateGltUserTicketRelease(data: GltUserTicketRelease) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket-release',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除水票释放
|
||||||
|
*/
|
||||||
|
export async function removeGltUserTicketRelease(id?: number) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket-release/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除水票释放
|
||||||
|
*/
|
||||||
|
export async function removeBatchGltUserTicketRelease(data: (number | undefined)[]) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-user-ticket-release/batch',
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询水票释放
|
||||||
|
*/
|
||||||
|
export async function getGltUserTicketRelease(id: number) {
|
||||||
|
const res = await request.get<ApiResult<GltUserTicketRelease>>(
|
||||||
|
'/glt/glt-user-ticket-release/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
38
src/api/glt/gltUserTicketRelease/model/index.ts
Normal file
38
src/api/glt/gltUserTicketRelease/model/index.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { PageParam } from '@/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 水票释放
|
||||||
|
*/
|
||||||
|
export interface GltUserTicketRelease {
|
||||||
|
//
|
||||||
|
id?: string;
|
||||||
|
// 水票ID
|
||||||
|
userTicketId?: string;
|
||||||
|
// 用户ID
|
||||||
|
userId?: number;
|
||||||
|
// 周期编号
|
||||||
|
periodNo?: number;
|
||||||
|
// 释放数量
|
||||||
|
releaseQty?: number;
|
||||||
|
// 释放时间
|
||||||
|
releaseTime?: string;
|
||||||
|
// 状态
|
||||||
|
status?: number;
|
||||||
|
// 是否删除, 0否, 1是
|
||||||
|
deleted?: number;
|
||||||
|
// 租户id
|
||||||
|
tenantId?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 修改时间
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 水票释放搜索条件
|
||||||
|
*/
|
||||||
|
export interface GltUserTicketReleaseParam extends PageParam {
|
||||||
|
id?: number;
|
||||||
|
userId?: number;
|
||||||
|
keywords?: string;
|
||||||
|
}
|
||||||
@@ -31,5 +31,11 @@ export interface ShopDealerCapital {
|
|||||||
*/
|
*/
|
||||||
export interface ShopDealerCapitalParam extends PageParam {
|
export interface ShopDealerCapitalParam extends PageParam {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
// 仅查询当前分销商的收益/资金明细
|
||||||
|
userId?: number;
|
||||||
|
// 可选:按订单过滤
|
||||||
|
orderId?: number;
|
||||||
|
// 可选:资金流动类型过滤
|
||||||
|
flowType?: number;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ export interface ShopDealerOrder {
|
|||||||
id?: number;
|
id?: number;
|
||||||
// 买家用户ID
|
// 买家用户ID
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
nickname?: string;
|
||||||
|
// 订单编号(部分接口会直接返回订单号字符串)
|
||||||
|
orderNo?: string;
|
||||||
// 订单ID
|
// 订单ID
|
||||||
orderId?: number;
|
orderId?: number;
|
||||||
// 订单总金额(不含运费)
|
// 订单总金额(不含运费)
|
||||||
@@ -28,6 +31,10 @@ export interface ShopDealerOrder {
|
|||||||
isInvalid?: number;
|
isInvalid?: number;
|
||||||
// 佣金结算(0未结算 1已结算)
|
// 佣金结算(0未结算 1已结算)
|
||||||
isSettled?: number;
|
isSettled?: number;
|
||||||
|
// 佣金解冻(0未解冻 1已解冻)
|
||||||
|
isUnfreeze?: number;
|
||||||
|
// 订单状态
|
||||||
|
orderStatus?: number;
|
||||||
// 结算时间
|
// 结算时间
|
||||||
settleTime?: number;
|
settleTime?: number;
|
||||||
// 商城ID
|
// 商城ID
|
||||||
@@ -47,5 +54,7 @@ export interface ShopDealerOrderParam extends PageParam {
|
|||||||
secondUserId?: number;
|
secondUserId?: number;
|
||||||
thirdUserId?: number;
|
thirdUserId?: number;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
// 数据权限/资源ID(通常传当前登录用户ID)
|
||||||
|
resourceId?: number;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,8 +95,9 @@ export async function getShopDealerUser(userId: number) {
|
|||||||
const res = await request.get<ApiResult<ShopDealerUser>>(
|
const res = await request.get<ApiResult<ShopDealerUser>>(
|
||||||
'/shop/shop-dealer-user/' + userId
|
'/shop/shop-dealer-user/' + userId
|
||||||
);
|
);
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0) {
|
||||||
return res.data;
|
// 未注册为分销商时,后端可能返回 data=null,这里用 null 表示“没有分销商信息”
|
||||||
|
return res.data || null;
|
||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.message));
|
return Promise.reject(new Error(res.message));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,21 @@ import request from '@/utils/request';
|
|||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { ShopDealerWithdraw, ShopDealerWithdrawParam } from './model';
|
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) {
|
export async function addShopDealerWithdraw(data: ShopDealerWithdraw): Promise<ShopDealerWithdrawCreateResult> {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<any>>(
|
||||||
'/shop/shop-dealer-withdraw',
|
'/shop/shop-dealer-withdraw',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
// Some backends return `message`, while WeChat transfer flow returns `data.package_info`.
|
||||||
|
return res.data ?? res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户领取(仅当 applyStatus=20 时)- 后台返回 package_info 供小程序调起确认收款页
|
||||||
|
*/
|
||||||
|
export async function receiveShopDealerWithdraw(id: number): Promise<ShopDealerWithdrawReceiveResult> {
|
||||||
|
const res = await request.post<ApiResult<any>>(
|
||||||
|
'/shop/shop-dealer-withdraw/receive/' + id,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data ?? res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领取成功回调:前端确认收款后通知后台把状态置为 applyStatus=40
|
||||||
|
*/
|
||||||
|
export async function receiveSuccessShopDealerWithdraw(id: number) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-dealer-withdraw/receive-success/' + id,
|
||||||
|
{}
|
||||||
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
return res.message;
|
return res.message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { PageParam } from '@/api';
|
import type { PageParam } from '@/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 礼品卡
|
* 水票
|
||||||
*/
|
*/
|
||||||
export interface ShopGift {
|
export interface ShopGift {
|
||||||
// 礼品卡ID
|
// 礼品卡ID
|
||||||
|
|||||||
@@ -146,4 +146,7 @@ export interface ShopGoodsParam extends PageParam {
|
|||||||
isShow?: number;
|
isShow?: number;
|
||||||
stock?: number;
|
stock?: number;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
|
recommend?: number;
|
||||||
|
// 0上架 1下架(以实际后端约定为准)
|
||||||
|
status?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from '@/utils/request';
|
import request, { ErrorType, RequestError } from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { ShopOrder, ShopOrderParam, OrderCreateRequest } from './model';
|
import type { ShopOrder, ShopOrderParam, OrderCreateRequest } from './model';
|
||||||
|
|
||||||
@@ -113,6 +113,44 @@ export interface WxPayResult {
|
|||||||
paySign: string;
|
paySign: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单重新发起支付(对“已创建但未支付”的订单生成新的预支付参数,不应重复创建订单)
|
||||||
|
*
|
||||||
|
* 说明:不同后端版本可能暴露不同路径,这里做兼容探测;若全部失败,调用方可自行降级处理。
|
||||||
|
*/
|
||||||
|
export interface OrderPrepayRequest {
|
||||||
|
orderId: number;
|
||||||
|
payType: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepayShopOrder(data: OrderPrepayRequest) {
|
||||||
|
const urls = [
|
||||||
|
'/shop/shop-order/pay',
|
||||||
|
'/shop/shop-order/prepay',
|
||||||
|
'/shop/shop-order/repay'
|
||||||
|
];
|
||||||
|
|
||||||
|
let lastError: unknown;
|
||||||
|
let businessError: unknown;
|
||||||
|
for (const url of urls) {
|
||||||
|
try {
|
||||||
|
const res = await request.post<ApiResult<WxPayResult>>(url, data, { showError: false });
|
||||||
|
// request.ts 在 code!=0 时会直接 throw;走到这里通常都是 code===0
|
||||||
|
if (res.code === 0) return res.data;
|
||||||
|
} catch (e) {
|
||||||
|
// 若已命中“业务错误”(例如订单已取消/已支付),优先保留该错误用于向上提示;
|
||||||
|
// 不要被后续的 404/网络错误覆盖掉,避免调用方误判为“不支持该接口”而降级走创建订单。
|
||||||
|
if (!businessError && e instanceof RequestError && e.type === ErrorType.BUSINESS_ERROR) {
|
||||||
|
businessError = e;
|
||||||
|
} else {
|
||||||
|
lastError = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(businessError || lastError || new Error('发起支付失败'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建订单
|
* 创建订单
|
||||||
*/
|
*/
|
||||||
@@ -140,3 +178,18 @@ export async function repairOrder(data: ShopOrder) {
|
|||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.message));
|
return Promise.reject(new Error(res.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 申请|同意退款
|
||||||
|
*/
|
||||||
|
export async function refundShopOrder(data: ShopOrder) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-order/refund',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PageParam } from '@/api/index';
|
import 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;
|
merchantName?: string;
|
||||||
// 商户编号
|
// 商户编号
|
||||||
merchantCode?: string;
|
merchantCode?: string;
|
||||||
|
// 归属门店ID(shop_store.id)
|
||||||
|
storeId?: number;
|
||||||
|
// 归属门店名称
|
||||||
|
storeName?: string;
|
||||||
|
// 配送员用户ID(优先级派单)
|
||||||
|
riderId?: number;
|
||||||
|
// 发货仓库ID
|
||||||
|
warehouseId?: number;
|
||||||
// 使用的优惠券id
|
// 使用的优惠券id
|
||||||
couponId?: number;
|
couponId?: number;
|
||||||
// 使用的会员卡id
|
// 使用的会员卡id
|
||||||
@@ -61,6 +69,8 @@ export interface ShopOrder {
|
|||||||
sendStartTime?: string;
|
sendStartTime?: string;
|
||||||
// 配送结束时间
|
// 配送结束时间
|
||||||
sendEndTime?: string;
|
sendEndTime?: string;
|
||||||
|
// 配送员送达拍照(选填)
|
||||||
|
sendEndImg?: string;
|
||||||
// 发货店铺id
|
// 发货店铺id
|
||||||
expressMerchantId?: number;
|
expressMerchantId?: number;
|
||||||
// 发货店铺
|
// 发货店铺
|
||||||
@@ -83,6 +93,8 @@ export interface ShopOrder {
|
|||||||
totalNum?: number;
|
totalNum?: number;
|
||||||
// 教练id
|
// 教练id
|
||||||
coachId?: number;
|
coachId?: number;
|
||||||
|
// 商品ID
|
||||||
|
formId?: number;
|
||||||
// 支付的用户id
|
// 支付的用户id
|
||||||
payUserId?: number;
|
payUserId?: number;
|
||||||
// 0余额支付, 1微信支付,102微信Native,2会员卡支付,3支付宝,4现金,5POS机,6VIP月卡,7VIP年卡,8VIP次卡,9IC月卡,10IC年卡,11IC次卡,12免费,13VIP充值卡,14IC充值卡,15积分支付,16VIP季卡,17IC季卡,18代付
|
// 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;
|
hasTakeGift?: string;
|
||||||
// 订单商品项
|
// 订单商品项
|
||||||
orderGoods?: OrderGoods[];
|
orderGoods?: ShopOrderGoods[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,6 +177,14 @@ export interface OrderGoodsItem {
|
|||||||
export interface OrderCreateRequest {
|
export interface OrderCreateRequest {
|
||||||
// 商品信息列表
|
// 商品信息列表
|
||||||
goodsItems: OrderGoodsItem[];
|
goodsItems: OrderGoodsItem[];
|
||||||
|
// 归属门店ID(shop_store.id)
|
||||||
|
storeId?: number;
|
||||||
|
// 归属门店名称(可选)
|
||||||
|
storeName?: string;
|
||||||
|
// 配送员用户ID(优先级派单)
|
||||||
|
riderId?: number;
|
||||||
|
// 发货仓库ID
|
||||||
|
warehouseId?: number;
|
||||||
// 收货地址ID
|
// 收货地址ID
|
||||||
addressId?: number;
|
addressId?: number;
|
||||||
// 支付方式
|
// 支付方式
|
||||||
@@ -173,6 +193,8 @@ export interface OrderCreateRequest {
|
|||||||
couponId?: number;
|
couponId?: number;
|
||||||
// 备注
|
// 备注
|
||||||
comments?: string;
|
comments?: string;
|
||||||
|
// 配送开始时间(用于预约/配送时间)
|
||||||
|
sendStartTime?: string;
|
||||||
// 配送方式 0快递 1自提
|
// 配送方式 0快递 1自提
|
||||||
deliveryType?: number;
|
deliveryType?: number;
|
||||||
// 自提店铺ID
|
// 自提店铺ID
|
||||||
@@ -197,6 +219,14 @@ export interface OrderGoodsItem {
|
|||||||
export interface OrderCreateRequest {
|
export interface OrderCreateRequest {
|
||||||
// 商品信息列表
|
// 商品信息列表
|
||||||
goodsItems: OrderGoodsItem[];
|
goodsItems: OrderGoodsItem[];
|
||||||
|
// 归属门店ID(shop_store.id)
|
||||||
|
storeId?: number;
|
||||||
|
// 归属门店名称(可选)
|
||||||
|
storeName?: string;
|
||||||
|
// 配送员用户ID(优先级派单)
|
||||||
|
riderId?: number;
|
||||||
|
// 发货仓库ID
|
||||||
|
warehouseId?: number;
|
||||||
// 收货地址ID
|
// 收货地址ID
|
||||||
addressId?: number;
|
addressId?: number;
|
||||||
// 支付方式
|
// 支付方式
|
||||||
@@ -205,6 +235,8 @@ export interface OrderCreateRequest {
|
|||||||
couponId?: number;
|
couponId?: number;
|
||||||
// 备注
|
// 备注
|
||||||
comments?: string;
|
comments?: string;
|
||||||
|
// 配送开始时间(用于预约/配送时间)
|
||||||
|
sendStartTime?: string;
|
||||||
// 配送方式 0快递 1自提
|
// 配送方式 0快递 1自提
|
||||||
deliveryType?: number;
|
deliveryType?: number;
|
||||||
// 自提店铺ID
|
// 自提店铺ID
|
||||||
@@ -223,6 +255,12 @@ export interface ShopOrderParam extends PageParam {
|
|||||||
payType?: number;
|
payType?: number;
|
||||||
isInvoice?: boolean;
|
isInvoice?: boolean;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
// 归属门店ID(shop_store.id)
|
||||||
|
storeId?: number;
|
||||||
|
// 配送员用户ID
|
||||||
|
riderId?: number;
|
||||||
|
// 发货仓库ID
|
||||||
|
warehouseId?: number;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
deliveryStatus?: number;
|
deliveryStatus?: number;
|
||||||
statusFilter?: number;
|
statusFilter?: number;
|
||||||
|
|||||||
101
src/api/shop/shopStore/index.ts
Normal file
101
src/api/shop/shopStore/index.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import type { ShopStore, ShopStoreParam } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询门店
|
||||||
|
*/
|
||||||
|
export async function pageShopStore(params: ShopStoreParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<ShopStore>>>(
|
||||||
|
'/shop/shop-store/page',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询门店列表
|
||||||
|
*/
|
||||||
|
export async function listShopStore(params?: ShopStoreParam) {
|
||||||
|
const res = await request.get<ApiResult<ShopStore[]>>(
|
||||||
|
'/shop/shop-store',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加门店
|
||||||
|
*/
|
||||||
|
export async function addShopStore(data: ShopStore) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改门店
|
||||||
|
*/
|
||||||
|
export async function updateShopStore(data: ShopStore) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除门店
|
||||||
|
*/
|
||||||
|
export async function removeShopStore(id?: number) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除门店
|
||||||
|
*/
|
||||||
|
export async function removeBatchShopStore(data: (number | undefined)[]) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store/batch',
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询门店
|
||||||
|
*/
|
||||||
|
export async function getShopStore(id: number) {
|
||||||
|
const res = await request.get<ApiResult<ShopStore>>(
|
||||||
|
'/shop/shop-store/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
63
src/api/shop/shopStore/model/index.ts
Normal file
63
src/api/shop/shopStore/model/index.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import type { PageParam } from '@/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门店
|
||||||
|
*/
|
||||||
|
export interface ShopStore {
|
||||||
|
// 自增ID
|
||||||
|
id?: number;
|
||||||
|
// 店铺名称
|
||||||
|
name?: string;
|
||||||
|
// 门店地址
|
||||||
|
address?: string;
|
||||||
|
// 手机号码
|
||||||
|
phone?: string;
|
||||||
|
// 邮箱
|
||||||
|
email?: string;
|
||||||
|
// 门店经理
|
||||||
|
managerName?: string;
|
||||||
|
// 门店banner
|
||||||
|
shopBanner?: string;
|
||||||
|
// 所在省份
|
||||||
|
province?: string;
|
||||||
|
// 所在城市
|
||||||
|
city?: string;
|
||||||
|
// 所在辖区
|
||||||
|
region?: string;
|
||||||
|
// 经度和纬度
|
||||||
|
lngAndLat?: string;
|
||||||
|
// 位置
|
||||||
|
location?:string;
|
||||||
|
// 区域
|
||||||
|
district?: string;
|
||||||
|
// 轮廓
|
||||||
|
points?: string;
|
||||||
|
// 用户ID
|
||||||
|
userId?: number;
|
||||||
|
// 默认仓库ID(shop_warehouse.id)
|
||||||
|
warehouseId?: number;
|
||||||
|
// 默认仓库名称(可选)
|
||||||
|
warehouseName?: string;
|
||||||
|
// 状态
|
||||||
|
status?: number;
|
||||||
|
// 备注
|
||||||
|
comments?: string;
|
||||||
|
// 排序号
|
||||||
|
sortNumber?: number;
|
||||||
|
// 是否删除
|
||||||
|
isDelete?: number;
|
||||||
|
// 租户id
|
||||||
|
tenantId?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 修改时间
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门店搜索条件
|
||||||
|
*/
|
||||||
|
export interface ShopStoreParam extends PageParam {
|
||||||
|
id?: number;
|
||||||
|
keywords?: string;
|
||||||
|
}
|
||||||
101
src/api/shop/shopStoreFence/index.ts
Normal file
101
src/api/shop/shopStoreFence/index.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api/index';
|
||||||
|
import type { ShopStoreFence, ShopStoreFenceParam } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询黄家明_电子围栏
|
||||||
|
*/
|
||||||
|
export async function pageShopStoreFence(params: ShopStoreFenceParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<ShopStoreFence>>>(
|
||||||
|
'/shop/shop-store-fence/page',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询黄家明_电子围栏列表
|
||||||
|
*/
|
||||||
|
export async function listShopStoreFence(params?: ShopStoreFenceParam) {
|
||||||
|
const res = await request.get<ApiResult<ShopStoreFence[]>>(
|
||||||
|
'/shop/shop-store-fence',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加黄家明_电子围栏
|
||||||
|
*/
|
||||||
|
export async function addShopStoreFence(data: ShopStoreFence) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-fence',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改黄家明_电子围栏
|
||||||
|
*/
|
||||||
|
export async function updateShopStoreFence(data: ShopStoreFence) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-fence',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除黄家明_电子围栏
|
||||||
|
*/
|
||||||
|
export async function removeShopStoreFence(id?: number) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-fence/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除黄家明_电子围栏
|
||||||
|
*/
|
||||||
|
export async function removeBatchShopStoreFence(data: (number | undefined)[]) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-fence/batch',
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询黄家明_电子围栏
|
||||||
|
*/
|
||||||
|
export async function getShopStoreFence(id: number) {
|
||||||
|
const res = await request.get<ApiResult<ShopStoreFence>>(
|
||||||
|
'/shop/shop-store-fence/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
43
src/api/shop/shopStoreFence/model/index.ts
Normal file
43
src/api/shop/shopStoreFence/model/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import type { PageParam } from '@/api/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 黄家明_电子围栏
|
||||||
|
*/
|
||||||
|
export interface ShopStoreFence {
|
||||||
|
// 自增ID
|
||||||
|
id?: number;
|
||||||
|
// 围栏名称
|
||||||
|
name?: string;
|
||||||
|
// 类型 0圆形 1方形
|
||||||
|
type?: number;
|
||||||
|
// 定位
|
||||||
|
location?: string;
|
||||||
|
// 经度
|
||||||
|
longitude?: string;
|
||||||
|
// 纬度
|
||||||
|
latitude?: string;
|
||||||
|
// 区域
|
||||||
|
district?: string;
|
||||||
|
// 电子围栏轮廓
|
||||||
|
points?: string;
|
||||||
|
// 排序(数字越小越靠前)
|
||||||
|
sortNumber?: number;
|
||||||
|
// 备注
|
||||||
|
comments?: string;
|
||||||
|
// 状态, 0正常, 1冻结
|
||||||
|
status?: number;
|
||||||
|
// 租户id
|
||||||
|
tenantId?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 修改时间
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 黄家明_电子围栏搜索条件
|
||||||
|
*/
|
||||||
|
export interface ShopStoreFenceParam extends PageParam {
|
||||||
|
id?: number;
|
||||||
|
keywords?: string;
|
||||||
|
}
|
||||||
101
src/api/shop/shopStoreRider/index.ts
Normal file
101
src/api/shop/shopStoreRider/index.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import type { ShopStoreRider, ShopStoreRiderParam } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询配送员
|
||||||
|
*/
|
||||||
|
export async function pageShopStoreRider(params: ShopStoreRiderParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<ShopStoreRider>>>(
|
||||||
|
'/shop/shop-store-rider/page',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询配送员列表
|
||||||
|
*/
|
||||||
|
export async function listShopStoreRider(params?: ShopStoreRiderParam) {
|
||||||
|
const res = await request.get<ApiResult<ShopStoreRider[]>>(
|
||||||
|
'/shop/shop-store-rider',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加配送员
|
||||||
|
*/
|
||||||
|
export async function addShopStoreRider(data: ShopStoreRider) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-rider',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改配送员
|
||||||
|
*/
|
||||||
|
export async function updateShopStoreRider(data: ShopStoreRider) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-rider',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除配送员
|
||||||
|
*/
|
||||||
|
export async function removeShopStoreRider(id?: number) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-rider/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除配送员
|
||||||
|
*/
|
||||||
|
export async function removeBatchShopStoreRider(data: (number | undefined)[]) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-rider/batch',
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询配送员
|
||||||
|
*/
|
||||||
|
export async function getShopStoreRider(id: number) {
|
||||||
|
const res = await request.get<ApiResult<ShopStoreRider>>(
|
||||||
|
'/shop/shop-store-rider/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
71
src/api/shop/shopStoreRider/model/index.ts
Normal file
71
src/api/shop/shopStoreRider/model/index.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import type { PageParam } from '@/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配送员
|
||||||
|
*/
|
||||||
|
export interface ShopStoreRider {
|
||||||
|
// 主键ID
|
||||||
|
id?: string;
|
||||||
|
// 配送点ID(shop_dealer.id)
|
||||||
|
dealerId?: number;
|
||||||
|
// 骑手编号(可选)
|
||||||
|
riderNo?: string;
|
||||||
|
// 姓名
|
||||||
|
realName?: string;
|
||||||
|
// 手机号
|
||||||
|
mobile?: string;
|
||||||
|
// 头像
|
||||||
|
avatar?: string;
|
||||||
|
// 身份证号(可选)
|
||||||
|
idCardNo?: string;
|
||||||
|
// 状态:1启用;0禁用
|
||||||
|
status?: number;
|
||||||
|
// 接单状态:0休息/下线;1在线;2忙碌
|
||||||
|
workStatus?: number;
|
||||||
|
// 是否开启自动派单:1是;0否
|
||||||
|
autoDispatchEnabled?: number;
|
||||||
|
// 派单优先级(同小区多骑手时可用,值越大越优先)
|
||||||
|
dispatchPriority?: number;
|
||||||
|
// 最大同时配送单数(0表示不限制)
|
||||||
|
maxOnhandOrders?: number;
|
||||||
|
// 是否计算工资(提成):1计算;0不计算(如三方配送点可设0)
|
||||||
|
commissionCalcEnabled?: number;
|
||||||
|
// 水每桶提成金额(元/桶)
|
||||||
|
waterBucketUnitFee?: string;
|
||||||
|
// 其他商品提成方式:1按订单固定金额;2按订单金额比例;3按商品规则(另表)
|
||||||
|
otherGoodsCommissionType?: number;
|
||||||
|
// 其他商品提成值:固定金额(元)或比例(%)
|
||||||
|
otherGoodsCommissionValue?: string;
|
||||||
|
// 用户ID
|
||||||
|
userId?: number;
|
||||||
|
// 经度(配送员当前位置)
|
||||||
|
longitude?: string;
|
||||||
|
// 纬度(配送员当前位置)
|
||||||
|
latitude?: string;
|
||||||
|
// 备注
|
||||||
|
comments?: string;
|
||||||
|
// 排序号
|
||||||
|
sortNumber?: number;
|
||||||
|
// 是否删除
|
||||||
|
isDelete?: number;
|
||||||
|
// 租户id
|
||||||
|
tenantId?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 修改时间
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配送员搜索条件
|
||||||
|
*/
|
||||||
|
export interface ShopStoreRiderParam extends PageParam {
|
||||||
|
id?: number;
|
||||||
|
keywords?: string;
|
||||||
|
// 配送点/门店ID(后端可能用 dealerId 或 storeId)
|
||||||
|
dealerId?: number;
|
||||||
|
storeId?: number;
|
||||||
|
status?: number;
|
||||||
|
workStatus?: number;
|
||||||
|
autoDispatchEnabled?: number;
|
||||||
|
}
|
||||||
101
src/api/shop/shopStoreUser/index.ts
Normal file
101
src/api/shop/shopStoreUser/index.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import type { ShopStoreUser, ShopStoreUserParam } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询店员
|
||||||
|
*/
|
||||||
|
export async function pageShopStoreUser(params: ShopStoreUserParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<ShopStoreUser>>>(
|
||||||
|
'/shop/shop-store-user/page',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询店员列表
|
||||||
|
*/
|
||||||
|
export async function listShopStoreUser(params?: ShopStoreUserParam) {
|
||||||
|
const res = await request.get<ApiResult<ShopStoreUser[]>>(
|
||||||
|
'/shop/shop-store-user',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加店员
|
||||||
|
*/
|
||||||
|
export async function addShopStoreUser(data: ShopStoreUser) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-user',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改店员
|
||||||
|
*/
|
||||||
|
export async function updateShopStoreUser(data: ShopStoreUser) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-user',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除店员
|
||||||
|
*/
|
||||||
|
export async function removeShopStoreUser(id?: number) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-user/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除店员
|
||||||
|
*/
|
||||||
|
export async function removeBatchShopStoreUser(data: (number | undefined)[]) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-user/batch',
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询店员
|
||||||
|
*/
|
||||||
|
export async function getShopStoreUser(id: number) {
|
||||||
|
const res = await request.get<ApiResult<ShopStoreUser>>(
|
||||||
|
'/shop/shop-store-user/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
36
src/api/shop/shopStoreUser/model/index.ts
Normal file
36
src/api/shop/shopStoreUser/model/index.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { PageParam } from '@/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 店员
|
||||||
|
*/
|
||||||
|
export interface ShopStoreUser {
|
||||||
|
// 主键ID
|
||||||
|
id?: number;
|
||||||
|
// 配送点ID(shop_dealer.id)
|
||||||
|
storeId?: number;
|
||||||
|
// 用户ID
|
||||||
|
userId?: number;
|
||||||
|
// 备注
|
||||||
|
comments?: string;
|
||||||
|
// 排序号
|
||||||
|
sortNumber?: number;
|
||||||
|
// 是否删除
|
||||||
|
isDelete?: number;
|
||||||
|
// 租户id
|
||||||
|
tenantId?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 修改时间
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 店员搜索条件
|
||||||
|
*/
|
||||||
|
export interface ShopStoreUserParam extends PageParam {
|
||||||
|
id?: number;
|
||||||
|
keywords?: string;
|
||||||
|
storeId?: number;
|
||||||
|
userId?: number;
|
||||||
|
isDelete?: number;
|
||||||
|
}
|
||||||
101
src/api/shop/shopStoreWarehouse/index.ts
Normal file
101
src/api/shop/shopStoreWarehouse/index.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api/index';
|
||||||
|
import type { ShopStoreWarehouse, ShopStoreWarehouseParam } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询仓库
|
||||||
|
*/
|
||||||
|
export async function pageShopStoreWarehouse(params: ShopStoreWarehouseParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<ShopStoreWarehouse>>>(
|
||||||
|
'/shop/shop-store-warehouse/page',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询仓库列表
|
||||||
|
*/
|
||||||
|
export async function listShopStoreWarehouse(params?: ShopStoreWarehouseParam) {
|
||||||
|
const res = await request.get<ApiResult<ShopStoreWarehouse[]>>(
|
||||||
|
'/shop/shop-store-warehouse',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加仓库
|
||||||
|
*/
|
||||||
|
export async function addShopStoreWarehouse(data: ShopStoreWarehouse) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-warehouse',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改仓库
|
||||||
|
*/
|
||||||
|
export async function updateShopStoreWarehouse(data: ShopStoreWarehouse) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-warehouse',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除仓库
|
||||||
|
*/
|
||||||
|
export async function removeShopStoreWarehouse(id?: number) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-warehouse/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除仓库
|
||||||
|
*/
|
||||||
|
export async function removeBatchShopStoreWarehouse(data: (number | undefined)[]) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-store-warehouse/batch',
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询仓库
|
||||||
|
*/
|
||||||
|
export async function getShopStoreWarehouse(id: number) {
|
||||||
|
const res = await request.get<ApiResult<ShopStoreWarehouse>>(
|
||||||
|
'/shop/shop-store-warehouse/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
53
src/api/shop/shopStoreWarehouse/model/index.ts
Normal file
53
src/api/shop/shopStoreWarehouse/model/index.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { PageParam } from '@/api/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仓库
|
||||||
|
*/
|
||||||
|
export interface ShopStoreWarehouse {
|
||||||
|
// 自增ID
|
||||||
|
id?: number;
|
||||||
|
// 仓库名称
|
||||||
|
name?: string;
|
||||||
|
// 唯一标识
|
||||||
|
code?: string;
|
||||||
|
// 类型 中心仓,区域仓,门店仓
|
||||||
|
type?: string;
|
||||||
|
// 仓库地址
|
||||||
|
address?: string;
|
||||||
|
// 真实姓名
|
||||||
|
realName?: string;
|
||||||
|
// 联系电话
|
||||||
|
phone?: string;
|
||||||
|
// 所在省份
|
||||||
|
province?: string;
|
||||||
|
// 所在城市
|
||||||
|
city?: string;
|
||||||
|
// 所在辖区
|
||||||
|
region?: string;
|
||||||
|
// 经纬度
|
||||||
|
lngAndLat?: string;
|
||||||
|
// 用户ID
|
||||||
|
userId?: number;
|
||||||
|
// 状态
|
||||||
|
status?: number;
|
||||||
|
// 备注
|
||||||
|
comments?: string;
|
||||||
|
// 排序号
|
||||||
|
sortNumber?: number;
|
||||||
|
// 是否删除
|
||||||
|
isDelete?: number;
|
||||||
|
// 租户id
|
||||||
|
tenantId?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 修改时间
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仓库搜索条件
|
||||||
|
*/
|
||||||
|
export interface ShopStoreWarehouseParam extends PageParam {
|
||||||
|
id?: number;
|
||||||
|
keywords?: string;
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ export interface ShopUserAddress {
|
|||||||
tenantId?: number;
|
tenantId?: number;
|
||||||
// 注册时间
|
// 注册时间
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
|
// 更新时间
|
||||||
|
updateTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export async function uploadOssByPath(filePath: string) {
|
|||||||
let stsExpired = Taro.getStorageSync('stsExpiredAt');
|
let stsExpired = Taro.getStorageSync('stsExpiredAt');
|
||||||
if (!sts || (stsExpired && dayjs().isBefore(dayjs(stsExpired)))) {
|
if (!sts || (stsExpired && dayjs().isBefore(dayjs(stsExpired)))) {
|
||||||
// @ts-ignore
|
// @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('sts', credentials)
|
||||||
Taro.setStorageSync('stsExpiredAt', credentials.expiration)
|
Taro.setStorageSync('stsExpiredAt', credentials.expiration)
|
||||||
sts = credentials
|
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));
|
return crypto.enc.Base64.stringify(crypto.HmacSHA1(canonicalString, accessKeySecret));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export async function uploadFile() {
|
|||||||
const tempFilePath = res.tempFilePaths[0];
|
const tempFilePath = res.tempFilePaths[0];
|
||||||
// 上传图片到OSS
|
// 上传图片到OSS
|
||||||
Taro.uploadFile({
|
Taro.uploadFile({
|
||||||
url: 'https://server.websoft.top/api/oss/upload',
|
url: 'https://glt-server.websoft.top/api/oss/upload',
|
||||||
filePath: tempFilePath,
|
filePath: tempFilePath,
|
||||||
name: 'file',
|
name: 'file',
|
||||||
header: {
|
header: {
|
||||||
|
|||||||
@@ -30,3 +30,18 @@ export async function updateUserRole(data: UserRole) {
|
|||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.message));
|
return Promise.reject(new Error(res.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增用户角色
|
||||||
|
* 说明:部分后端实现为 POST 新增、PUT 修改;这里补齐 API 以便新用户无角色时可以创建默认角色。
|
||||||
|
*/
|
||||||
|
export async function addUserRole(data: UserRole) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
SERVER_API_URL + '/system/user-role',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ export interface UserOrderStats {
|
|||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户卡片统计(个人中心头部:余额/积分/优惠券/水票)
|
||||||
|
export interface UserCardStats {
|
||||||
|
balance: string
|
||||||
|
points: number
|
||||||
|
coupons: number
|
||||||
|
giftCards: number
|
||||||
|
lastUpdateTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
// 用户完整数据
|
// 用户完整数据
|
||||||
export interface UserDashboard {
|
export interface UserDashboard {
|
||||||
balance: UserBalance
|
balance: UserBalance
|
||||||
@@ -108,6 +117,17 @@ export async function getUserOrderStats() {
|
|||||||
return Promise.reject(new Error(res.message))
|
return Promise.reject(new Error(res.message))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户卡片统计(一次性返回余额/积分/可用优惠券/未使用礼品卡数量)
|
||||||
|
*/
|
||||||
|
export async function getUserCardStats() {
|
||||||
|
const res = await request.get<ApiResult<UserCardStats>>('/user/card/stats')
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户完整仪表板数据(一次性获取所有数据)
|
* 获取用户完整仪表板数据(一次性获取所有数据)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -54,10 +54,15 @@ export default {
|
|||||||
"wallet/wallet",
|
"wallet/wallet",
|
||||||
"coupon/index",
|
"coupon/index",
|
||||||
"points/points",
|
"points/points",
|
||||||
"gift/index",
|
"ticket/index",
|
||||||
"gift/redeem",
|
"ticket/use",
|
||||||
"gift/detail",
|
"ticket/orders/index",
|
||||||
|
// "gift/index",
|
||||||
|
// "gift/redeem",
|
||||||
|
// "gift/detail",
|
||||||
|
// "gift/add",
|
||||||
"store/verification",
|
"store/verification",
|
||||||
|
"store/orders/index",
|
||||||
"theme/index",
|
"theme/index",
|
||||||
"poster/poster",
|
"poster/poster",
|
||||||
"chat/conversation/index",
|
"chat/conversation/index",
|
||||||
@@ -73,6 +78,7 @@ export default {
|
|||||||
"apply/add",
|
"apply/add",
|
||||||
"withdraw/index",
|
"withdraw/index",
|
||||||
"orders/index",
|
"orders/index",
|
||||||
|
"capital/index",
|
||||||
"team/index",
|
"team/index",
|
||||||
"qrcode/index",
|
"qrcode/index",
|
||||||
"invite-stats/index",
|
"invite-stats/index",
|
||||||
@@ -90,6 +96,21 @@ export default {
|
|||||||
'comments/index',
|
'comments/index',
|
||||||
'search/index']
|
'search/index']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"root": "store",
|
||||||
|
"pages": [
|
||||||
|
"index",
|
||||||
|
"orders/index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "rider",
|
||||||
|
"pages": [
|
||||||
|
"index",
|
||||||
|
"orders/index",
|
||||||
|
"ticket/verification/index"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"root": "admin",
|
"root": "admin",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -116,12 +137,6 @@ export default {
|
|||||||
selectedIconPath: "assets/tabbar/home-active.png",
|
selectedIconPath: "assets/tabbar/home-active.png",
|
||||||
text: "首页",
|
text: "首页",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
pagePath: "pages/category/index",
|
|
||||||
iconPath: "assets/tabbar/category.png",
|
|
||||||
selectedIconPath: "assets/tabbar/category-active.png",
|
|
||||||
text: "基地生活",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
pagePath: "pages/cart/cart",
|
pagePath: "pages/cart/cart",
|
||||||
iconPath: "assets/tabbar/cart.png",
|
iconPath: "assets/tabbar/cart.png",
|
||||||
@@ -144,6 +159,9 @@ export default {
|
|||||||
permission: {
|
permission: {
|
||||||
"scope.userLocation": {
|
"scope.userLocation": {
|
||||||
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||||
|
},
|
||||||
|
"scope.writePhotosAlbum": {
|
||||||
|
"desc": "用于保存小程序码到相册,方便分享给好友"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ button[open-type="chooseAvatar"] {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
}
|
}
|
||||||
|
.cart-buy-only{
|
||||||
|
border-radius: 20px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
image {
|
image {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function Category() {
|
|||||||
|
|
||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
title: `${nav?.categoryName}_时里院子市集`,
|
title: `${nav?.categoryName}_桂乐淘`,
|
||||||
path: `/shop/category/index?id=${categoryId}`,
|
path: `/shop/category/index?id=${categoryId}`,
|
||||||
success: function () {
|
success: function () {
|
||||||
console.log('分享成功');
|
console.log('分享成功');
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {getUserInfo} from "@/api/layout";
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {getCmsArticle} from "@/api/cms/cmsArticle";
|
import {getCmsArticle} from "@/api/cms/cmsArticle";
|
||||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||||
|
import { goToRegister } from '@/utils/auth'
|
||||||
|
|
||||||
function AddCartBar() {
|
function AddCartBar() {
|
||||||
const { router } = getCurrentInstance();
|
const { router } = getCurrentInstance();
|
||||||
@@ -13,13 +14,8 @@ function AddCartBar() {
|
|||||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||||
const onPay = () => {
|
const onPay = () => {
|
||||||
if (!IsLogin) {
|
if (!IsLogin) {
|
||||||
Taro.showToast({title: `请先登录`, icon: 'error'})
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.switchTab(
|
goToRegister({ redirect: '/pages/user/user' })
|
||||||
{
|
|
||||||
url: '/pages/user/user',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}, 1000)
|
}, 1000)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export interface GiftCardProps {
|
|||||||
faceValue?: string
|
faceValue?: string
|
||||||
/** 商品原价 */
|
/** 商品原价 */
|
||||||
originalPrice?: string
|
originalPrice?: string
|
||||||
/** 礼品卡类型:10-实物礼品卡 20-虚拟礼品卡 30-服务礼品卡 */
|
/** 礼品卡类型:10-礼品劵 20-虚拟礼品卡 30-服务礼品卡 */
|
||||||
type?: number
|
type?: number
|
||||||
/** 状态:0-未使用 1-已使用 2-失效 */
|
/** 状态:0-未使用 1-已使用 2-失效 */
|
||||||
status?: number
|
status?: number
|
||||||
@@ -112,10 +112,10 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
|||||||
// 获取礼品卡类型文本
|
// 获取礼品卡类型文本
|
||||||
const getTypeText = () => {
|
const getTypeText = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 10: return '实物礼品卡'
|
case 10: return '礼品劵'
|
||||||
case 20: return '虚拟礼品卡'
|
case 20: return '虚拟礼品卡'
|
||||||
case 30: return '服务礼品卡'
|
case 30: return '服务礼品卡'
|
||||||
default: return '礼品卡'
|
default: return '水票'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const GiftCardGuide: React.FC<GiftCardGuideProps> = ({
|
|||||||
title: '礼品卡类型说明',
|
title: '礼品卡类型说明',
|
||||||
icon: <Gift size="24" className="text-purple-500" />,
|
icon: <Gift size="24" className="text-purple-500" />,
|
||||||
content: [
|
content: [
|
||||||
'🎁 实物礼品卡:需到指定地址领取商品',
|
'🎁 礼品劵:需到指定地址领取商品',
|
||||||
'💻 虚拟礼品卡:自动发放到账户余额',
|
'💻 虚拟礼品卡:自动发放到账户余额',
|
||||||
'🛎️ 服务礼品卡:联系客服预约服务',
|
'🛎️ 服务礼品卡:联系客服预约服务',
|
||||||
'⏰ 注意查看有效期,过期无法使用'
|
'⏰ 注意查看有效期,过期无法使用'
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ const GiftCardShare: React.FC<GiftCardShareProps> = ({
|
|||||||
// 获取礼品卡类型文本
|
// 获取礼品卡类型文本
|
||||||
const getTypeText = () => {
|
const getTypeText = () => {
|
||||||
switch (giftCard.type) {
|
switch (giftCard.type) {
|
||||||
case 10: return '实物礼品卡'
|
case 10: return '礼品劵'
|
||||||
case 20: return '虚拟礼品卡'
|
case 20: return '虚拟礼品卡'
|
||||||
case 30: return '服务礼品卡'
|
case 30: return '服务礼品卡'
|
||||||
default: return '礼品卡'
|
default: return '水票'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# PaymentCountdown 支付倒计时组件
|
# PaymentCountdown 支付倒计时组件
|
||||||
|
|
||||||
基于订单创建时间的支付倒计时组件,支持静态显示和实时更新两种模式。
|
基于订单过期时间(`expirationTime`)的支付倒计时组件,支持静态显示和实时更新两种模式。
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
|||||||
|
|
||||||
// 订单列表页 - 静态显示
|
// 订单列表页 - 静态显示
|
||||||
<PaymentCountdown
|
<PaymentCountdown
|
||||||
createTime={order.createTime}
|
expirationTime={order.expirationTime}
|
||||||
payStatus={order.payStatus}
|
payStatus={order.payStatus}
|
||||||
realTime={false}
|
realTime={false}
|
||||||
mode="badge"
|
mode="badge"
|
||||||
@@ -27,7 +27,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
|||||||
|
|
||||||
// 订单详情页 - 实时更新
|
// 订单详情页 - 实时更新
|
||||||
<PaymentCountdown
|
<PaymentCountdown
|
||||||
createTime={order.createTime}
|
expirationTime={order.expirationTime}
|
||||||
payStatus={order.payStatus}
|
payStatus={order.payStatus}
|
||||||
realTime={true}
|
realTime={true}
|
||||||
showSeconds={true}
|
showSeconds={true}
|
||||||
@@ -43,7 +43,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
|||||||
```tsx
|
```tsx
|
||||||
// 自定义超时时间(12小时)
|
// 自定义超时时间(12小时)
|
||||||
<PaymentCountdown
|
<PaymentCountdown
|
||||||
createTime={order.createTime}
|
expirationTime={order.expirationTime}
|
||||||
payStatus={order.payStatus}
|
payStatus={order.payStatus}
|
||||||
realTime={true}
|
realTime={true}
|
||||||
timeoutHours={12}
|
timeoutHours={12}
|
||||||
@@ -55,7 +55,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
|||||||
|
|
||||||
// 纯文本模式
|
// 纯文本模式
|
||||||
<PaymentCountdown
|
<PaymentCountdown
|
||||||
createTime={order.createTime}
|
expirationTime={order.expirationTime}
|
||||||
payStatus={order.payStatus}
|
payStatus={order.payStatus}
|
||||||
realTime={false}
|
realTime={false}
|
||||||
mode="text"
|
mode="text"
|
||||||
@@ -67,6 +67,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
|||||||
| 参数 | 类型 | 默认值 | 说明 |
|
| 参数 | 类型 | 默认值 | 说明 |
|
||||||
|------|------|--------|------|
|
|------|------|--------|------|
|
||||||
| createTime | string | - | 订单创建时间 |
|
| createTime | string | - | 订单创建时间 |
|
||||||
|
| expirationTime | string | - | 订单过期时间(推荐) |
|
||||||
| payStatus | boolean | false | 支付状态 |
|
| payStatus | boolean | false | 支付状态 |
|
||||||
| realTime | boolean | false | 是否实时更新 |
|
| realTime | boolean | false | 是否实时更新 |
|
||||||
| timeoutHours | number | 24 | 超时小时数 |
|
| timeoutHours | number | 24 | 超时小时数 |
|
||||||
@@ -102,12 +103,13 @@ import PaymentCountdown from '@/components/PaymentCountdown';
|
|||||||
import { usePaymentCountdown, formatCountdownText } from '@/hooks/usePaymentCountdown';
|
import { usePaymentCountdown, formatCountdownText } from '@/hooks/usePaymentCountdown';
|
||||||
|
|
||||||
const MyComponent = ({ order }) => {
|
const MyComponent = ({ order }) => {
|
||||||
const timeLeft = usePaymentCountdown(
|
const timeLeft = usePaymentCountdown({
|
||||||
order.createTime,
|
expirationTime: order.expirationTime,
|
||||||
order.payStatus,
|
createTime: order.createTime, // expirationTime 缺失时回退
|
||||||
true, // 实时更新
|
payStatus: order.payStatus,
|
||||||
24 // 24小时超时
|
realTime: true,
|
||||||
);
|
timeoutHours: 24
|
||||||
|
});
|
||||||
|
|
||||||
const countdownText = formatCountdownText(timeLeft, true);
|
const countdownText = formatCountdownText(timeLeft, true);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import './PaymentCountdown.scss';
|
|||||||
export interface PaymentCountdownProps {
|
export interface PaymentCountdownProps {
|
||||||
/** 订单创建时间 */
|
/** 订单创建时间 */
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
|
/** 订单过期时间(推荐直接传后端返回的 expirationTime) */
|
||||||
|
expirationTime?: string;
|
||||||
/** 支付状态 */
|
/** 支付状态 */
|
||||||
payStatus?: boolean;
|
payStatus?: boolean;
|
||||||
/** 是否实时更新(详情页用true,列表页用false) */
|
/** 是否实时更新(详情页用true,列表页用false) */
|
||||||
@@ -29,18 +31,25 @@ export interface PaymentCountdownProps {
|
|||||||
|
|
||||||
const PaymentCountdown: React.FC<PaymentCountdownProps> = ({
|
const PaymentCountdown: React.FC<PaymentCountdownProps> = ({
|
||||||
createTime,
|
createTime,
|
||||||
|
expirationTime,
|
||||||
payStatus = false,
|
payStatus = false,
|
||||||
realTime = false,
|
realTime = false,
|
||||||
timeoutHours = 1,
|
timeoutHours = 24,
|
||||||
showSeconds = false,
|
showSeconds = false,
|
||||||
className = '',
|
className = '',
|
||||||
onExpired,
|
onExpired,
|
||||||
mode = 'badge'
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const SimpleQRCodeModal: React.FC<SimpleQRCodeModalProps> = ({
|
|||||||
{qrContent ? (
|
{qrContent ? (
|
||||||
<View className={'flex flex-col justify-center'}>
|
<View className={'flex flex-col justify-center'}>
|
||||||
<img
|
<img
|
||||||
src={`https://mp-api.websoft.top/api/qr-code/create-encrypted-qr-image?size=300x300&expireMinutes=60&businessType=gift&data=${encodeURIComponent(qrContent)}`}
|
src={`https://glt-api.websoft.top/api/qr-code/create-encrypted-qr-image?size=300x300&expireMinutes=60&businessType=gift&data=${encodeURIComponent(qrContent)}`}
|
||||||
alt="二维码"
|
alt="二维码"
|
||||||
style={{width: '200px', height: '200px'}}
|
style={{width: '200px', height: '200px'}}
|
||||||
className="mx-auto"
|
className="mx-auto"
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
title: '核销成功',
|
title: '核销成功',
|
||||||
content: '是否继续扫码核销其他礼品卡?',
|
content: '是否继续扫码核销其他水票/礼品卡?',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
handleClick(); // 递归调用继续扫码
|
handleClick(); // 递归调用继续扫码
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '邀请注册',
|
navigationBarTitleText: '注册成为会员',
|
||||||
navigationBarTextStyle: 'black'
|
navigationBarTextStyle: 'black'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import {updateUser} from "@/api/system/user";
|
|||||||
import {User} from "@/api/system/user/model";
|
import {User} from "@/api/system/user/model";
|
||||||
import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite";
|
import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite";
|
||||||
import {addShopDealerUser} from "@/api/shop/shopDealerUser";
|
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 {
|
interface ChooseAvatarEvent {
|
||||||
@@ -26,7 +28,7 @@ interface InputEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AddUserAddress = () => {
|
const AddUserAddress = () => {
|
||||||
const {user, loginUser} = useUser()
|
const {user, loginUser, fetchUserInfo} = useUser()
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
const [loading, setLoading] = useState<boolean>(true)
|
||||||
const [FormData, setFormData] = useState<User>()
|
const [FormData, setFormData] = useState<User>()
|
||||||
const formRef = useRef<any>(null)
|
const formRef = useRef<any>(null)
|
||||||
@@ -59,7 +61,7 @@ const AddUserAddress = () => {
|
|||||||
setFormData(tempFormData)
|
setFormData(tempFormData)
|
||||||
|
|
||||||
Taro.uploadFile({
|
Taro.uploadFile({
|
||||||
url: 'https://server.websoft.top/api/oss/upload',
|
url: 'https://glt-server.websoft.top/api/oss/upload',
|
||||||
filePath: detail.avatarUrl,
|
filePath: detail.avatarUrl,
|
||||||
name: 'file',
|
name: 'file',
|
||||||
header: {
|
header: {
|
||||||
@@ -127,7 +129,7 @@ const AddUserAddress = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const submitSucceed = async (values: any) => {
|
const submitSucceed = async (values: User) => {
|
||||||
try {
|
try {
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
if (!values.phone && !FormData?.phone) {
|
if (!values.phone && !FormData?.phone) {
|
||||||
@@ -142,8 +144,8 @@ const AddUserAddress = () => {
|
|||||||
const nickname = values.realName || FormData?.nickname || '';
|
const nickname = values.realName || FormData?.nickname || '';
|
||||||
if (!nickname || nickname.trim() === '') {
|
if (!nickname || nickname.trim() === '') {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '请填写昵称',
|
title: '请上传头像和填写昵称',
|
||||||
icon: 'error'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -176,12 +178,27 @@ const AddUserAddress = () => {
|
|||||||
}
|
}
|
||||||
console.log(values,FormData)
|
console.log(values,FormData)
|
||||||
|
|
||||||
const roles = await listUserRole({userId: user?.userId})
|
if (!user?.userId) {
|
||||||
console.log(roles, 'roles...')
|
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({
|
await updateUser({
|
||||||
userId: user?.userId,
|
userId: user.userId,
|
||||||
nickname: values.realName || FormData?.nickname,
|
nickname: values.realName || FormData?.nickname,
|
||||||
phone: values.phone || FormData?.phone,
|
phone: values.phone || FormData?.phone,
|
||||||
avatar: values.avatar || FormData?.avatar,
|
avatar: values.avatar || FormData?.avatar,
|
||||||
@@ -189,17 +206,55 @@ const AddUserAddress = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await addShopDealerUser({
|
await addShopDealerUser({
|
||||||
userId: user?.userId,
|
userId: user.userId,
|
||||||
realName: values.realName || FormData?.nickname,
|
realName: values.realName || FormData?.nickname,
|
||||||
mobile: values.phone || FormData?.phone,
|
mobile: values.phone || FormData?.phone,
|
||||||
refereeId: values.refereeId || FormData?.refereeId
|
refereeId: Number(values.refereeId) || Number(FormData?.refereeId)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (roles.length > 0) {
|
// 通知其他页面(如“我的”页、分销中心页)刷新经销商信息
|
||||||
await updateUserRole({
|
Taro.eventCenter.trigger('dealerUser:changed')
|
||||||
...roles[0],
|
|
||||||
roleId: 1848
|
// 角色为空时这里会导致“注册成功但没有角色”,这里做一次兜底写入默认 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(() => {
|
setTimeout(() => {
|
||||||
Taro.navigateBack();
|
// “我的”是 tabBar 页面,注册完成后直接切到“我的”
|
||||||
|
Taro.switchTab({ url: '/pages/user/user' });
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -241,7 +297,7 @@ const AddUserAddress = () => {
|
|||||||
success: (loginRes) => {
|
success: (loginRes) => {
|
||||||
if (code) {
|
if (code) {
|
||||||
Taro.request({
|
Taro.request({
|
||||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
authCode: loginRes.code,
|
authCode: loginRes.code,
|
||||||
@@ -382,9 +438,9 @@ const AddUserAddress = () => {
|
|||||||
>
|
>
|
||||||
<View className={'bg-gray-100 h-3'}></View>
|
<View className={'bg-gray-100 h-3'}></View>
|
||||||
<CellGroup style={{padding: '4px 0'}}>
|
<CellGroup style={{padding: '4px 0'}}>
|
||||||
<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>
|
{/*<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>*/}
|
||||||
<Input placeholder="邀请人ID" disabled={true}/>
|
{/* <Input placeholder="邀请人ID" disabled={false}/>*/}
|
||||||
</Form.Item>
|
{/*</Form.Item>*/}
|
||||||
<Form.Item name="phone" label="手机号" initialValue={FormData?.phone} required>
|
<Form.Item name="phone" label="手机号" initialValue={FormData?.phone} required>
|
||||||
<View className="flex items-center justify-between">
|
<View className="flex items-center justify-between">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
4
src/dealer/capital/index.config.ts
Normal file
4
src/dealer/capital/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '收益明细'
|
||||||
|
})
|
||||||
|
|
||||||
2
src/dealer/capital/index.scss
Normal file
2
src/dealer/capital/index.scss
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/* Intentionally empty: styling is done via utility classes. */
|
||||||
|
|
||||||
199
src/dealer/capital/index.tsx
Normal file
199
src/dealer/capital/index.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import React, {useCallback, useEffect, useState} from 'react'
|
||||||
|
import {View, Text, ScrollView} from '@tarojs/components'
|
||||||
|
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import {pageShopDealerCapital} from '@/api/shop/shopDealerCapital'
|
||||||
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
|
import type {ShopDealerCapital} from '@/api/shop/shopDealerCapital/model'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
const DealerCapital: React.FC = () => {
|
||||||
|
const {dealerUser} = useDealerUser()
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false)
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [records, setRecords] = useState<ShopDealerCapital[]>([])
|
||||||
|
|
||||||
|
const getFlowTypeText = (flowType?: number) => {
|
||||||
|
switch (flowType) {
|
||||||
|
case 10:
|
||||||
|
return '佣金收入'
|
||||||
|
case 20:
|
||||||
|
return '提现支出'
|
||||||
|
case 30:
|
||||||
|
return '转账支出'
|
||||||
|
case 40:
|
||||||
|
return '转账收入'
|
||||||
|
default:
|
||||||
|
return '资金变动'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFlowTypeTag = (flowType?: number) => {
|
||||||
|
// 收入:success;支出:danger;其它:default
|
||||||
|
if (flowType === 10 || flowType === 40) return 'success'
|
||||||
|
if (flowType === 20 || flowType === 30) return 'danger'
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatMoney = (flowType?: number, money?: string) => {
|
||||||
|
const isIncome = flowType === 10 || flowType === 40
|
||||||
|
const isExpense = flowType === 20 || flowType === 30
|
||||||
|
const sign = isIncome ? '+' : isExpense ? '-' : ''
|
||||||
|
return `${sign}${money || '0.00'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRecords = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||||
|
if (!dealerUser?.userId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isRefresh) {
|
||||||
|
setRefreshing(true)
|
||||||
|
} else if (page === 1) {
|
||||||
|
setLoading(true)
|
||||||
|
} else {
|
||||||
|
setLoadingMore(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pageShopDealerCapital({
|
||||||
|
page,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
// 只显示与当前登录用户相关的收益明细
|
||||||
|
userId: dealerUser.userId
|
||||||
|
})
|
||||||
|
|
||||||
|
const list = result?.list || []
|
||||||
|
if (page === 1) {
|
||||||
|
setRecords(list)
|
||||||
|
} else {
|
||||||
|
setRecords(prev => [...prev, ...list])
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasMore(list.length === PAGE_SIZE)
|
||||||
|
setCurrentPage(page)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取收益明细失败:', error)
|
||||||
|
Taro.showToast({
|
||||||
|
title: '获取收益明细失败',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
setRefreshing(false)
|
||||||
|
setLoadingMore(false)
|
||||||
|
}
|
||||||
|
}, [dealerUser?.userId])
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
await fetchRecords(1, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoadMore = async () => {
|
||||||
|
if (!loadingMore && hasMore) {
|
||||||
|
await fetchRecords(currentPage + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dealerUser?.userId) {
|
||||||
|
fetchRecords(1)
|
||||||
|
}
|
||||||
|
}, [fetchRecords, dealerUser?.userId])
|
||||||
|
|
||||||
|
if (!dealerUser) {
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
|
<Loading/>
|
||||||
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="min-h-screen bg-gray-50">
|
||||||
|
<PullToRefresh
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
disabled={refreshing}
|
||||||
|
pullingText="下拉刷新"
|
||||||
|
canReleaseText="释放刷新"
|
||||||
|
refreshingText="刷新中..."
|
||||||
|
completeText="刷新完成"
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
scrollY
|
||||||
|
className="h-screen"
|
||||||
|
onScrollToLower={handleLoadMore}
|
||||||
|
lowerThreshold={50}
|
||||||
|
>
|
||||||
|
<View className="p-4">
|
||||||
|
{loading && records.length === 0 ? (
|
||||||
|
<View className="text-center py-8">
|
||||||
|
<Loading/>
|
||||||
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
|
</View>
|
||||||
|
) : records.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{records.map((item) => (
|
||||||
|
<View key={item.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||||
|
<View className="flex justify-between items-start mb-1">
|
||||||
|
<Text className="font-semibold text-gray-800">
|
||||||
|
{item.describe || '收益明细'}
|
||||||
|
</Text>
|
||||||
|
<Tag type={getFlowTypeTag(item.flowType)}>
|
||||||
|
{getFlowTypeText(item.flowType)}
|
||||||
|
</Tag>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex justify-between items-center mb-1">
|
||||||
|
<Text className="text-sm text-gray-400">
|
||||||
|
佣金收入
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
className={`text-sm font-semibold ${
|
||||||
|
item.flowType === 10 || item.flowType === 40 ? 'text-green-600' :
|
||||||
|
item.flowType === 20 || item.flowType === 30 ? 'text-red-500' :
|
||||||
|
'text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{formatMoney(item.flowType, item.money)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex justify-between items-center">
|
||||||
|
<Text className="text-sm text-gray-400">
|
||||||
|
{/*用户:{item.userId ?? '-'}*/}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-sm text-gray-400">
|
||||||
|
{item.createTime || '-'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{loadingMore && (
|
||||||
|
<View className="text-center py-4">
|
||||||
|
<Loading/>
|
||||||
|
<Text className="text-gray-500 mt-1 text-sm">加载更多...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{!hasMore && records.length > 0 && (
|
||||||
|
<View className="text-center py-4">
|
||||||
|
<Text className="text-gray-400 text-sm">没有更多数据了</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Empty description="暂无收益明细"/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</PullToRefresh>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DealerCapital
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '分销中心'
|
navigationBarTitleText: '桂乐淘分享中心'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
<View className="text-sm" style={{
|
<View className="text-sm" style={{
|
||||||
color: 'rgba(255, 255, 255, 0.8)'
|
color: 'rgba(255, 255, 255, 0.8)'
|
||||||
}}>
|
}}>
|
||||||
ID: {dealerUser.userId} | 推荐人: {dealerUser.refereeId || '无'}
|
ID: {dealerUser.userId}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className="text-right hidden">
|
<View className="text-right hidden">
|
||||||
@@ -129,10 +129,10 @@ const DealerIndex: React.FC = () => {
|
|||||||
{dealerUser && (
|
{dealerUser && (
|
||||||
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
|
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
|
||||||
<View className="mb-4">
|
<View className="mb-4">
|
||||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
<Text className="font-semibold text-gray-800">资金统计</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="grid grid-cols-3 gap-3">
|
<View className="grid grid-cols-3 gap-3">
|
||||||
<View className="text-center p-3 rounded-lg" style={{
|
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||||
background: businessGradients.money.available
|
background: businessGradients.money.available
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-lg font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
@@ -140,7 +140,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="text-center p-3 rounded-lg" style={{
|
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||||
background: businessGradients.money.frozen
|
background: businessGradients.money.frozen
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-lg font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
@@ -148,7 +148,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="text-center p-3 rounded-lg" style={{
|
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||||
background: businessGradients.money.total
|
background: businessGradients.money.total
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-lg font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
@@ -217,7 +217,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</Grid.Item>
|
</Grid.Item>
|
||||||
|
|
||||||
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
<Grid.Item text={'申请提现'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
<Purse color="#10b981" size="20"/>
|
<Purse color="#10b981" size="20"/>
|
||||||
@@ -225,7 +225,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</Grid.Item>
|
</Grid.Item>
|
||||||
|
|
||||||
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
|
<Grid.Item text={'我的团队'} onClick={() => navigateToPage('/dealer/team/index')}>
|
||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
<People color="#8b5cf6" size="20"/>
|
<People color="#8b5cf6" size="20"/>
|
||||||
@@ -233,7 +233,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</Grid.Item>
|
</Grid.Item>
|
||||||
|
|
||||||
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
|
<Grid.Item text={'实名认证'} onClick={() => navigateToPage('/user/userVerify/index')}>
|
||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
<Dongdong color="#f59e0b" size="20"/>
|
<Dongdong color="#f59e0b" size="20"/>
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ const DealerOrders: React.FC = () => {
|
|||||||
|
|
||||||
// 获取订单数据
|
// 获取订单数据
|
||||||
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||||
if (!dealerUser?.userId) return
|
// 需要当前登录用户ID(用于 resourceId 参数)
|
||||||
|
if (!dealerUser || !dealerUser.userId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isRefresh) {
|
if (isRefresh) {
|
||||||
@@ -37,14 +38,17 @@ const DealerOrders: React.FC = () => {
|
|||||||
|
|
||||||
const result = await pageShopDealerOrder({
|
const result = await pageShopDealerOrder({
|
||||||
page,
|
page,
|
||||||
limit: 10
|
limit: 10,
|
||||||
|
// 后端需要 resourceId=当前登录用户ID 才能正确过滤分销订单
|
||||||
|
resourceId: dealerUser.userId
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result?.list) {
|
if (result?.list) {
|
||||||
const newOrders = result.list.map(order => ({
|
const newOrders = result.list.map(order => ({
|
||||||
...order,
|
...order,
|
||||||
orderNo: `${order.orderId}`,
|
// 优先使用接口返回的订单号;没有则降级展示 orderId
|
||||||
customerName: `用户${order.userId}`,
|
orderNo: order.orderNo ?? (order.orderId != null ? String(order.orderId) : undefined),
|
||||||
|
customerName: `${order.nickname}${order.userId}`,
|
||||||
userCommission: order.firstMoney || '0.00'
|
userCommission: order.firstMoney || '0.00'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -90,44 +94,53 @@ const DealerOrders: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [fetchOrders])
|
}, [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 (isInvalid === 1) return '已失效'
|
||||||
|
if (isUnfreeze === 1) return '已解冻'
|
||||||
if (isSettled === 1) return '已结算'
|
if (isSettled === 1) return '已结算'
|
||||||
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 (isInvalid === 1) return 'danger'
|
||||||
if (isSettled === 1) return 'success'
|
if (isUnfreeze === 1) return 'success'
|
||||||
|
if (isSettled === 1) return 'info'
|
||||||
return 'warning'
|
return 'warning'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGoCapital = () => {
|
||||||
|
Taro.navigateTo({url: '/dealer/capital/index'})
|
||||||
|
}
|
||||||
|
|
||||||
const renderOrderItem = (order: OrderWithDetails) => (
|
const renderOrderItem = (order: OrderWithDetails) => (
|
||||||
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
<View
|
||||||
|
key={order.id}
|
||||||
|
className="bg-white rounded-lg p-4 mb-3 shadow-sm"
|
||||||
|
onClick={handleGoCapital}
|
||||||
|
>
|
||||||
<View className="flex justify-between items-start mb-1">
|
<View className="flex justify-between items-start mb-1">
|
||||||
<Text className="font-semibold text-gray-800">
|
<Text className="font-semibold text-gray-800">
|
||||||
订单号:{order.orderNo}
|
订单号:{order.orderNo || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
<Tag type={getStatusColor(order.isSettled, order.isInvalid, order.isUnfreeze,order.orderStatus)}>
|
||||||
{getStatusText(order.isSettled, order.isInvalid)}
|
{getStatusText(order.isSettled, order.isInvalid, order.isUnfreeze,order.orderStatus)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="flex justify-between items-center mb-1">
|
{/*<View className="flex justify-between items-center mb-1">*/}
|
||||||
<Text className="text-sm text-gray-400">
|
{/* <Text className="text-sm text-gray-400">*/}
|
||||||
订单金额:¥{order.orderPrice || '0.00'}
|
{/* 订单金额:¥{order.orderPrice || '0.00'}*/}
|
||||||
</Text>
|
{/* </Text>*/}
|
||||||
<Text className="text-sm text-orange-500 font-semibold">
|
{/*</View>*/}
|
||||||
我的佣金:¥{order.userCommission}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex justify-between items-center">
|
<View className="flex justify-between items-center">
|
||||||
<Text className="text-sm text-gray-400">
|
<Text className="text-sm text-gray-400">
|
||||||
客户:{order.customerName}
|
{order.createTime}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-sm text-gray-400">
|
<Text className="text-sm text-gray-400">
|
||||||
{order.createTime}
|
订单金额:¥{order.orderPrice || '0.00'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '推广二维码'
|
navigationBarTitleText: '桂乐淘分享中心',
|
||||||
|
// Enable "Share to friends" and "Share to Moments" (timeline) for this page.
|
||||||
|
enableShareAppMessage: true,
|
||||||
|
enableShareTimeline: true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react'
|
|||||||
import {View, Text, Image} from '@tarojs/components'
|
import {View, Text, Image} from '@tarojs/components'
|
||||||
import {Button, Loading} from '@nutui/nutui-react-taro'
|
import {Button, Loading} from '@nutui/nutui-react-taro'
|
||||||
import {Download, QrCode} from '@nutui/icons-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 {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import {generateInviteCode} from '@/api/invite'
|
import {generateInviteCode} from '@/api/invite'
|
||||||
// import type {InviteStats} from '@/api/invite'
|
// import type {InviteStats} from '@/api/invite'
|
||||||
@@ -10,10 +10,44 @@ import {businessGradients} from '@/styles/gradients'
|
|||||||
|
|
||||||
const DealerQrcode: React.FC = () => {
|
const DealerQrcode: React.FC = () => {
|
||||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [codeLoading, setCodeLoading] = useState<boolean>(false)
|
||||||
|
const [saving, setSaving] = useState<boolean>(false)
|
||||||
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||||
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||||
const {dealerUser} = useDealerUser()
|
const {dealerUser, loading: dealerLoading, error, refresh} = useDealerUser()
|
||||||
|
|
||||||
|
// Enable "转发给朋友" + "分享到朋友圈" items in the share panel/menu.
|
||||||
|
useEffect(() => {
|
||||||
|
// Some clients require explicit call to show both share entries.
|
||||||
|
Taro.showShareMenu({
|
||||||
|
withShareTicket: true,
|
||||||
|
showShareItems: ['shareAppMessage', 'shareTimeline']
|
||||||
|
}).catch(() => {})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 转发给朋友(分享小程序链接)
|
||||||
|
useShareAppMessage(() => {
|
||||||
|
const inviterRaw = dealerUser?.userId ?? Taro.getStorageSync('UserId')
|
||||||
|
const inviter = Number(inviterRaw)
|
||||||
|
const hasInviter = Number.isFinite(inviter) && inviter > 0
|
||||||
|
|
||||||
|
const user = Taro.getStorageSync('User') || {}
|
||||||
|
const nickname = (user && (user.nickname || user.realName || user.username)) || ''
|
||||||
|
const title = hasInviter ? `${nickname || '我'}邀请你加入桂乐淘伙伴计划` : '桂乐淘伙伴计划'
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
path: hasInviter
|
||||||
|
? `/pages/index/index?inviter=${inviter}&source=dealer_qrcode&t=${Date.now()}`
|
||||||
|
: `/pages/index/index`,
|
||||||
|
success: function () {
|
||||||
|
Taro.showToast({title: '分享成功', icon: 'success', duration: 2000})
|
||||||
|
},
|
||||||
|
fail: function () {
|
||||||
|
Taro.showToast({title: '分享失败', icon: 'none', duration: 2000})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 生成小程序码
|
// 生成小程序码
|
||||||
const generateMiniProgramCode = async () => {
|
const generateMiniProgramCode = async () => {
|
||||||
@@ -22,7 +56,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setCodeLoading(true)
|
||||||
|
|
||||||
// 生成邀请小程序码
|
// 生成邀请小程序码
|
||||||
const codeUrl = await generateInviteCode(dealerUser.userId)
|
const codeUrl = await generateInviteCode(dealerUser.userId)
|
||||||
@@ -40,7 +74,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
// 清空之前的二维码
|
// 清空之前的二维码
|
||||||
setMiniProgramCodeUrl('')
|
setMiniProgramCodeUrl('')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setCodeLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +101,66 @@ const DealerQrcode: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [dealerUser?.userId])
|
}, [dealerUser?.userId])
|
||||||
|
|
||||||
|
const isAlbumAuthError = (errMsg?: string) => {
|
||||||
|
if (!errMsg) return false
|
||||||
|
// WeChat uses variants like: "saveImageToPhotosAlbum:fail auth deny",
|
||||||
|
// "saveImageToPhotosAlbum:fail auth denied", "authorize:fail auth deny"
|
||||||
|
return (
|
||||||
|
errMsg.includes('auth deny') ||
|
||||||
|
errMsg.includes('auth denied') ||
|
||||||
|
errMsg.includes('authorize') ||
|
||||||
|
errMsg.includes('scope.writePhotosAlbum')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureWriteAlbumPermission = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const setting = await Taro.getSetting()
|
||||||
|
if (setting?.authSetting?.['scope.writePhotosAlbum']) return true
|
||||||
|
|
||||||
|
await Taro.authorize({scope: 'scope.writePhotosAlbum'})
|
||||||
|
return true
|
||||||
|
} catch (error: any) {
|
||||||
|
const modal = await Taro.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '需要您授权保存图片到相册,请在设置中开启相册权限',
|
||||||
|
confirmText: '去设置'
|
||||||
|
})
|
||||||
|
if (modal.confirm) {
|
||||||
|
await Taro.openSetting()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadImageToLocalPath = async (url: string): Promise<string> => {
|
||||||
|
// saveImageToPhotosAlbum must receive a local temp path (e.g. `http://tmp/...` or `wxfile://...`).
|
||||||
|
// Some environments may return a non-existing temp path from getImageInfo, so we verify.
|
||||||
|
if (url.startsWith('http://tmp/') || url.startsWith('wxfile://')) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = Taro.getStorageSync('access_token')
|
||||||
|
const tenantId = Taro.getStorageSync('TenantId')
|
||||||
|
const header: Record<string, string> = {}
|
||||||
|
if (token) header.Authorization = token
|
||||||
|
if (tenantId) header.TenantId = tenantId
|
||||||
|
|
||||||
|
// 先下载到本地临时文件再保存到相册
|
||||||
|
const res = await Taro.downloadFile({url, header})
|
||||||
|
if (res.statusCode !== 200 || !res.tempFilePath) {
|
||||||
|
throw new Error(`图片下载失败(${res.statusCode || 'unknown'})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-check file exists to avoid: saveImageToPhotosAlbum:fail no such file or directory
|
||||||
|
try {
|
||||||
|
await Taro.getFileInfo({filePath: res.tempFilePath})
|
||||||
|
} catch (_) {
|
||||||
|
throw new Error('图片临时文件不存在,请重试')
|
||||||
|
}
|
||||||
|
return res.tempFilePath
|
||||||
|
}
|
||||||
|
|
||||||
// 保存小程序码到相册
|
// 保存小程序码到相册
|
||||||
const saveMiniProgramCode = async () => {
|
const saveMiniProgramCode = async () => {
|
||||||
if (!miniProgramCodeUrl) {
|
if (!miniProgramCodeUrl) {
|
||||||
@@ -78,39 +172,64 @@ const DealerQrcode: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先下载图片到本地
|
if (saving) return
|
||||||
const res = await Taro.downloadFile({
|
setSaving(true)
|
||||||
url: miniProgramCodeUrl
|
Taro.showLoading({title: '保存中...'})
|
||||||
})
|
|
||||||
|
|
||||||
if (res.statusCode === 200) {
|
const hasPermission = await ensureWriteAlbumPermission()
|
||||||
// 保存到相册
|
if (!hasPermission) return
|
||||||
await Taro.saveImageToPhotosAlbum({
|
|
||||||
filePath: res.tempFilePath
|
|
||||||
})
|
|
||||||
|
|
||||||
Taro.showToast({
|
let filePath = await downloadImageToLocalPath(miniProgramCodeUrl)
|
||||||
title: '保存成功',
|
try {
|
||||||
icon: 'success'
|
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) {
|
} catch (error: any) {
|
||||||
if (error.errMsg?.includes('auth deny')) {
|
const errMsg = error?.errMsg || error?.message
|
||||||
Taro.showModal({
|
if (errMsg?.includes('cancel')) {
|
||||||
|
Taro.showToast({title: '已取消', icon: 'none'})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAlbumAuthError(errMsg)) {
|
||||||
|
const modal = await Taro.showModal({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
content: '需要您授权保存图片到相册',
|
content: '需要您授权保存图片到相册',
|
||||||
success: (res) => {
|
confirmText: '去设置'
|
||||||
if (res.confirm) {
|
|
||||||
Taro.openSetting()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
if (modal.confirm) {
|
||||||
|
await Taro.openSetting()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Taro.showToast({
|
// Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
|
||||||
|
await Taro.showModal({
|
||||||
title: '保存失败',
|
title: '保存失败',
|
||||||
icon: 'error'
|
content: errMsg || '保存失败,请稍后重试',
|
||||||
|
showCancel: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
Taro.hideLoading()
|
||||||
|
setSaving(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +245,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
//
|
//
|
||||||
// const inviteText = `🎉 邀请您加入我的团队!
|
// const inviteText = `🎉 邀请您加入我的团队!
|
||||||
//
|
//
|
||||||
// 扫描小程序码或搜索"时里院子市集"小程序,即可享受优质商品和服务!
|
// 扫描小程序码或搜索"桂乐淘"小程序,即可享受优质商品和服务!
|
||||||
//
|
//
|
||||||
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
||||||
// 🎁 新用户专享优惠等你来拿
|
// 🎁 新用户专享优惠等你来拿
|
||||||
@@ -162,7 +281,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (!dealerUser) {
|
if (dealerLoading) {
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
<Loading/>
|
<Loading/>
|
||||||
@@ -171,6 +290,33 @@ const DealerQrcode: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
|
||||||
|
<Text className="text-gray-800 font-semibold">加载失败</Text>
|
||||||
|
<Text className="text-gray-500 text-sm mt-2">{error}</Text>
|
||||||
|
<Button className="mt-6" type="primary" onClick={refresh}>重试</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未成为分销商时给出明确引导,避免一直停留在“加载中”
|
||||||
|
if (!dealerUser) {
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
|
||||||
|
<Text className="text-gray-800 font-semibold">你还不是分销商</Text>
|
||||||
|
<Text className="text-gray-500 text-sm mt-2 text-center">申请成为分销商后即可生成分享码</Text>
|
||||||
|
<Button
|
||||||
|
className="mt-6"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => Taro.navigateTo({url: '/dealer/apply/add'})}
|
||||||
|
>
|
||||||
|
去申请
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen">
|
<View className="bg-gray-50 min-h-screen">
|
||||||
{/* 头部卡片 */}
|
{/* 头部卡片 */}
|
||||||
@@ -185,9 +331,9 @@ const DealerQrcode: React.FC = () => {
|
|||||||
}}></View>
|
}}></View>
|
||||||
|
|
||||||
<View className="relative z-10 flex flex-col">
|
<View className="relative z-10 flex flex-col">
|
||||||
<Text className="text-2xl font-bold mb-2 text-white">我的邀请小程序码</Text>
|
<Text className="text-2xl font-bold mb-2 text-white">我的分享码</Text>
|
||||||
<Text className="text-white text-opacity-80">
|
<Text className="text-white text-opacity-80">
|
||||||
分享小程序码邀请好友,获得丰厚佣金奖励
|
与好友“共享福利 一起省、一起赚”
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -196,7 +342,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
{/* 小程序码展示区 */}
|
{/* 小程序码展示区 */}
|
||||||
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
{loading ? (
|
{codeLoading ? (
|
||||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||||||
<Loading/>
|
<Loading/>
|
||||||
<Text className="text-gray-500 mt-2">生成中...</Text>
|
<Text className="text-gray-500 mt-2">生成中...</Text>
|
||||||
@@ -239,10 +385,10 @@ const DealerQrcode: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<View className="text-lg font-semibold text-gray-800 mb-2">
|
<View className="text-lg font-semibold text-gray-800 mb-2">
|
||||||
扫码加入我的团队
|
桂乐淘伙伴计划
|
||||||
</View>
|
</View>
|
||||||
<View className="text-sm text-gray-500 mb-4">
|
<View className="text-sm text-gray-500 mb-4">
|
||||||
好友扫描小程序码即可直接进入小程序并建立邀请关系
|
自购省 | 分享赚 | 好友惠
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
||||||
@@ -258,34 +404,12 @@ const DealerQrcode: React.FC = () => {
|
|||||||
block
|
block
|
||||||
icon={<Download/>}
|
icon={<Download/>}
|
||||||
onClick={saveMiniProgramCode}
|
onClick={saveMiniProgramCode}
|
||||||
disabled={!miniProgramCodeUrl || loading}
|
disabled={!miniProgramCodeUrl || codeLoading || saving}
|
||||||
>
|
>
|
||||||
保存小程序码到相册
|
保存小程序码到相册
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
{/*<View className={'my-2 bg-white'}>*/}
|
|
||||||
{/* <Button*/}
|
|
||||||
{/* size="large"*/}
|
|
||||||
{/* block*/}
|
|
||||||
{/* icon={<Copy/>}*/}
|
|
||||||
{/* onClick={copyInviteInfo}*/}
|
|
||||||
{/* disabled={!dealerUser?.userId || loading}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* 复制邀请信息*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
{/*<View className={'my-2 bg-white'}>*/}
|
|
||||||
{/* <Button*/}
|
|
||||||
{/* size="large"*/}
|
|
||||||
{/* block*/}
|
|
||||||
{/* fill="outline"*/}
|
|
||||||
{/* icon={<Share/>}*/}
|
|
||||||
{/* onClick={shareMiniProgramCode}*/}
|
|
||||||
{/* disabled={!dealerUser?.userId || loading}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* 分享给好友*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 推广说明 */}
|
{/* 推广说明 */}
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ const DealerTeam: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
{/* 显示手机号(仅本级可见) */}
|
{/* 显示手机号(仅本级可见) */}
|
||||||
{showPhone && member.phone && (
|
{showPhone && member.phone && (
|
||||||
<Text className="text-sm text-gray-500" onClick={(e) => {
|
<Text className="text-sm text-gray-500 hidden" onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
makePhoneCall(member.phone || '');
|
makePhoneCall(member.phone || '');
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { render, fireEvent, waitFor } from '@testing-library/react'
|
|
||||||
import DealerWithdraw from '../index'
|
|
||||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
|
||||||
import * as withdrawAPI from '@/api/shop/shopDealerWithdraw'
|
|
||||||
|
|
||||||
// Mock dependencies
|
|
||||||
jest.mock('@/hooks/useDealerUser')
|
|
||||||
jest.mock('@/api/shop/shopDealerWithdraw')
|
|
||||||
jest.mock('@tarojs/taro', () => ({
|
|
||||||
showToast: jest.fn(),
|
|
||||||
getStorageSync: jest.fn(() => 123),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const mockUseDealerUser = useDealerUser as jest.MockedFunction<typeof useDealerUser>
|
|
||||||
const mockAddShopDealerWithdraw = withdrawAPI.addShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.addShopDealerWithdraw>
|
|
||||||
const mockPageShopDealerWithdraw = withdrawAPI.pageShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.pageShopDealerWithdraw>
|
|
||||||
|
|
||||||
describe('DealerWithdraw', () => {
|
|
||||||
const mockDealerUser = {
|
|
||||||
userId: 123,
|
|
||||||
money: '10000.00',
|
|
||||||
realName: '测试用户',
|
|
||||||
mobile: '13800138000'
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockUseDealerUser.mockReturnValue({
|
|
||||||
dealerUser: mockDealerUser,
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
refresh: jest.fn()
|
|
||||||
})
|
|
||||||
|
|
||||||
mockPageShopDealerWithdraw.mockResolvedValue({
|
|
||||||
list: [],
|
|
||||||
count: 0
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('应该正确显示可提现余额', () => {
|
|
||||||
const { getByText } = render(<DealerWithdraw />)
|
|
||||||
expect(getByText('10000.00')).toBeInTheDocument()
|
|
||||||
expect(getByText('可提现余额')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('应该验证最低提现金额', async () => {
|
|
||||||
mockAddShopDealerWithdraw.mockResolvedValue('success')
|
|
||||||
|
|
||||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
|
||||||
|
|
||||||
// 输入低于最低金额的数值
|
|
||||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
|
||||||
fireEvent.change(amountInput, { target: { value: '50' } })
|
|
||||||
|
|
||||||
// 选择提现方式
|
|
||||||
const wechatRadio = getByText('微信钱包')
|
|
||||||
fireEvent.click(wechatRadio)
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const submitButton = getByText('申请提现')
|
|
||||||
fireEvent.click(submitButton)
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
|
||||||
title: '最低提现金额为100元',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('应该验证提现金额不超过可用余额', async () => {
|
|
||||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
|
||||||
|
|
||||||
// 输入超过可用余额的金额
|
|
||||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
|
||||||
fireEvent.change(amountInput, { target: { value: '20000' } })
|
|
||||||
|
|
||||||
// 选择提现方式
|
|
||||||
const wechatRadio = getByText('微信钱包')
|
|
||||||
fireEvent.click(wechatRadio)
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const submitButton = getByText('申请提现')
|
|
||||||
fireEvent.click(submitButton)
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
|
||||||
title: '提现金额超过可用余额',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('应该验证支付宝账户信息完整性', async () => {
|
|
||||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
|
||||||
|
|
||||||
// 输入有效金额
|
|
||||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
|
||||||
fireEvent.change(amountInput, { target: { value: '1000' } })
|
|
||||||
|
|
||||||
// 选择支付宝提现
|
|
||||||
const alipayRadio = getByText('支付宝')
|
|
||||||
fireEvent.click(alipayRadio)
|
|
||||||
|
|
||||||
// 只填写账号,不填写姓名
|
|
||||||
const accountInput = getByPlaceholderText('请输入支付宝账号')
|
|
||||||
fireEvent.change(accountInput, { target: { value: 'test@alipay.com' } })
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const submitButton = getByText('申请提现')
|
|
||||||
fireEvent.click(submitButton)
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
|
||||||
title: '请填写完整的支付宝信息',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('应该成功提交微信提现申请', async () => {
|
|
||||||
mockAddShopDealerWithdraw.mockResolvedValue('success')
|
|
||||||
|
|
||||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
|
||||||
|
|
||||||
// 输入有效金额
|
|
||||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
|
||||||
fireEvent.change(amountInput, { target: { value: '1000' } })
|
|
||||||
|
|
||||||
// 选择微信提现
|
|
||||||
const wechatRadio = getByText('微信钱包')
|
|
||||||
fireEvent.click(wechatRadio)
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const submitButton = getByText('申请提现')
|
|
||||||
fireEvent.click(submitButton)
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockAddShopDealerWithdraw).toHaveBeenCalledWith({
|
|
||||||
userId: 123,
|
|
||||||
money: '1000',
|
|
||||||
payType: 10,
|
|
||||||
applyStatus: 10,
|
|
||||||
platform: 'MiniProgram'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
|
||||||
title: '提现申请已提交',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('快捷金额按钮应该正常工作', () => {
|
|
||||||
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
|
|
||||||
|
|
||||||
// 点击快捷金额按钮
|
|
||||||
const quickAmountButton = getByText('500')
|
|
||||||
fireEvent.click(quickAmountButton)
|
|
||||||
|
|
||||||
// 验证金额输入框的值
|
|
||||||
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
|
|
||||||
expect(amountInput.value).toBe('500')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('全部按钮应该设置为可用余额', () => {
|
|
||||||
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
|
|
||||||
|
|
||||||
// 点击全部按钮
|
|
||||||
const allButton = getByText('全部')
|
|
||||||
fireEvent.click(allButton)
|
|
||||||
|
|
||||||
// 验证金额输入框的值
|
|
||||||
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
|
|
||||||
expect(amountInput.value).toBe('10000.00')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import {
|
import {
|
||||||
Cell,
|
|
||||||
Space,
|
Space,
|
||||||
Button,
|
Button,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
CellGroup,
|
CellGroup,
|
||||||
Radio,
|
|
||||||
Tabs,
|
Tabs,
|
||||||
Tag,
|
Tag,
|
||||||
Empty,
|
Empty,
|
||||||
@@ -18,24 +16,100 @@ import {Wallet} from '@nutui/icons-react-taro'
|
|||||||
import {businessGradients} from '@/styles/gradients'
|
import {businessGradients} from '@/styles/gradients'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
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'
|
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
|
||||||
|
|
||||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||||
accountDisplay?: string
|
accountDisplay?: string
|
||||||
|
// Backend may include these fields for WeChat "confirm receipt" flow after approval.
|
||||||
|
package_info?: string
|
||||||
|
packageInfo?: string
|
||||||
|
package?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractPackageInfo = (result: unknown): string | null => {
|
||||||
|
if (typeof result === 'string') return result
|
||||||
|
if (!result || typeof result !== 'object') return null
|
||||||
|
const r = result as any
|
||||||
|
return (
|
||||||
|
r.package_info ??
|
||||||
|
r.packageInfo ??
|
||||||
|
r.package ??
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canRequestMerchantTransferConfirm = (): boolean => {
|
||||||
|
try {
|
||||||
|
if (typeof (Taro as any).getEnv === 'function' && (Taro as any).ENV_TYPE) {
|
||||||
|
const env = (Taro as any).getEnv()
|
||||||
|
if (env !== (Taro as any).ENV_TYPE.WEAPP) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const api =
|
||||||
|
(globalThis as any).wx?.requestMerchantTransfer ||
|
||||||
|
(Taro as any).requestMerchantTransfer
|
||||||
|
|
||||||
|
return typeof api === 'function'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestMerchantTransferConfirm = (packageInfo: string): Promise<any> => {
|
||||||
|
if (!canRequestMerchantTransferConfirm()) {
|
||||||
|
return Promise.reject(new Error('请在微信小程序内完成收款确认'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend may wrap/format base64 with newlines; WeChat API requires a clean string.
|
||||||
|
const cleanPackageInfo = String(packageInfo).replace(/\s+/g, '')
|
||||||
|
|
||||||
|
const api =
|
||||||
|
(globalThis as any).wx?.requestMerchantTransfer ||
|
||||||
|
(Taro as any).requestMerchantTransfer
|
||||||
|
|
||||||
|
if (typeof api !== 'function') {
|
||||||
|
return Promise.reject(new Error('当前环境不支持商家转账收款确认(缺少 requestMerchantTransfer)'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
api({
|
||||||
|
// WeChat API uses `package`, backend returns `package_info`.
|
||||||
|
package: cleanPackageInfo,
|
||||||
|
mchId: '1737910695',
|
||||||
|
appId: 'wxad831ba00ad6a026',
|
||||||
|
success: (res: any) => resolve(res),
|
||||||
|
fail: (err: any) => reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some backends may return money fields as number; keep internal usage always as string.
|
||||||
|
const normalizeMoneyString = (money: unknown) => {
|
||||||
|
if (money === null || money === undefined || money === '') return '0.00'
|
||||||
|
return typeof money === 'string' ? money : String(money)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DealerWithdraw: React.FC = () => {
|
const DealerWithdraw: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState<string | number>('0')
|
const [activeTab, setActiveTab] = useState<string | number>('0')
|
||||||
const [selectedAccount, setSelectedAccount] = useState('')
|
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||||
|
const [claimingId, setClaimingId] = useState<number | null>(null)
|
||||||
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
|
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
|
||||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||||
const formRef = useRef<any>(null)
|
const formRef = useRef<any>(null)
|
||||||
|
|
||||||
const {dealerUser} = useDealerUser()
|
const {dealerUser} = useDealerUser()
|
||||||
|
const [verifyStatus, setVerifyStatus] = useState<'unknown' | 'verified' | 'unverified' | 'pending' | 'rejected'>('unknown')
|
||||||
|
const [verifyStatusText, setVerifyStatusText] = useState<string>('')
|
||||||
|
|
||||||
// Tab 切换处理函数
|
// Tab 切换处理函数
|
||||||
const handleTabChange = (value: string | number) => {
|
const handleTabChange = (value: string | number) => {
|
||||||
@@ -52,7 +126,7 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
const fetchBalance = useCallback(async () => {
|
const fetchBalance = useCallback(async () => {
|
||||||
console.log(dealerUser, 'dealerUser...')
|
console.log(dealerUser, 'dealerUser...')
|
||||||
try {
|
try {
|
||||||
setAvailableAmount(dealerUser?.money || '0.00')
|
setAvailableAmount(normalizeMoneyString(dealerUser?.money))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取余额失败:', error)
|
console.error('获取余额失败:', error)
|
||||||
}
|
}
|
||||||
@@ -115,12 +189,63 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [fetchBalance, fetchWithdrawRecords])
|
}, [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) => {
|
const getStatusText = (status?: number) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 40:
|
case 40:
|
||||||
return '已到账'
|
return '已到账'
|
||||||
case 20:
|
case 20:
|
||||||
return '审核通过'
|
return '待领取'
|
||||||
case 10:
|
case 10:
|
||||||
return '待审核'
|
return '待审核'
|
||||||
case 30:
|
case 30:
|
||||||
@@ -135,7 +260,7 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
case 40:
|
case 40:
|
||||||
return 'success'
|
return 'success'
|
||||||
case 20:
|
case 20:
|
||||||
return 'success'
|
return 'info'
|
||||||
case 10:
|
case 10:
|
||||||
return 'warning'
|
return 'warning'
|
||||||
case 30:
|
case 30:
|
||||||
@@ -154,17 +279,17 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.accountType) {
|
if (verifyStatus !== 'verified') {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '请选择提现方式',
|
title: '请先完成实名认证',
|
||||||
icon: 'error'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证提现金额
|
// 验证提现金额
|
||||||
const amount = parseFloat(values.amount)
|
const amount = parseFloat(String(values.amount))
|
||||||
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, ''))
|
||||||
|
|
||||||
if (isNaN(amount) || amount <= 0) {
|
if (isNaN(amount) || amount <= 0) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
@@ -175,11 +300,11 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (amount < 100) {
|
if (amount < 100) {
|
||||||
Taro.showToast({
|
// Taro.showToast({
|
||||||
title: '最低提现金额为100元',
|
// title: '最低提现金额为100元',
|
||||||
icon: 'error'
|
// icon: 'error'
|
||||||
})
|
// })
|
||||||
return
|
// return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amount > available) {
|
if (amount > available) {
|
||||||
@@ -190,57 +315,26 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
return
|
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 {
|
try {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
|
|
||||||
const withdrawData: ShopDealerWithdraw = {
|
const withdrawData: ShopDealerWithdraw = {
|
||||||
userId: dealerUser.userId,
|
userId: dealerUser.userId,
|
||||||
money: values.amount,
|
money: values.amount,
|
||||||
payType: values.accountType === 'wechat' ? 10 :
|
// Only support WeChat wallet withdrawals.
|
||||||
values.accountType === 'alipay' ? 20 : 30,
|
payType: 10,
|
||||||
applyStatus: 10, // 待审核
|
|
||||||
platform: 'MiniProgram'
|
platform: 'MiniProgram'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据提现方式设置账户信息
|
// Security flow:
|
||||||
if (values.accountType === 'alipay') {
|
// 1) user submits => applyStatus=10 (待审核)
|
||||||
withdrawData.alipayAccount = values.account
|
// 2) backend审核通过 => applyStatus=20 (待领取)
|
||||||
withdrawData.alipayName = values.accountName
|
// 3) user goes to records to "领取" => applyStatus=40 (已到账)
|
||||||
} else if (values.accountType === 'bank') {
|
|
||||||
withdrawData.bankCard = values.account
|
|
||||||
withdrawData.bankAccount = values.accountName
|
|
||||||
withdrawData.bankName = values.bankName || '银行卡'
|
|
||||||
}
|
|
||||||
|
|
||||||
await addShopDealerWithdraw(withdrawData)
|
await addShopDealerWithdraw(withdrawData)
|
||||||
|
Taro.showToast({title: '提现申请已提交,等待审核', icon: 'success'})
|
||||||
Taro.showToast({
|
|
||||||
title: '提现申请已提交',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
formRef.current?.resetFields()
|
formRef.current?.resetFields()
|
||||||
setSelectedAccount('')
|
|
||||||
|
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
await handleRefresh()
|
await handleRefresh()
|
||||||
@@ -259,6 +353,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 quickAmounts = ['100', '300', '500', '1000']
|
||||||
|
|
||||||
const setQuickAmount = (amount: string) => {
|
const setQuickAmount = (amount: string) => {
|
||||||
@@ -266,17 +419,37 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setAllAmount = () => {
|
const setAllAmount = () => {
|
||||||
formRef.current?.setFieldsValue({amount: availableAmount.replace(/,/g, '')})
|
formRef.current?.setFieldsValue({amount: normalizeMoneyString(availableAmount).replace(/,/g, '')})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化金额
|
// 格式化金额
|
||||||
const formatMoney = (money?: string) => {
|
const formatMoney = (money?: unknown) => {
|
||||||
if (!money) return '0.00'
|
const n = parseFloat(normalizeMoneyString(money).replace(/,/g, ''))
|
||||||
return parseFloat(money).toFixed(2)
|
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||||||
|
}
|
||||||
|
|
||||||
|
const goVerify = () => {
|
||||||
|
goTo('/user/userVerify/index')
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderWithdrawForm = () => (
|
const renderWithdrawForm = () => (
|
||||||
<View>
|
<View>
|
||||||
|
{(verifyStatus === 'unverified' || verifyStatus === 'pending' || verifyStatus === 'rejected') && (
|
||||||
|
<View className="rounded-lg bg-white px-4 py-3 mb-4 mx-4">
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<View className="flex flex-col">
|
||||||
|
<Text className="text-sm text-red-500">提现前请先完成实名认证</Text>
|
||||||
|
{verifyStatusText ? (
|
||||||
|
<Text className="text-xs text-gray-500 mt-1">当前状态:{verifyStatusText}</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
<Text className="text-sm text-blue-600" onClick={goVerify}>
|
||||||
|
去认证
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 余额卡片 */}
|
{/* 余额卡片 */}
|
||||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||||
background: businessGradients.dealer.header
|
background: businessGradients.dealer.header
|
||||||
@@ -303,7 +476,7 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
|
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-white text-opacity-80 text-xs">
|
<Text className="text-white text-opacity-80 text-xs">
|
||||||
最低提现金额:¥100 | 手续费:免费
|
手续费:免费
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -318,14 +491,6 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="请输入提现金额"
|
placeholder="请输入提现金额"
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(value) => {
|
|
||||||
// 实时验证提现金额
|
|
||||||
const amount = parseFloat(value)
|
|
||||||
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
|
||||||
if (!isNaN(amount) && amount > available) {
|
|
||||||
// 可以在这里添加实时提示,但不阻止输入
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -353,54 +518,14 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Form.Item name="accountType" label="提现方式" required>
|
<View className="px-4 py-2">
|
||||||
<Radio.Group value={selectedAccount} onChange={() => setSelectedAccount}>
|
<Text className="text-sm text-red-500">
|
||||||
<Cell.Group>
|
注意事项:
|
||||||
<Cell>
|
1. 提取佣金必须完成实名认证。
|
||||||
<Radio value="wechat">微信钱包</Radio>
|
2. 佣金非自动到账,再您提取佣金申请通过后,请手动到我的申请记录点击领取。
|
||||||
</Cell>
|
3. 桂乐淘温馨提示,请您依法依规申报所得,缴税相关税费。
|
||||||
<Cell>
|
</Text>
|
||||||
<Radio value="alipay">支付宝</Radio>
|
</View>
|
||||||
</Cell>
|
|
||||||
<Cell>
|
|
||||||
<Radio value="bank">银行卡</Radio>
|
|
||||||
</Cell>
|
|
||||||
</Cell.Group>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
{selectedAccount === 'alipay' && (
|
|
||||||
<>
|
|
||||||
<Form.Item name="account" label="支付宝账号" required>
|
|
||||||
<Input placeholder="请输入支付宝账号"/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="accountName" label="支付宝姓名" required>
|
|
||||||
<Input placeholder="请输入支付宝实名姓名"/>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedAccount === 'bank' && (
|
|
||||||
<>
|
|
||||||
<Form.Item name="bankName" label="开户银行" required>
|
|
||||||
<Input placeholder="请输入开户银行名称"/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="account" label="银行卡号" required>
|
|
||||||
<Input placeholder="请输入银行卡号"/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="accountName" label="开户姓名" required>
|
|
||||||
<Input placeholder="请输入银行卡开户姓名"/>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedAccount === 'wechat' && (
|
|
||||||
<View className="px-4 py-2">
|
|
||||||
<Text className="text-sm text-gray-500">
|
|
||||||
微信钱包提现将直接转入您的微信零钱
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
|
|
||||||
<View className="mt-6 px-4">
|
<View className="mt-6 px-4">
|
||||||
@@ -409,7 +534,7 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
type="primary"
|
type="primary"
|
||||||
nativeType="submit"
|
nativeType="submit"
|
||||||
loading={submitting}
|
loading={submitting}
|
||||||
disabled={submitting || !selectedAccount}
|
disabled={submitting || verifyStatus !== 'verified'}
|
||||||
>
|
>
|
||||||
{submitting ? '提交中...' : '申请提现'}
|
{submitting ? '提交中...' : '申请提现'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -433,35 +558,53 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
) : withdrawRecords.length > 0 ? (
|
) : withdrawRecords.length > 0 ? (
|
||||||
withdrawRecords.map(record => (
|
withdrawRecords.map(record => (
|
||||||
<View key={record.id} className="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
|
<View key={record.id} className="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
|
||||||
<View className="flex justify-between items-start mb-3">
|
<View className="flex justify-between items-start mb-3">
|
||||||
<Space>
|
<Space>
|
||||||
<Text className="font-semibold text-gray-800 mb-1">
|
<Text className="font-semibold text-gray-800 mb-1">
|
||||||
提现金额:¥{record.money}
|
提现金额:¥{record.money}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-sm text-gray-500">
|
{/*<Text className="text-sm text-gray-500">*/}
|
||||||
提现账户:{record.accountDisplay}
|
{/* 提现账户:{record.accountDisplay}*/}
|
||||||
</Text>
|
{/*</Text>*/}
|
||||||
</Space>
|
</Space>
|
||||||
<Tag type={getStatusColor(record.applyStatus)}>
|
<Tag background="#999999" type={getStatusColor(record.applyStatus)} plain>
|
||||||
{getStatusText(record.applyStatus)}
|
{getStatusText(record.applyStatus)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="text-xs text-gray-400">
|
|
||||||
<Text>申请时间:{record.createTime}</Text>
|
{record.applyStatus === 20 && record.payType === 10 && (
|
||||||
{record.auditTime && (
|
<View className="flex mb-5 justify-center">
|
||||||
<Text className="block mt-1">
|
<Button
|
||||||
审核时间:{new Date(record.auditTime).toLocaleString()}
|
size="small"
|
||||||
</Text>
|
type="primary"
|
||||||
|
loading={claimingId === record.id}
|
||||||
|
disabled={claimingId !== null}
|
||||||
|
onClick={() => handleClaim(record)}
|
||||||
|
>
|
||||||
|
立即领取
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
{record.rejectReason && (
|
|
||||||
<Text className="block mt-1 text-red-500">
|
<View className="flex justify-between items-center">
|
||||||
驳回原因:{record.rejectReason}
|
<View className="text-xs text-gray-400">
|
||||||
</Text>
|
<Text>创建时间:{record.createTime}</Text>
|
||||||
)}
|
{record.auditTime && (
|
||||||
</View>
|
<Text className="block mt-1">
|
||||||
|
审核时间:{record.auditTime}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{record.rejectReason && (
|
||||||
|
<Text className="block mt-1 text-red-500">
|
||||||
|
驳回原因:{record.rejectReason}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useState, useEffect, useCallback} from 'react'
|
import {useState, useEffect, useCallback} from 'react'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
import {getShopDealerUser} from '@/api/shop/shopDealerUser'
|
import {getShopDealerUser} from '@/api/shop/shopDealerUser'
|
||||||
import type {ShopDealerUser} from '@/api/shop/shopDealerUser/model'
|
import type {ShopDealerUser} from '@/api/shop/shopDealerUser/model'
|
||||||
|
|
||||||
@@ -22,17 +22,20 @@ export interface UseDealerUserReturn {
|
|||||||
*/
|
*/
|
||||||
export const useDealerUser = (): UseDealerUserReturn => {
|
export const useDealerUser = (): UseDealerUserReturn => {
|
||||||
const [dealerUser, setDealerUser] = useState<ShopDealerUser | null>(null)
|
const [dealerUser, setDealerUser] = useState<ShopDealerUser | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const rawUserId = Taro.getStorageSync('UserId')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const userId = Number(rawUserId)
|
||||||
|
const hasUser = Number.isFinite(userId) && userId > 0
|
||||||
|
|
||||||
const userId = Taro.getStorageSync('UserId');
|
// If user is logged in, start in loading state to avoid "click too fast" mis-routing.
|
||||||
|
const [loading, setLoading] = useState<boolean>(hasUser)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
// 获取经销商用户数据
|
// 获取经销商用户数据
|
||||||
const fetchDealerData = useCallback(async () => {
|
const fetchDealerData = useCallback(async () => {
|
||||||
|
|
||||||
if (!userId) {
|
if (!hasUser) {
|
||||||
console.log('🔍 用户未登录,提前返回')
|
|
||||||
setDealerUser(null)
|
setDealerUser(null)
|
||||||
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +58,7 @@ export const useDealerUser = (): UseDealerUserReturn => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [userId])
|
}, [hasUser, userId])
|
||||||
|
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
@@ -64,13 +67,31 @@ export const useDealerUser = (): UseDealerUserReturn => {
|
|||||||
|
|
||||||
// 初始化加载数据
|
// 初始化加载数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userId) {
|
if (hasUser) {
|
||||||
console.log('🔍 调用 fetchDealerData')
|
|
||||||
fetchDealerData()
|
fetchDealerData()
|
||||||
} else {
|
} 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 {
|
return {
|
||||||
dealerUser,
|
dealerUser,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { UserOrderStats } from '@/api/user';
|
import { getUserOrderStats, UserOrderStats } from '@/api/user';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import {pageShopOrder} from "@/api/shop/shopOrder";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单统计Hook
|
* 订单统计Hook
|
||||||
@@ -31,20 +30,17 @@ export const useOrderStats = () => {
|
|||||||
if(!Taro.getStorageSync('UserId')){
|
if(!Taro.getStorageSync('UserId')){
|
||||||
return false;
|
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 stats = await getUserOrderStats();
|
||||||
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')})
|
|
||||||
setOrderStats({
|
setOrderStats({
|
||||||
pending: pending?.count || 0,
|
pending: stats?.pending || 0,
|
||||||
paid: paid?.count || 0,
|
paid: stats?.paid || 0,
|
||||||
shipped: shipped?.count || 0,
|
shipped: stats?.shipped || 0,
|
||||||
completed: completed?.count || 0,
|
completed: stats?.completed || 0,
|
||||||
refund: refund?.count || 0,
|
refund: stats?.refund || 0,
|
||||||
total: total?.count || 0
|
total: stats?.total || 0
|
||||||
})
|
})
|
||||||
|
|
||||||
if (showToast) {
|
if (showToast) {
|
||||||
|
|||||||
@@ -13,19 +13,30 @@ export interface CountdownTime {
|
|||||||
totalMinutes: number; // 总剩余分钟数
|
totalMinutes: number; // 总剩余分钟数
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UsePaymentCountdownParams {
|
||||||
|
/** 订单创建时间(用于兼容:当 expirationTime 缺失时按 createTime + timeoutHours 计算) */
|
||||||
|
createTime?: string;
|
||||||
|
/** 订单过期时间(推荐直接传后端返回的 expirationTime) */
|
||||||
|
expirationTime?: string;
|
||||||
|
/** 支付状态 */
|
||||||
|
payStatus?: boolean;
|
||||||
|
/** 是否实时更新(详情页用true,列表页用false) */
|
||||||
|
realTime?: boolean;
|
||||||
|
/** 超时小时数,默认24小时(仅在 expirationTime 缺失时生效) */
|
||||||
|
timeoutHours?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付倒计时Hook
|
* 支付倒计时Hook
|
||||||
* @param createTime 订单创建时间
|
* 优先使用 expirationTime;当 expirationTime 缺失时回退到 createTime + timeoutHours。
|
||||||
* @param payStatus 支付状态
|
|
||||||
* @param realTime 是否实时更新(详情页用true,列表页用false)
|
|
||||||
* @param timeoutHours 超时小时数,默认24小时
|
|
||||||
*/
|
*/
|
||||||
export const usePaymentCountdown = (
|
export const usePaymentCountdown = ({
|
||||||
createTime?: string,
|
createTime,
|
||||||
payStatus?: boolean,
|
expirationTime,
|
||||||
realTime: boolean = false,
|
payStatus,
|
||||||
timeoutHours: number = 24
|
realTime = false,
|
||||||
): CountdownTime => {
|
timeoutHours = 24
|
||||||
|
}: UsePaymentCountdownParams): CountdownTime => {
|
||||||
const [timeLeft, setTimeLeft] = useState<CountdownTime>({
|
const [timeLeft, setTimeLeft] = useState<CountdownTime>({
|
||||||
hours: 0,
|
hours: 0,
|
||||||
minutes: 0,
|
minutes: 0,
|
||||||
@@ -37,7 +48,7 @@ export const usePaymentCountdown = (
|
|||||||
// 计算剩余时间的函数
|
// 计算剩余时间的函数
|
||||||
const calculateTimeLeft = useMemo(() => {
|
const calculateTimeLeft = useMemo(() => {
|
||||||
return (): CountdownTime => {
|
return (): CountdownTime => {
|
||||||
if (!createTime || payStatus) {
|
if (payStatus || (!expirationTime && !createTime)) {
|
||||||
return {
|
return {
|
||||||
hours: 0,
|
hours: 0,
|
||||||
minutes: 0,
|
minutes: 0,
|
||||||
@@ -47,8 +58,27 @@ export const usePaymentCountdown = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const createTimeObj = dayjs(createTime);
|
// 优先使用后端过期时间;如果无法解析,再回退到 createTime + timeoutHours
|
||||||
const expireTime = createTimeObj.add(timeoutHours, 'hour');
|
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 now = dayjs();
|
||||||
const diff = expireTime.diff(now);
|
const diff = expireTime.diff(now);
|
||||||
|
|
||||||
@@ -76,10 +106,10 @@ export const usePaymentCountdown = (
|
|||||||
totalMinutes
|
totalMinutes
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}, [createTime, payStatus, timeoutHours]);
|
}, [createTime, expirationTime, payStatus, timeoutHours]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!createTime || payStatus) {
|
if (payStatus || (!expirationTime && !createTime)) {
|
||||||
setTimeLeft({
|
setTimeLeft({
|
||||||
hours: 0,
|
hours: 0,
|
||||||
minutes: 0,
|
minutes: 0,
|
||||||
@@ -111,7 +141,7 @@ export const usePaymentCountdown = (
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [createTime, payStatus, realTime, calculateTimeLeft]);
|
}, [createTime, expirationTime, payStatus, realTime, calculateTimeLeft]);
|
||||||
|
|
||||||
return timeLeft;
|
return timeLeft;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { gradientThemes, GradientTheme, gradientUtils } from '@/styles/gradients'
|
import { gradientThemes, type GradientTheme, gradientUtils } from '@/styles/gradients'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
|
|
||||||
export interface UseThemeReturn {
|
export interface UseThemeReturn {
|
||||||
@@ -14,28 +14,42 @@ export interface UseThemeReturn {
|
|||||||
* 提供主题切换和状态管理功能
|
* 提供主题切换和状态管理功能
|
||||||
*/
|
*/
|
||||||
export const useTheme = (): UseThemeReturn => {
|
export const useTheme = (): UseThemeReturn => {
|
||||||
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(gradientThemes[0])
|
const getSavedThemeName = useCallback((): string => {
|
||||||
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(true)
|
try {
|
||||||
|
return Taro.getStorageSync('user_theme') || 'nature'
|
||||||
// 获取当前主题
|
} catch {
|
||||||
const getCurrentTheme = (): GradientTheme => {
|
return 'nature'
|
||||||
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 getStoredUserId = useCallback((): number => {
|
||||||
|
try {
|
||||||
|
const raw = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId')
|
||||||
|
const asNumber = typeof raw === 'number' ? raw : parseInt(String(raw || '1'), 10)
|
||||||
|
return Number.isFinite(asNumber) ? asNumber : 1
|
||||||
|
} catch {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const resolveTheme = useCallback(
|
||||||
|
(themeName: string): GradientTheme => {
|
||||||
|
if (themeName === 'auto') {
|
||||||
|
return gradientUtils.getThemeByUserId(getStoredUserId())
|
||||||
|
}
|
||||||
|
return gradientThemes.find(t => t.name === themeName) || gradientUtils.getThemeByName('nature') || gradientThemes[0]
|
||||||
|
},
|
||||||
|
[getStoredUserId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(() => getSavedThemeName() === 'auto')
|
||||||
|
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(() => resolveTheme(getSavedThemeName()))
|
||||||
|
|
||||||
// 初始化主题
|
// 初始化主题
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
const savedTheme = getSavedThemeName()
|
||||||
setIsAutoTheme(savedTheme === 'auto')
|
setIsAutoTheme(savedTheme === 'auto')
|
||||||
setCurrentTheme(getCurrentTheme())
|
setCurrentTheme(resolveTheme(savedTheme))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 设置主题
|
// 设置主题
|
||||||
@@ -43,7 +57,7 @@ export const useTheme = (): UseThemeReturn => {
|
|||||||
try {
|
try {
|
||||||
Taro.setStorageSync('user_theme', themeName)
|
Taro.setStorageSync('user_theme', themeName)
|
||||||
setIsAutoTheme(themeName === 'auto')
|
setIsAutoTheme(themeName === 'auto')
|
||||||
setCurrentTheme(getCurrentTheme())
|
setCurrentTheme(resolveTheme(themeName))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存主题失败:', error)
|
console.error('保存主题失败:', error)
|
||||||
}
|
}
|
||||||
@@ -51,7 +65,7 @@ export const useTheme = (): UseThemeReturn => {
|
|||||||
|
|
||||||
// 刷新主题(用于自动主题模式下用户信息变更时)
|
// 刷新主题(用于自动主题模式下用户信息变更时)
|
||||||
const refreshTheme = () => {
|
const refreshTheme = () => {
|
||||||
setCurrentTheme(getCurrentTheme())
|
setCurrentTheme(resolveTheme(getSavedThemeName()))
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
parseQRContent
|
parseQRContent
|
||||||
} from '@/api/passport/qr-login';
|
} from '@/api/passport/qr-login';
|
||||||
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
||||||
|
import { getGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket';
|
||||||
import { useUser } from "@/hooks/useUser";
|
import { useUser } from "@/hooks/useUser";
|
||||||
import { isValidJSON } from "@/utils/jsonUtils";
|
import { isValidJSON } from "@/utils/jsonUtils";
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -29,6 +30,15 @@ export enum ScanType {
|
|||||||
UNKNOWN = 'unknown' // 未知类型
|
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格式(核销二维码)
|
// 1. 检查是否为JSON格式(核销二维码)
|
||||||
if (isValidJSON(scanResult)) {
|
if (isValidJSON(scanResult)) {
|
||||||
const json = JSON.parse(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;
|
return ScanType.VERIFICATION;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,35 +144,79 @@ export function useUnifiedQRScan() {
|
|||||||
throw new Error('您没有核销权限');
|
throw new Error('您没有核销权限');
|
||||||
}
|
}
|
||||||
|
|
||||||
let code = '';
|
let businessType: VerificationBusinessType = 'gift';
|
||||||
|
let decryptedOrRaw = '';
|
||||||
|
|
||||||
// 判断是否为加密的JSON格式
|
// 判断是否为加密的JSON格式
|
||||||
if (isValidJSON(scanResult)) {
|
if (isValidJSON(scanResult)) {
|
||||||
const json = JSON.parse(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({
|
const decryptedData = await decryptQrData({
|
||||||
token: json.token,
|
token: json.token,
|
||||||
encryptedData: json.data
|
encryptedData: json.data
|
||||||
});
|
});
|
||||||
|
|
||||||
if (decryptedData) {
|
if (decryptedData) {
|
||||||
code = decryptedData.toString();
|
decryptedOrRaw = decryptedData.toString();
|
||||||
} else {
|
} else {
|
||||||
throw new Error('解密失败');
|
throw new Error('解密失败');
|
||||||
}
|
}
|
||||||
|
} else if (json.userTicketId) {
|
||||||
|
businessType = 'ticket';
|
||||||
|
decryptedOrRaw = scanResult.trim();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 直接使用扫码结果作为核销码
|
// 直接使用扫码结果作为核销内容
|
||||||
code = scanResult.trim();
|
decryptedOrRaw = scanResult.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!code) {
|
if (!decryptedOrRaw) {
|
||||||
throw new Error('无法获取有效的核销码');
|
throw new Error('无法获取有效的核销码');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证核销码
|
if (businessType === 'ticket') {
|
||||||
const gift = await getShopGiftByCode(code);
|
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) {
|
if (!gift) {
|
||||||
throw new Error('核销码无效');
|
throw new Error('核销码无效');
|
||||||
@@ -187,7 +245,7 @@ export function useUnifiedQRScan() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
type: ScanType.VERIFICATION,
|
type: ScanType.VERIFICATION,
|
||||||
data: gift,
|
data: { businessType: 'gift', gift },
|
||||||
message: '核销成功'
|
message: '核销成功'
|
||||||
};
|
};
|
||||||
}, [isAdmin]);
|
}, [isAdmin]);
|
||||||
@@ -213,7 +271,14 @@ export function useUnifiedQRScan() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
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;
|
return result;
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
// User cancelled scanning (e.g. `scanCode:fail cancel`).
|
||||||
|
if (cancelRef.current) {
|
||||||
|
reset();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!cancelRef.current) {
|
if (!cancelRef.current) {
|
||||||
setState(UnifiedScanState.ERROR);
|
setState(UnifiedScanState.ERROR);
|
||||||
const errorMessage = err.message || '处理失败';
|
const errorMessage = err.message || '处理失败';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Taro from '@tarojs/taro';
|
|||||||
import { User } from '@/api/system/user/model';
|
import { User } from '@/api/system/user/model';
|
||||||
import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout';
|
import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout';
|
||||||
import { TenantId } from '@/config/app';
|
import { TenantId } from '@/config/app';
|
||||||
import {getStoredInviteParams, handleInviteRelation} from '@/utils/invite';
|
import { handleInviteRelation } from '@/utils/invite';
|
||||||
|
|
||||||
// 用户Hook
|
// 用户Hook
|
||||||
export const useUser = () => {
|
export const useUser = () => {
|
||||||
@@ -44,15 +44,10 @@ export const useUser = () => {
|
|||||||
reject(new Error('自动登录失败'));
|
reject(new Error('自动登录失败'));
|
||||||
}
|
}
|
||||||
}).catch(_ => {
|
}).catch(_ => {
|
||||||
// 首次注册,跳转到邀请注册页面
|
// 登录失败(通常是新用户尚未注册/未绑定手机号等)。
|
||||||
const pages = Taro.getCurrentPages();
|
// 这里不做任何“自动跳转”,避免用户点击「我的」时被强制带到分销/申请页,体验割裂。
|
||||||
const currentPage = pages[pages.length - 1];
|
// 需要登录的页面请使用 utils/auth 的 ensureLoggedIn / goToRegister 做显式跳转。
|
||||||
const inviteParams = getStoredInviteParams()
|
reject(new Error('autoLoginByOpenId failed'));
|
||||||
if (currentPage?.route !== 'dealer/apply/add' && inviteParams?.inviter) {
|
|
||||||
return Taro.navigateTo({
|
|
||||||
url: '/dealer/apply/add'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fail: reject
|
fail: reject
|
||||||
@@ -60,7 +55,11 @@ export const useUser = () => {
|
|||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('自动登录失败:', error);
|
const msg = error instanceof Error ? error.message : String(error);
|
||||||
|
// 新用户首次进入、未绑定手机号等场景属于“预期失败”,避免刷屏报错。
|
||||||
|
if (msg !== 'autoLoginByOpenId failed') {
|
||||||
|
console.error('自动登录失败:', error);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -280,11 +279,14 @@ export const useUser = () => {
|
|||||||
|
|
||||||
// 检查用户是否是管理员
|
// 检查用户是否是管理员
|
||||||
const isAdmin = () => {
|
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 = () => {
|
const isSuperAdmin = () => {
|
||||||
return user?.isSuperAdmin === true;
|
const v: any = (user as any)?.isSuperAdmin;
|
||||||
|
return v === true || v === 1 || v === '1';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取用户余额
|
// 获取用户余额
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import {pageShopUserCoupon} from "@/api/shop/shopUserCoupon";
|
|
||||||
import {pageShopGift} from "@/api/shop/shopGift";
|
|
||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {getUserInfo} from "@/api/layout";
|
import { getUserCardStats } from '@/api/user'
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
balance: number
|
balance: string
|
||||||
points: number
|
points: number
|
||||||
coupons: number
|
coupons: number
|
||||||
giftCards: number
|
giftCards: number
|
||||||
@@ -24,7 +22,7 @@ interface UseUserDataReturn {
|
|||||||
loading: boolean
|
loading: boolean
|
||||||
error: string | null
|
error: string | null
|
||||||
refresh: () => Promise<void>
|
refresh: () => Promise<void>
|
||||||
updateBalance: (newBalance: number) => void
|
updateBalance: (newBalance: string) => void
|
||||||
updatePoints: (newPoints: number) => void
|
updatePoints: (newPoints: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,18 +41,14 @@ export const useUserData = (): UseUserDataReturn => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 并发请求所有数据
|
// 聚合接口:一次请求返回余额/积分/优惠券/礼品卡统计(后端可按用户做缓存)
|
||||||
const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
|
const stats = await getUserCardStats()
|
||||||
getUserInfo(),
|
|
||||||
pageShopUserCoupon({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0}),
|
|
||||||
pageShopGift({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0})
|
|
||||||
])
|
|
||||||
|
|
||||||
const newData: UserData = {
|
const newData: UserData = {
|
||||||
balance: userDataRes?.balance || 0.00,
|
balance: stats?.balance || '0.00',
|
||||||
points: userDataRes?.points || 0,
|
points: stats?.points || 0,
|
||||||
coupons: couponsRes?.count || 0,
|
coupons: stats?.coupons || 0,
|
||||||
giftCards: giftCardsRes?.count || 0,
|
giftCards: stats?.giftCards || 0,
|
||||||
orders: {
|
orders: {
|
||||||
pending: 0,
|
pending: 0,
|
||||||
paid: 0,
|
paid: 0,
|
||||||
@@ -78,7 +72,7 @@ export const useUserData = (): UseUserDataReturn => {
|
|||||||
}, [fetchUserData])
|
}, [fetchUserData])
|
||||||
|
|
||||||
// 更新余额(本地更新,避免频繁请求)
|
// 更新余额(本地更新,避免频繁请求)
|
||||||
const updateBalance = useCallback((newBalance: number) => {
|
const updateBalance = useCallback((newBalance: string) => {
|
||||||
setData(prev => prev ? { ...prev, balance: newBalance } : null)
|
setData(prev => prev ? { ...prev, balance: newBalance } : null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
ConfigProvider
|
ConfigProvider
|
||||||
} from '@nutui/nutui-react-taro';
|
} 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 {View} from '@tarojs/components';
|
||||||
import {CartItem, useCart} from "@/hooks/useCart";
|
import {CartItem, useCart} from "@/hooks/useCart";
|
||||||
import './cart.scss';
|
import './cart.scss';
|
||||||
|
import { ensureLoggedIn } from '@/utils/auth'
|
||||||
|
|
||||||
function Cart() {
|
function Cart() {
|
||||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
|
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
|
||||||
@@ -41,7 +42,7 @@ function Cart() {
|
|||||||
|
|
||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
title: '购物车 - 时里院子市集',
|
title: '购物车 - 桂乐淘',
|
||||||
success: function () {
|
success: function () {
|
||||||
console.log('分享成功');
|
console.log('分享成功');
|
||||||
},
|
},
|
||||||
@@ -150,6 +151,9 @@ function Cart() {
|
|||||||
// 将选中的商品信息存储到本地,供结算页面使用
|
// 将选中的商品信息存储到本地,供结算页面使用
|
||||||
Taro.setStorageSync('checkout_items', JSON.stringify(selectedCartItems));
|
Taro.setStorageSync('checkout_items', JSON.stringify(selectedCartItems));
|
||||||
|
|
||||||
|
// 未登录则引导去注册/登录;登录后回到购物车结算页
|
||||||
|
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
|
||||||
|
|
||||||
// 跳转到购物车结算页面
|
// 跳转到购物车结算页面
|
||||||
Taro.navigateTo({
|
Taro.navigateTo({
|
||||||
url: '/shop/orderConfirmCart/index'
|
url: '/shop/orderConfirmCart/index'
|
||||||
@@ -171,7 +175,6 @@ function Cart() {
|
|||||||
<NavBar
|
<NavBar
|
||||||
fixed={true}
|
fixed={true}
|
||||||
style={{marginTop: `${statusBarHeight}px`}}
|
style={{marginTop: `${statusBarHeight}px`}}
|
||||||
left={<ArrowLeft onClick={() => Taro.navigateBack()}/>}
|
|
||||||
right={
|
right={
|
||||||
cartItems.length > 0 && (
|
cartItems.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
@@ -227,7 +230,6 @@ function Cart() {
|
|||||||
<NavBar
|
<NavBar
|
||||||
fixed={true}
|
fixed={true}
|
||||||
style={{marginTop: `${statusBarHeight}px`}}
|
style={{marginTop: `${statusBarHeight}px`}}
|
||||||
left={<ArrowLeft onClick={() => Taro.navigateBack()}/>}
|
|
||||||
right={
|
right={
|
||||||
cartItems.length > 0 && (
|
cartItems.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function Category() {
|
|||||||
|
|
||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
title: `${nav?.categoryName}_时里院子市集`,
|
title: `${nav?.categoryName}_桂乐淘`,
|
||||||
path: `/shop/category/index?id=${categoryId}`,
|
path: `/shop/category/index?id=${categoryId}`,
|
||||||
success: function () {
|
success: function () {
|
||||||
console.log('分享成功');
|
console.log('分享成功');
|
||||||
|
|||||||
@@ -6,52 +6,81 @@ import {Image} from '@nutui/nutui-react-taro'
|
|||||||
import {getCmsAdByCode} from "@/api/cms/cmsAd";
|
import {getCmsAdByCode} from "@/api/cms/cmsAd";
|
||||||
import navTo from "@/utils/common";
|
import navTo from "@/utils/common";
|
||||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
import Taro from '@tarojs/taro'
|
||||||
|
|
||||||
|
type AdImage = {
|
||||||
|
url?: string
|
||||||
|
path?: string
|
||||||
|
title?: string
|
||||||
|
// Compatible keys (some backends use different fields)
|
||||||
|
src?: string
|
||||||
|
imageUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAdImages(ad?: CmsAd): AdImage[] {
|
||||||
|
const list = ad?.imageList
|
||||||
|
if (Array.isArray(list) && list.length) return list as AdImage[]
|
||||||
|
|
||||||
|
// Some APIs only return `images` as a JSON string.
|
||||||
|
const raw = ad?.images
|
||||||
|
if (!raw) return []
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
return Array.isArray(parsed) ? (parsed as AdImage[]) : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumberPx(input: unknown, fallback: number) {
|
||||||
|
const n = typeof input === 'number' ? input : Number.parseInt(String(input ?? ''), 10)
|
||||||
|
return Number.isFinite(n) ? n : fallback
|
||||||
|
}
|
||||||
|
|
||||||
const MyPage = () => {
|
const MyPage = () => {
|
||||||
const [carouselData, setCarouselData] = useState<CmsAd>()
|
const [carouselData, setCarouselData] = useState<CmsAd>()
|
||||||
const [hotToday, setHotToday] = useState<CmsAd>()
|
|
||||||
const [item, setItem] = useState<CmsArticle>()
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
// const [disableSwiper, setDisableSwiper] = useState(false)
|
// const [disableSwiper, setDisableSwiper] = useState(false)
|
||||||
|
|
||||||
|
const CACHE_KEY = 'home_banner_mp-ad'
|
||||||
|
|
||||||
// 用于记录触摸开始位置
|
// 用于记录触摸开始位置
|
||||||
// const touchStartRef = useRef({x: 0, y: 0})
|
// const touchStartRef = useRef({x: 0, y: 0})
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const loadData = async () => {
|
const loadData = async (opts?: {silent?: boolean}) => {
|
||||||
|
if (!opts?.silent) setLoading(true)
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
// 只阻塞 banner 自己的数据;其他数据预热不应影响首屏展示速度
|
||||||
// 轮播图
|
const flash = await getCmsAdByCode('mp-ad')
|
||||||
const flash = await getCmsAdByCode('flash')
|
setCarouselData(flash)
|
||||||
// 今日热卖
|
void Taro.setStorage({ key: CACHE_KEY, data: flash }).catch(() => {})
|
||||||
const hotToday = await getCmsAdByCode('hot_today')
|
|
||||||
// 时里动态
|
|
||||||
const news = await pageCmsArticle({limit:1,recommend:1})
|
|
||||||
// 赋值
|
|
||||||
if(flash){
|
|
||||||
setCarouselData(flash)
|
|
||||||
}
|
|
||||||
if(hotToday){
|
|
||||||
setHotToday(hotToday)
|
|
||||||
}
|
|
||||||
if(news && news.list.length > 0){
|
|
||||||
setItem(news.list[0])
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Banner数据加载失败:', error)
|
console.error('Banner数据加载失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
if (!opts?.silent) setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 后台预热(不阻塞 banner 渲染)
|
||||||
|
void getCmsAdByCode('hot_today').catch(() => {})
|
||||||
|
void pageCmsArticle({ limit: 1, recommend: 1 }).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
const cached = Taro.getStorageSync(CACHE_KEY) as CmsAd | undefined
|
||||||
|
// 有缓存则先渲染缓存,避免首屏等待;再静默刷新
|
||||||
|
if (cached && normalizeAdImages(cached).length) {
|
||||||
|
setCarouselData(cached)
|
||||||
|
setLoading(false)
|
||||||
|
void loadData({ silent: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void loadData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 轮播图高度,默认300px
|
// 轮播图高度,默认300px
|
||||||
const carouselHeight = carouselData?.height || 300;
|
const carouselHeight = toNumberPx(carouselData?.height, 300)
|
||||||
|
const carouselImages = normalizeAdImages(carouselData)
|
||||||
|
|
||||||
// 骨架屏组件
|
// 骨架屏组件
|
||||||
const BannerSkeleton = () => (
|
const BannerSkeleton = () => (
|
||||||
@@ -100,94 +129,43 @@ const MyPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex p-2 justify-between" style={{height: `${carouselHeight}px`}}>
|
<Swiper
|
||||||
{/* 左侧轮播图区域 */}
|
defaultValue={0}
|
||||||
<View
|
height={carouselHeight}
|
||||||
style={{width: '50%', height: '100%'}}
|
indicator
|
||||||
className="banner-swiper-container"
|
autoPlay
|
||||||
>
|
duration={3000}
|
||||||
<Swiper
|
style={{
|
||||||
defaultValue={0}
|
height: `${carouselHeight}px`,
|
||||||
height={carouselHeight}
|
touchAction: 'pan-y' // 关键修改:允许垂直滑动
|
||||||
indicator
|
}}
|
||||||
autoPlay
|
disableTouch={false}
|
||||||
duration={3000}
|
direction="horizontal"
|
||||||
style={{
|
className="custom-swiper"
|
||||||
height: `${carouselHeight}px`,
|
>
|
||||||
touchAction: 'pan-y' // 关键修改:允许垂直滑动
|
{carouselImages.map((img, index) => {
|
||||||
}}
|
const src = img.url || img.src || img.imageUrl
|
||||||
disableTouch={false}
|
if (!src) return null
|
||||||
direction="horizontal"
|
return (
|
||||||
className="custom-swiper"
|
<Swiper.Item key={index} style={{ touchAction: 'pan-x pan-y' }}>
|
||||||
>
|
|
||||||
{carouselData && carouselData?.imageList?.map((img, index) => (
|
|
||||||
<Swiper.Item key={index} style={{ touchAction: 'pan-x pan-y' }}>
|
|
||||||
<Image
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
src={img.url}
|
|
||||||
mode={'scaleToFill'}
|
|
||||||
onClick={() => navTo(`${img.path}`)}
|
|
||||||
lazyLoad={false}
|
|
||||||
style={{
|
|
||||||
height: `${carouselHeight}px`,
|
|
||||||
borderRadius: '4px',
|
|
||||||
touchAction: 'manipulation' // 关键修改:优化触摸操作
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Swiper.Item>
|
|
||||||
))}
|
|
||||||
</Swiper>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 右侧上下图片区域 - 从API获取数据 */}
|
|
||||||
<View className="flex flex-col" style={{width: '50%', height: '100%'}}>
|
|
||||||
{/* 上层图片 - 使用今日热卖素材 */}
|
|
||||||
<View className={'ml-2 bg-white rounded-lg shadow-sm'}>
|
|
||||||
<View className={'px-3 my-2 font-bold text-sm'}>今日热卖</View>
|
|
||||||
<View className={'px-3 flex'} style={{
|
|
||||||
height: '110px'
|
|
||||||
}}>
|
|
||||||
{
|
|
||||||
hotToday?.imageList?.map(item => (
|
|
||||||
<View className={'item flex flex-col mr-1'} key={item.url}>
|
|
||||||
<Image
|
|
||||||
width={70}
|
|
||||||
height={70}
|
|
||||||
src={item.url}
|
|
||||||
mode={'scaleToFill'}
|
|
||||||
lazyLoad={false}
|
|
||||||
style={{
|
|
||||||
borderRadius: '4px'
|
|
||||||
}}
|
|
||||||
onClick={() => navTo('/shop/category/index?id=4424')}
|
|
||||||
/>
|
|
||||||
<View className={'text-xs py-2 text-orange-600 whitespace-nowrap'}>{item.title || '到手价¥9.9'}</View>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 下层图片 - 使用社区拼团素材 */}
|
|
||||||
<View className={'ml-2 bg-white rounded-lg mt-3 shadow-sm'}>
|
|
||||||
<View className={'px-3 my-2 font-bold text-sm'}>走进社区</View>
|
|
||||||
<View className={'rounded-lg px-3 pb-3'}>
|
|
||||||
<Image
|
<Image
|
||||||
width={'100%'}
|
width="100%"
|
||||||
height={94}
|
height="100%"
|
||||||
src={item?.image}
|
src={src}
|
||||||
mode={'scaleToFill'}
|
mode={'scaleToFill'}
|
||||||
lazyLoad={false}
|
onClick={() => (img.path ? navTo(`${img.path}`) : undefined)}
|
||||||
|
// 首张图优先加载,其余按需懒加载,避免并发图片请求拖慢首屏可见
|
||||||
|
lazyLoad={index !== 0}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: '4px'
|
height: `${carouselHeight}px`,
|
||||||
|
borderRadius: '4px',
|
||||||
|
touchAction: 'manipulation' // 关键修改:优化触摸操作
|
||||||
}}
|
}}
|
||||||
onClick={() => navTo('cms/detail/index?id=' + item?.articleId)}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</Swiper.Item>
|
||||||
</View>
|
)
|
||||||
</View>
|
})}
|
||||||
</View>
|
</Swiper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,3 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 吸顶状态下的样式 */
|
|
||||||
.nutui-sticky--fixed {
|
|
||||||
.header-bg {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,151 @@
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import {Button, Space, Sticky} from '@nutui/nutui-react-taro'
|
import {Button, Popup, Cell, CellGroup} from '@nutui/nutui-react-taro'
|
||||||
import {TriangleDown} from '@nutui/icons-react-taro'
|
// import {TriangleDown} from '@nutui/icons-react-taro'
|
||||||
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
|
import { NavBar} from '@nutui/nutui-react-taro'
|
||||||
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
||||||
import {TenantId} from "@/config/app";
|
import {TenantId, TenantName} from "@/config/app";
|
||||||
import {getOrganization} from "@/api/system/organization";
|
import {getOrganization} from "@/api/system/organization";
|
||||||
import {myUserVerify} from "@/api/system/userVerify";
|
import {myUserVerify} from "@/api/system/userVerify";
|
||||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
// import { useShopInfo } from '@/hooks/useShopInfo';
|
||||||
import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite";
|
import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite";
|
||||||
import {View,Text} from '@tarojs/components'
|
import {View,Text} from '@tarojs/components'
|
||||||
import MySearch from "./MySearch";
|
import MySearch from "./MySearch";
|
||||||
import './Header.scss';
|
import './Header.scss';
|
||||||
|
import {User} from "@/api/system/user/model";
|
||||||
|
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
||||||
|
import type {ShopStore} from "@/api/shop/shopStore/model";
|
||||||
|
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
||||||
|
|
||||||
const Header = (_: any) => {
|
const Header = (_: any) => {
|
||||||
// 使用新的useShopInfo Hook
|
// 使用新的useShopInfo Hook
|
||||||
const {
|
// const {
|
||||||
getWebsiteName,
|
// getWebsiteLogo
|
||||||
getWebsiteLogo
|
// } = useShopInfo();
|
||||||
} = useShopInfo();
|
|
||||||
|
|
||||||
const [IsLogin, setIsLogin] = useState<boolean>(true)
|
const [IsLogin, setIsLogin] = useState<boolean>(true)
|
||||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||||
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
const [userInfo] = useState<User>()
|
||||||
|
|
||||||
|
// 门店选择:用于首页展示“最近门店”,并在下单时写入订单 storeId
|
||||||
|
const [storePopupVisible, setStorePopupVisible] = useState(false)
|
||||||
|
const [stores, setStores] = useState<ShopStore[]>([])
|
||||||
|
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
|
||||||
|
const [userLocation, setUserLocation] = useState<{lng: number; lat: number} | null>(null)
|
||||||
|
|
||||||
|
const getTenantName = () => {
|
||||||
|
return userInfo?.tenantName || TenantName
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseStoreCoords = (s: ShopStore): {lng: number; lat: number} | null => {
|
||||||
|
const raw = (s.lngAndLat || s.location || '').trim()
|
||||||
|
if (!raw) return null
|
||||||
|
|
||||||
|
const parts = raw.split(/[,\s]+/).filter(Boolean)
|
||||||
|
if (parts.length < 2) return null
|
||||||
|
|
||||||
|
const a = parseFloat(parts[0])
|
||||||
|
const b = parseFloat(parts[1])
|
||||||
|
if (Number.isNaN(a) || Number.isNaN(b)) return null
|
||||||
|
|
||||||
|
// 常见格式是 "lng,lat";这里做一个简单兜底(经度范围更宽)
|
||||||
|
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90
|
||||||
|
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180
|
||||||
|
if (looksLikeLngLat) return {lng: a, lat: b}
|
||||||
|
if (looksLikeLatLng) return {lng: b, lat: a}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const distanceMeters = (a: {lng: number; lat: number}, b: {lng: number; lat: number}) => {
|
||||||
|
const toRad = (x: number) => (x * Math.PI) / 180
|
||||||
|
const R = 6371000 // meters
|
||||||
|
const dLat = toRad(b.lat - a.lat)
|
||||||
|
const dLng = toRad(b.lng - a.lng)
|
||||||
|
const lat1 = toRad(a.lat)
|
||||||
|
const lat2 = toRad(b.lat)
|
||||||
|
const sin1 = Math.sin(dLat / 2)
|
||||||
|
const sin2 = Math.sin(dLng / 2)
|
||||||
|
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2
|
||||||
|
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDistance = (meters?: number) => {
|
||||||
|
if (meters === undefined || Number.isNaN(meters)) return ''
|
||||||
|
if (meters < 1000) return `${Math.round(meters)}m`
|
||||||
|
return `${(meters / 1000).toFixed(1)}km`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStoreDistance = (s: ShopStore) => {
|
||||||
|
if (!userLocation) return undefined
|
||||||
|
const coords = parseStoreCoords(s)
|
||||||
|
if (!coords) return undefined
|
||||||
|
return distanceMeters(userLocation, coords)
|
||||||
|
}
|
||||||
|
|
||||||
|
const initStoreSelection = async () => {
|
||||||
|
// 先读取本地已选门店,避免页面首屏抖动
|
||||||
|
const stored = getSelectedStoreFromStorage()
|
||||||
|
if (stored?.id) {
|
||||||
|
setSelectedStore(stored)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拉取门店列表(失败时允许用户手动重试/继续使用本地门店)
|
||||||
|
let list: ShopStore[] = []
|
||||||
|
try {
|
||||||
|
list = await listShopStore()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取门店列表失败:', e)
|
||||||
|
list = []
|
||||||
|
}
|
||||||
|
const usable = (list || []).filter(s => s?.isDelete !== 1)
|
||||||
|
setStores(usable)
|
||||||
|
|
||||||
|
// 尝试获取定位,用于计算最近门店
|
||||||
|
let loc: {lng: number; lat: number} | null = null
|
||||||
|
try {
|
||||||
|
const r = await Taro.getLocation({type: 'gcj02'})
|
||||||
|
loc = {lng: r.longitude, lat: r.latitude}
|
||||||
|
} catch (e) {
|
||||||
|
// 不强制定位授权;无定位时仍允许用户手动选择
|
||||||
|
console.warn('获取定位失败,将不显示最近门店距离:', e)
|
||||||
|
}
|
||||||
|
setUserLocation(loc)
|
||||||
|
|
||||||
|
const ensureStoreDetail = async (s: ShopStore) => {
|
||||||
|
if (!s?.id) return s
|
||||||
|
// 如果后端已经返回默认仓库等字段,就不额外请求
|
||||||
|
if (s.warehouseId) return s
|
||||||
|
try {
|
||||||
|
const full = await getShopStore(s.id)
|
||||||
|
return full || s
|
||||||
|
} catch (_e) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若用户没有选过门店,则自动选择最近门店(或第一个)
|
||||||
|
const alreadySelected = stored?.id
|
||||||
|
if (alreadySelected || usable.length === 0) return
|
||||||
|
|
||||||
|
let autoPick: ShopStore | undefined
|
||||||
|
if (loc) {
|
||||||
|
autoPick = [...usable]
|
||||||
|
.map(s => {
|
||||||
|
const coords = parseStoreCoords(s)
|
||||||
|
const d = coords ? distanceMeters(loc, coords) : undefined
|
||||||
|
return {s, d}
|
||||||
|
})
|
||||||
|
.sort((x, y) => (x.d ?? Number.POSITIVE_INFINITY) - (y.d ?? Number.POSITIVE_INFINITY))[0]?.s
|
||||||
|
} else {
|
||||||
|
autoPick = usable[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoPick?.id) {
|
||||||
|
const full = await ensureStoreDetail(autoPick)
|
||||||
|
setSelectedStore(full)
|
||||||
|
saveSelectedStoreToStorage(full)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = async () => {
|
||||||
Taro.getSystemInfo({
|
Taro.getSystemInfo({
|
||||||
@@ -109,7 +232,7 @@ const Header = (_: any) => {
|
|||||||
success: function () {
|
success: function () {
|
||||||
if (code) {
|
if (code) {
|
||||||
Taro.request({
|
Taro.request({
|
||||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
code,
|
code,
|
||||||
@@ -167,12 +290,6 @@ const Header = (_: any) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理粘性布局状态变化
|
|
||||||
const onStickyChange = (isSticky: boolean) => {
|
|
||||||
setStickyStatus(isSticky)
|
|
||||||
console.log('Header 粘性状态:', isSticky ? '已固定' : '取消固定')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取小程序系统信息
|
// 获取小程序系统信息
|
||||||
// const getSystemInfo = () => {
|
// const getSystemInfo = () => {
|
||||||
// const systemInfo = Taro.getSystemInfoSync()
|
// const systemInfo = Taro.getSystemInfoSync()
|
||||||
@@ -182,61 +299,119 @@ const Header = (_: any) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reload().then()
|
reload().then()
|
||||||
|
initStoreSelection().then()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sticky
|
<View
|
||||||
threshold={0}
|
className={'header-bg'}
|
||||||
onChange={onStickyChange}
|
|
||||||
style={{
|
style={{
|
||||||
zIndex: 1000,
|
height: '180px',
|
||||||
backgroundColor: stickyStatus ? '#03605c' : 'transparent',
|
paddingBottom: '12px',
|
||||||
transition: 'background-color 0.3s ease',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View className={'header-bg'} style={{
|
<MySearch statusBarHeight={statusBarHeight} />
|
||||||
height: !stickyStatus ? '180px' : `${(statusBarHeight || 0) + 44}px`,
|
</View>
|
||||||
paddingBottom: !stickyStatus ? '12px' : '0px'
|
|
||||||
}}>
|
<NavBar
|
||||||
{/* 只在非吸顶状态下显示搜索框 */}
|
style={{
|
||||||
{!stickyStatus && <MySearch statusBarHeight={statusBarHeight} />}
|
marginTop: `${statusBarHeight}px`,
|
||||||
|
marginBottom: '0px',
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
}}
|
||||||
|
onBackClick={() => {
|
||||||
|
}}
|
||||||
|
// left={
|
||||||
|
// <View
|
||||||
|
// style={{display: 'flex', alignItems: 'center', gap: '8px'}}
|
||||||
|
// onClick={() => setStorePopupVisible(true)}
|
||||||
|
// >
|
||||||
|
// <Avatar
|
||||||
|
// size="22"
|
||||||
|
// src={getWebsiteLogo()}
|
||||||
|
// />
|
||||||
|
// <Text className={'text-white'}>
|
||||||
|
// {selectedStore?.name || '请选择门店'}
|
||||||
|
// </Text>
|
||||||
|
// <TriangleDown className={'text-white'} size={9}/>
|
||||||
|
// </View>
|
||||||
|
// }
|
||||||
|
right={
|
||||||
|
!IsLogin ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
fill="none"
|
||||||
|
style={{color: '#ffffff'}}
|
||||||
|
open-type="getPhoneNumber"
|
||||||
|
onGetPhoneNumber={handleGetPhoneNumber}
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text className={'text-white'}>{getTenantName()}</Text>
|
||||||
|
</NavBar>
|
||||||
|
|
||||||
|
<Popup
|
||||||
|
visible={storePopupVisible}
|
||||||
|
position="bottom"
|
||||||
|
style={{height: '70vh'}}
|
||||||
|
onClose={() => setStorePopupVisible(false)}
|
||||||
|
>
|
||||||
|
<View className="p-4">
|
||||||
|
<View className="flex justify-between items-center mb-3">
|
||||||
|
<Text className="text-base font-medium">选择门店</Text>
|
||||||
|
<Text
|
||||||
|
className="text-sm text-gray-500"
|
||||||
|
onClick={() => setStorePopupVisible(false)}
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="text-xs text-gray-500 mb-2">
|
||||||
|
{userLocation ? '已获取定位,按距离排序' : '未获取定位,可手动选择门店'}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<CellGroup>
|
||||||
|
{[...stores]
|
||||||
|
.sort((a, b) => (getStoreDistance(a) ?? Number.POSITIVE_INFINITY) - (getStoreDistance(b) ?? Number.POSITIVE_INFINITY))
|
||||||
|
.map((s) => {
|
||||||
|
const d = getStoreDistance(s)
|
||||||
|
const isActive = !!selectedStore?.id && selectedStore.id === s.id
|
||||||
|
return (
|
||||||
|
<Cell
|
||||||
|
key={s.id}
|
||||||
|
title={
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<Text className={isActive ? 'text-green-600' : ''}>{s.name || `门店${s.id}`}</Text>
|
||||||
|
{d !== undefined && <Text className="text-xs text-gray-500">{formatDistance(d)}</Text>}
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
description={s.address || ''}
|
||||||
|
onClick={async () => {
|
||||||
|
let storeToSave = s
|
||||||
|
if (s?.id) {
|
||||||
|
try {
|
||||||
|
const full = await getShopStore(s.id)
|
||||||
|
if (full) storeToSave = full
|
||||||
|
} catch (_e) {
|
||||||
|
// keep base item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSelectedStore(storeToSave)
|
||||||
|
saveSelectedStoreToStorage(storeToSave)
|
||||||
|
setStorePopupVisible(false)
|
||||||
|
Taro.showToast({title: '门店已切换', icon: 'success'})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</CellGroup>
|
||||||
</View>
|
</View>
|
||||||
<NavBar
|
</Popup>
|
||||||
style={{
|
|
||||||
marginTop: `${statusBarHeight}px`,
|
|
||||||
marginBottom: '0px',
|
|
||||||
backgroundColor: 'transparent'
|
|
||||||
}}
|
|
||||||
onBackClick={() => {
|
|
||||||
}}
|
|
||||||
left={
|
|
||||||
!IsLogin ? (
|
|
||||||
<View style={{display: 'flex', alignItems: 'center'}}>
|
|
||||||
<Button style={{color: '#ffffff'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
|
|
||||||
<Space>
|
|
||||||
<Avatar
|
|
||||||
size="22"
|
|
||||||
src={getWebsiteLogo()}
|
|
||||||
/>
|
|
||||||
<Text style={{color: '#ffffff'}}>{getWebsiteName()}</Text>
|
|
||||||
<TriangleDown size={9} className={'text-white'}/>
|
|
||||||
</Space>
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
|
|
||||||
<Avatar
|
|
||||||
size="22"
|
|
||||||
src={getWebsiteLogo()}
|
|
||||||
/>
|
|
||||||
<Text className={'text-white'}>{getWebsiteName()}</Text>
|
|
||||||
<TriangleDown className={'text-white'} size={9}/>
|
|
||||||
</View>
|
|
||||||
)}>
|
|
||||||
{/*<QRLoginButton />*/}
|
|
||||||
</NavBar>
|
|
||||||
</Sticky>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const Login = (props: LoginProps) => {
|
|||||||
success: function () {
|
success: function () {
|
||||||
if (code) {
|
if (code) {
|
||||||
Taro.request({
|
Taro.request({
|
||||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
code,
|
code,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function MySearch(props: any) {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'z-50 left-0 w-full'}>
|
<div className={'z-50 left-0 w-full hidden'}>
|
||||||
<div className={'px-2'}>
|
<div className={'px-2'}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -4,6 +4,376 @@ page {
|
|||||||
background: linear-gradient(to bottom, #e9fff2, #ffffff);
|
background: linear-gradient(to bottom, #e9fff2, #ffffff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-page {
|
||||||
|
padding: 24rpx 24rpx calc(32rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: linear-gradient(180deg, #bfefff 0%, #eafaff 40%, #fff7ec 100%);
|
||||||
|
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(360rpx 240rpx at 18% 16%, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)),
|
||||||
|
radial-gradient(320rpx 220rpx at 84% 18%, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0)),
|
||||||
|
linear-gradient(180deg, rgba(0, 207, 255, 0.12), rgba(0, 0, 0, 0));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18rpx;
|
||||||
|
padding: 26rpx 24rpx 28rpx;
|
||||||
|
min-height: 320rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__topRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__brand {
|
||||||
|
flex: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8rpx 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 214, 84, 0.92);
|
||||||
|
color: #2a2a2a;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__brandText {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__tag {
|
||||||
|
flex: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background: linear-gradient(90deg, #22d64a 0%, #7df4b0 100%);
|
||||||
|
box-shadow: 0 14rpx 24rpx rgba(36, 202, 148, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__tagText {
|
||||||
|
font-size: 56rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__date {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10rpx 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__dateText {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__headline {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__headlineText {
|
||||||
|
display: block;
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #0b0b0b;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__right {
|
||||||
|
width: 200rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__bottle {
|
||||||
|
position: relative;
|
||||||
|
width: 190rpx;
|
||||||
|
height: 250rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background:
|
||||||
|
radial-gradient(240rpx 360rpx at 60% 30%, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.18)),
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.1));
|
||||||
|
border: 2rpx solid rgba(255, 255, 255, 0.65);
|
||||||
|
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__bottleCap {
|
||||||
|
position: absolute;
|
||||||
|
top: 14rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 88rpx;
|
||||||
|
height: 26rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: linear-gradient(180deg, #d7e6f3, #b0cadd);
|
||||||
|
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__bottleLabel {
|
||||||
|
position: absolute;
|
||||||
|
left: 18rpx;
|
||||||
|
right: 18rpx;
|
||||||
|
bottom: 30rpx;
|
||||||
|
padding: 12rpx 12rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background: linear-gradient(90deg, rgba(0, 150, 255, 0.18), rgba(0, 255, 210, 0.18));
|
||||||
|
border: 2rpx solid rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero__bottleLabelText {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: rgba(0, 80, 140, 0.95);
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-card {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-card__head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 18rpx 20rpx;
|
||||||
|
background: linear-gradient(90deg, #22d64a 0%, #2fa560 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-card__title {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-card__count {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-card__countNum {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-card__body {
|
||||||
|
padding: 20rpx 10rpx 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-grid {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-grid__item {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-grid__icon {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
color: #20c26a;
|
||||||
|
border: 2rpx solid rgba(32, 194, 106, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-grid__text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-tabs {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-tabs__inner {
|
||||||
|
display: flex;
|
||||||
|
gap: 18rpx;
|
||||||
|
padding: 0 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-tabs__item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-tabs__item--active {
|
||||||
|
background: rgba(32, 194, 106, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-tabs__itemText {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2a2a2a;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-tabs__item--active .home-tabs__itemText {
|
||||||
|
color: #16b65a;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-grid {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card {
|
||||||
|
border-radius: 22rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__imgWrap {
|
||||||
|
padding: 18rpx 18rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__img {
|
||||||
|
width: 100%;
|
||||||
|
height: 280rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__body {
|
||||||
|
padding: 18rpx 18rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__title {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1c1c1c;
|
||||||
|
min-height: 72rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__meta {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__sold {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9a9a9a;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__price {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4rpx;
|
||||||
|
color: #27c86b;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__priceUnit {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__priceValue {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__actions {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__btn--ghost {
|
||||||
|
border: 2rpx solid rgba(32, 194, 106, 0.7);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__btn--primary {
|
||||||
|
background: linear-gradient(90deg, #24d34c 0%, #6df09a 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__btnText {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #18b85a;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__btnText--primary {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.buy-btn{
|
.buy-btn{
|
||||||
height: 70px;
|
height: 70px;
|
||||||
background: linear-gradient(to bottom, #1cd98a, #24ca94);
|
background: linear-gradient(to bottom, #1cd98a, #24ca94);
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
import Header from './Header';
|
// import Header from './Header'
|
||||||
import BestSellers from './BestSellers';
|
import Banner from './Banner'
|
||||||
import Taro from '@tarojs/taro';
|
import Taro, { useDidShow, useShareAppMessage } from '@tarojs/taro'
|
||||||
import {useShareAppMessage} from "@tarojs/taro"
|
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||||
import {getShopInfo} from "@/api/layout";
|
import { Cart, Gift, Ticket, Agenda } from '@nutui/icons-react-taro'
|
||||||
import Menu from "./Menu";
|
import { getShopInfo } from '@/api/layout'
|
||||||
import Banner from "./Banner";
|
import { checkAndHandleInviteRelation, hasPendingInvite } from '@/utils/invite'
|
||||||
import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite";
|
import { pageShopGoods } from '@/api/shop/shopGoods'
|
||||||
|
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'
|
||||||
|
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
|
||||||
|
import { ensureLoggedIn } from '@/utils/auth'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
// 吸顶状态
|
const [activeTabKey, setActiveTabKey] = useState('recommend')
|
||||||
// const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
const [goodsList, setGoodsList] = useState<ShopGoods[]>([])
|
||||||
// Tabs粘性状态
|
const [ticketTotal, setTicketTotal] = useState(0)
|
||||||
const [_, setTabsStickyStatus] = useState<boolean>(false)
|
|
||||||
|
|
||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
// 获取当前用户ID,用于生成邀请链接
|
// 获取当前用户ID,用于生成邀请链接
|
||||||
const userId = Taro.getStorageSync('UserId');
|
const userId = Taro.getStorageSync('UserId');
|
||||||
|
const user = Taro.getStorageSync('User') || {};
|
||||||
|
const nickname =
|
||||||
|
(user && (user.nickname || user.realName || user.username)) || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: '🏠 首页 🏠',
|
title: (nickname || '') + '超值推荐',
|
||||||
path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`,
|
path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`,
|
||||||
success: function () {
|
success: function () {
|
||||||
console.log('首页分享成功');
|
console.log('首页分享成功');
|
||||||
@@ -85,14 +90,30 @@ function Home() {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// 处理Tabs粘性状态变化
|
// 处理Tabs粘性状态变化
|
||||||
const handleTabsStickyChange = (isSticky: boolean) => {
|
// const handleTabsStickyChange = (isSticky: boolean) => {}
|
||||||
setTabsStickyStatus(isSticky)
|
|
||||||
}
|
|
||||||
|
|
||||||
const reload = () => {
|
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(() => {
|
useEffect(() => {
|
||||||
// 获取站点信息
|
// 获取站点信息
|
||||||
getShopInfo().then(() => {
|
getShopInfo().then(() => {
|
||||||
@@ -135,7 +156,6 @@ function Home() {
|
|||||||
if (res.authSetting['scope.userInfo']) {
|
if (res.authSetting['scope.userInfo']) {
|
||||||
// 用户已经授权过,可以直接获取用户信息
|
// 用户已经授权过,可以直接获取用户信息
|
||||||
console.log('用户已经授权过,可以直接获取用户信息')
|
console.log('用户已经授权过,可以直接获取用户信息')
|
||||||
reload();
|
|
||||||
} else {
|
} else {
|
||||||
// 用户未授权,需要弹出授权窗口
|
// 用户未授权,需要弹出授权窗口
|
||||||
console.log('用户未授权,需要弹出授权窗口')
|
console.log('用户未授权,需要弹出授权窗口')
|
||||||
@@ -147,21 +167,196 @@ function Home() {
|
|||||||
Taro.getUserInfo({
|
Taro.getUserInfo({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
const avatar = res.userInfo.avatarUrl;
|
const avatar = res.userInfo.avatarUrl;
|
||||||
|
// Keep WeChat display name in storage so share title can use it.
|
||||||
console.log(avatar, 'avatarUrl')
|
console.log(avatar, 'avatarUrl')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const tabs = useMemo<
|
||||||
|
Array<{ key: string; title: string; params: Partial<ShopGoodsParam> }>
|
||||||
|
>(
|
||||||
|
() => [
|
||||||
|
{ key: 'recommend', title: '推荐', params: { recommend: 1 } },
|
||||||
|
{ key: '4476', title: '桶装水', params: { categoryId: 4476 } },
|
||||||
|
{ key: '4556', title: '水票套餐', params: { categoryId: 4556 } },
|
||||||
|
// { key: '4557', title: '购机套餐', params: { categoryId: 4557 } },
|
||||||
|
// { key: '4477', title: '饮水设备', params: { categoryId: 4477 } },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const tab = tabs.find((t) => t.key === activeTabKey) || tabs[0]
|
||||||
|
if (!tab) return
|
||||||
|
|
||||||
|
pageShopGoods({ ...tab.params, status: 0 })
|
||||||
|
.then((res) => setGoodsList((res?.list || []).filter((g) => g?.status === 0)))
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('首页商品列表加载失败:', err)
|
||||||
|
setGoodsList([])
|
||||||
|
})
|
||||||
|
}, [activeTabKey, tabs])
|
||||||
|
|
||||||
|
const shortcuts = useMemo<
|
||||||
|
Array<{ key: string; title: string; icon: ReactNode; onClick: () => void }>
|
||||||
|
>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: 'ticket',
|
||||||
|
title: '我的水票',
|
||||||
|
icon: <Ticket size={30} />,
|
||||||
|
onClick: () => {
|
||||||
|
if (!ensureLoggedIn('/user/ticket/index')) return
|
||||||
|
Taro.navigateTo({ url: '/user/ticket/index' })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'order',
|
||||||
|
title: '立即送水',
|
||||||
|
icon: <Cart size={30} />,
|
||||||
|
onClick: () => {
|
||||||
|
if (!ensureLoggedIn('/user/ticket/use?goodsId=10074')) return
|
||||||
|
Taro.navigateTo({ url: '/user/ticket/use?goodsId=10074' })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'order',
|
||||||
|
title: '送水订单',
|
||||||
|
icon: <Agenda size={30} />,
|
||||||
|
onClick: () => {
|
||||||
|
if (!ensureLoggedIn('/user/ticket/index')) return
|
||||||
|
Taro.navigateTo({ url: '/user/ticket/index' })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'invite',
|
||||||
|
title: '邀请有礼',
|
||||||
|
icon: <Gift size={30} />,
|
||||||
|
onClick: () => {
|
||||||
|
if (!ensureLoggedIn('/dealer/qrcode/index')) return
|
||||||
|
Taro.navigateTo({ url: '/dealer/qrcode/index' })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// key: 'coupon',
|
||||||
|
// title: '领券中心',
|
||||||
|
// icon: <Coupon size={30} />,
|
||||||
|
// onClick: () => Taro.navigateTo({ url: '/coupon/index' }),
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const visibleGoods = useMemo(() => {
|
||||||
|
// 先按效果图展示两列卡片,数据不够时也保持布局稳定
|
||||||
|
const list = goodsList || []
|
||||||
|
if (list.length <= 6) return list
|
||||||
|
return list.slice(0, 6)
|
||||||
|
}, [goodsList])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Header区域 - 现在由Header组件内部处理吸顶逻辑 */}
|
{/* Header区域 */}
|
||||||
<Header />
|
{/*<Header />*/}
|
||||||
|
|
||||||
<div className={'flex flex-col mt-12'}>
|
<View className="home-page">
|
||||||
<Menu/>
|
{/* 顶部活动主视觉:使用 Banner 组件 */}
|
||||||
<Banner/>
|
<Banner />
|
||||||
<BestSellers onStickyChange={handleTabsStickyChange}/>
|
|
||||||
</div>
|
{/* 电子水票 */}
|
||||||
|
<View className="ticket-card">
|
||||||
|
<View className="ticket-card__head">
|
||||||
|
<Text className="ticket-card__title">电子水票</Text>
|
||||||
|
<Text className="ticket-card__count">
|
||||||
|
您还有 <Text className="ticket-card__countNum">{ticketTotal}</Text> 张水票
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="ticket-card__body">
|
||||||
|
<View className="shortcut-grid">
|
||||||
|
{shortcuts.map((item) => (
|
||||||
|
<View
|
||||||
|
key={item.key}
|
||||||
|
className="shortcut-grid__item"
|
||||||
|
onClick={item.onClick}
|
||||||
|
>
|
||||||
|
<View className="shortcut-grid__icon">{item.icon}</View>
|
||||||
|
<Text className="shortcut-grid__text">{item.title}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 分类Tabs */}
|
||||||
|
<ScrollView className="home-tabs" scrollX enableFlex>
|
||||||
|
<View className="home-tabs__inner">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const active = tab.key === activeTabKey
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={tab.key}
|
||||||
|
className={`home-tabs__item ${active ? 'home-tabs__item--active' : ''}`}
|
||||||
|
onClick={() => setActiveTabKey(tab.key)}
|
||||||
|
>
|
||||||
|
<Text className="home-tabs__itemText">{tab.title}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* 商品列表 */}
|
||||||
|
<View className="goods-grid">
|
||||||
|
{visibleGoods.map((item) => (
|
||||||
|
<View key={item.goodsId} className="goods-card">
|
||||||
|
<View className="goods-card__imgWrap">
|
||||||
|
<Image
|
||||||
|
className="goods-card__img"
|
||||||
|
src={item.image || ''}
|
||||||
|
mode="aspectFill"
|
||||||
|
lazyLoad={false}
|
||||||
|
onClick={() =>
|
||||||
|
Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="goods-card__body">
|
||||||
|
<Text className="goods-card__title">{item.name}</Text>
|
||||||
|
<View className="goods-card__meta">
|
||||||
|
<Text className="goods-card__sold">已购:{item.sales || 0}人</Text>
|
||||||
|
<View className="goods-card__price">
|
||||||
|
<Text className="goods-card__priceUnit">¥</Text>
|
||||||
|
<Text className="goods-card__priceValue">{item.price}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="goods-card__actions">
|
||||||
|
<View
|
||||||
|
className="goods-card__btn goods-card__btn--ghost"
|
||||||
|
onClick={() => {
|
||||||
|
if (!ensureLoggedIn('/shop/orderConfirm/index?goodsId=10074')) return
|
||||||
|
Taro.navigateTo({ url: '/shop/orderConfirm/index?goodsId=10074' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className="goods-card__btnText">买水票更优惠</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className="goods-card__btn goods-card__btn--primary"
|
||||||
|
onClick={() =>
|
||||||
|
Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text className="goods-card__btnText goods-card__btnText--primary">立即购买</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import {useUser} from '@/hooks/useUser'
|
|||||||
import {useDealerUser} from "@/hooks/useDealerUser";
|
import {useDealerUser} from "@/hooks/useDealerUser";
|
||||||
import {useThemeStyles} from "@/hooks/useTheme";
|
import {useThemeStyles} from "@/hooks/useTheme";
|
||||||
import { useConfig } from "@/hooks/useConfig"; // 使用新的自定义Hook
|
import { useConfig } from "@/hooks/useConfig"; // 使用新的自定义Hook
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
|
||||||
const IsDealer = () => {
|
const IsDealer = () => {
|
||||||
const themeStyles = useThemeStyles();
|
const themeStyles = useThemeStyles();
|
||||||
const { config } = useConfig(); // 使用新的Hook
|
const { config } = useConfig(); // 使用新的Hook
|
||||||
const {isSuperAdmin} = useUser();
|
const {isSuperAdmin} = useUser();
|
||||||
const {dealerUser} = useDealerUser()
|
const {dealerUser, loading: dealerLoading} = useDealerUser()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理中心
|
* 管理中心
|
||||||
@@ -51,7 +52,7 @@ const IsDealer = () => {
|
|||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||||
<Reward className={'text-orange-100 '} size={16}/>
|
<Reward className={'text-orange-100 '} size={16}/>
|
||||||
<Text style={{fontSize: '16px'}}
|
<Text style={{fontSize: '16px'}}
|
||||||
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '入驻申请'}</Text>
|
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '桂乐淘分享中心'}</Text>
|
||||||
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
|
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
@@ -75,12 +76,18 @@ const IsDealer = () => {
|
|||||||
title={
|
title={
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||||
<Reward className={'text-orange-100 '} size={16}/>
|
<Reward className={'text-orange-100 '} size={16}/>
|
||||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '开通VIP'}</Text>
|
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '桂乐淘分享中心'}</Text>
|
||||||
<Text className={'text-white opacity-80 pl-3'}>{config?.vipComments || '享优惠'}</Text>
|
<Text className={'text-white opacity-80 pl-3'}>{config?.vipComments || ''}</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||||
onClick={() => navTo('/dealer/apply/add', true)}
|
onClick={() => {
|
||||||
|
if (dealerLoading) {
|
||||||
|
Taro.showToast({ title: '正在加载信息,请稍等...', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navTo('/dealer/apply/add', true)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -8,33 +8,94 @@ import navTo from "@/utils/common";
|
|||||||
import {TenantId} from "@/config/app";
|
import {TenantId} from "@/config/app";
|
||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
import {useUserData} from "@/hooks/useUserData";
|
import {useUserData} from "@/hooks/useUserData";
|
||||||
import {getStoredInviteParams} from "@/utils/invite";
|
import {checkAndHandleInviteRelation, getStoredInviteParams, hasPendingInvite} from "@/utils/invite";
|
||||||
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
||||||
import {useThemeStyles} from "@/hooks/useTheme";
|
import {useThemeStyles} from "@/hooks/useTheme";
|
||||||
|
import {getRootDomain} from "@/utils/domain";
|
||||||
|
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
|
||||||
|
import { saveStorageByLoginUser } from '@/utils/server'
|
||||||
|
|
||||||
const UserCard = forwardRef<any, any>((_, ref) => {
|
const UserCard = forwardRef<any, any>((_, ref) => {
|
||||||
const {data, refresh} = useUserData()
|
const {data, refresh} = useUserData()
|
||||||
const {getDisplayName, getRoleName} = useUser();
|
const {loadUserFromStorage} = useUser();
|
||||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||||
const [userInfo, setUserInfo] = useState<User>()
|
const [userInfo, setUserInfo] = useState<User>()
|
||||||
|
const [ticketTotal, setTicketTotal] = useState<number>(0)
|
||||||
|
|
||||||
const themeStyles = useThemeStyles();
|
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()
|
await refresh()
|
||||||
Taro.showToast({
|
reloadTicketTotal()
|
||||||
title: '刷新成功',
|
if (showToast) {
|
||||||
icon: 'success'
|
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, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
handleRefresh
|
handleRefresh: async () => {
|
||||||
|
await reloadUserInfo()
|
||||||
|
await reloadStats(true)
|
||||||
|
},
|
||||||
|
reloadStats,
|
||||||
|
reloadUserInfo
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 独立于用户信息授权:只要有登录 token,就可以拉取水票总数
|
||||||
|
reloadTicketTotal()
|
||||||
|
|
||||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||||
Taro.getSetting({
|
Taro.getSetting({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
@@ -51,6 +112,23 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const reloadTicketTotal = () => {
|
||||||
|
const token = Taro.getStorageSync('access_token')
|
||||||
|
const userIdRaw = Taro.getStorageSync('UserId')
|
||||||
|
const userId = Number(userIdRaw)
|
||||||
|
const hasUserId = Number.isFinite(userId) && userId > 0
|
||||||
|
if (!token && !hasUserId) {
|
||||||
|
setTicketTotal(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getMyGltUserTicketTotal(hasUserId ? userId : undefined)
|
||||||
|
.then((total) => setTicketTotal(typeof total === 'number' ? total : 0))
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('个人中心水票总数加载失败:', err)
|
||||||
|
setTicketTotal(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
Taro.getUserInfo({
|
Taro.getUserInfo({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
@@ -60,25 +138,15 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
nickname: res.userInfo.nickName,
|
nickname: res.userInfo.nickName,
|
||||||
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
||||||
})
|
})
|
||||||
getUserInfo().then((data) => {
|
reloadUserInfo()
|
||||||
if (data) {
|
.then(() => {
|
||||||
setUserInfo(data)
|
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
|
||||||
setIsLogin(true);
|
refresh().then()
|
||||||
Taro.setStorageSync('UserId', data.userId)
|
reloadTicketTotal()
|
||||||
|
})
|
||||||
// 获取openId
|
.catch(() => {
|
||||||
if (!data.openid) {
|
console.log('未登录')
|
||||||
Taro.login({
|
})
|
||||||
success: (res) => {
|
|
||||||
getWxOpenId({code: res.code}).then(() => {
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
console.log('未登录')
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -133,7 +201,7 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
success: function () {
|
success: function () {
|
||||||
if (code) {
|
if (code) {
|
||||||
Taro.request({
|
Taro.request({
|
||||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
code,
|
code,
|
||||||
@@ -158,10 +226,19 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 登录成功
|
// 登录成功
|
||||||
Taro.setStorageSync('access_token', res.data.data.access_token)
|
saveStorageByLoginUser(res.data.data.access_token, res.data.data.user)
|
||||||
Taro.setStorageSync('UserId', res.data.data.user.userId)
|
|
||||||
setUserInfo(res.data.data.user)
|
setUserInfo(res.data.data.user)
|
||||||
setIsLogin(true)
|
setIsLogin(true)
|
||||||
|
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
|
||||||
|
refresh().then()
|
||||||
|
reloadTicketTotal()
|
||||||
|
|
||||||
|
// 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定
|
||||||
|
if (hasPendingInvite()) {
|
||||||
|
checkAndHandleInviteRelation().catch((e) => {
|
||||||
|
console.error('个人中心登录后处理邀请关系失败:', e)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -189,7 +266,9 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
/>
|
/>
|
||||||
<View className={'flex flex-col'}>
|
<View className={'flex flex-col'}>
|
||||||
<Text style={{color: '#ffffff'}}>{getDisplayName() || '点击登录'}</Text>
|
<Text style={{color: '#ffffff'}}>{getDisplayName() || '点击登录'}</Text>
|
||||||
<View><Tag type="success">{getRoleName()}</Tag></View>
|
{getRootDomain() && (
|
||||||
|
<View><Tag type="success">{getRoleName()}</Tag></View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -209,33 +288,62 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Space style={{
|
{/*统一扫码入口 - 仅管理员可见*/}
|
||||||
marginTop: '30px',
|
{canShowScanButton && (
|
||||||
marginRight: '10px'
|
<Space style={{
|
||||||
}}>
|
marginTop: '30px',
|
||||||
{/*统一扫码入口 - 支持登录和核销*/}
|
marginRight: '10px'
|
||||||
<UnifiedQRButton
|
}}>
|
||||||
text="扫一扫"
|
<UnifiedQRButton
|
||||||
size="small"
|
text="扫一扫"
|
||||||
onSuccess={(result) => {
|
size="small"
|
||||||
console.log('统一扫码成功:', result);
|
onSuccess={(result) => {
|
||||||
// 根据扫码类型给出不同的提示
|
console.log('统一扫码成功:', result);
|
||||||
if (result.type === 'verification') {
|
// 根据扫码类型给出不同的提示
|
||||||
// 核销成功,可以显示更多信息或跳转到详情页
|
if (result.type === 'verification') {
|
||||||
Taro.showModal({
|
const businessType = result?.data?.businessType;
|
||||||
title: '核销成功',
|
if (businessType === 'gift' && result?.data?.gift) {
|
||||||
content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}`
|
const gift = result.data.gift;
|
||||||
});
|
Taro.showModal({
|
||||||
}
|
title: '核销成功',
|
||||||
}}
|
content: `已成功核销:${gift.goodsName || gift.name || '礼品'},面值¥${gift.faceValue}`
|
||||||
onError={(error) => {
|
});
|
||||||
console.error('统一扫码失败:', error);
|
return;
|
||||||
}}
|
}
|
||||||
/>
|
if (businessType === 'ticket' && result?.data?.ticket) {
|
||||||
</Space>
|
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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View className={'py-2'}>
|
<View className={'py-2'}>
|
||||||
<View className={'flex justify-around mt-1'}>
|
<View className={'flex justify-around mt-1'}>
|
||||||
|
<View className={'item flex justify-center flex-col items-center'}
|
||||||
|
onClick={() => navTo('/user/ticket/index', true)}>
|
||||||
|
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>水票</Text>
|
||||||
|
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{ticketTotal}</Text>
|
||||||
|
</View>
|
||||||
|
<View className={'item flex justify-center flex-col items-center'}
|
||||||
|
onClick={() => navTo('/user/coupon/index', true)}>
|
||||||
|
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>优惠券</Text>
|
||||||
|
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>
|
||||||
|
</View>
|
||||||
<View className={'item flex justify-center flex-col items-center'}
|
<View className={'item flex justify-center flex-col items-center'}
|
||||||
onClick={() => navTo('/user/wallet/wallet', true)}>
|
onClick={() => navTo('/user/wallet/wallet', true)}>
|
||||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>余额</Text>
|
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>余额</Text>
|
||||||
@@ -245,16 +353,6 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>积分</Text>
|
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>积分</Text>
|
||||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.points || 0}</Text>
|
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.points || 0}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className={'item flex justify-center flex-col items-center'}
|
|
||||||
onClick={() => navTo('/user/coupon/index', true)}>
|
|
||||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>优惠券</Text>
|
|
||||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>
|
|
||||||
</View>
|
|
||||||
<View className={'item flex justify-center flex-col items-center'}
|
|
||||||
onClick={() => navTo('/user/gift/index', true)}>
|
|
||||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>礼品卡</Text>
|
|
||||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.giftCards || 0}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const UserCell = () => {
|
|||||||
title={
|
title={
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||||
<Location size={16}/>
|
<Location size={16}/>
|
||||||
<Text className={'pl-3 text-sm'}>收货地址</Text>
|
<Text className={'pl-3 text-sm'}>配送地址</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
align="center"
|
align="center"
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ import {
|
|||||||
People,
|
People,
|
||||||
// AfterSaleService,
|
// AfterSaleService,
|
||||||
Logout,
|
Logout,
|
||||||
ShoppingAdd,
|
Shop,
|
||||||
|
Jdl,
|
||||||
Service
|
Service
|
||||||
} from '@nutui/icons-react-taro'
|
} from '@nutui/icons-react-taro'
|
||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
|
|
||||||
const UserCell = () => {
|
const UserCell = () => {
|
||||||
const {logoutUser} = useUser();
|
const {logoutUser, hasRole} = useUser();
|
||||||
|
|
||||||
const onLogout = () => {
|
const onLogout = () => {
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
@@ -49,10 +50,49 @@ const UserCell = () => {
|
|||||||
border: 'none'
|
border: 'none'
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties}
|
||||||
>
|
>
|
||||||
<Grid.Item text="企业采购" onClick={() => navTo('/user/poster/poster', true)}>
|
|
||||||
|
{hasRole('store') && (
|
||||||
|
<Grid.Item text="门店中心" onClick={() => navTo('/store/index', true)}>
|
||||||
|
<View className="text-center">
|
||||||
|
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Shop color="#3b82f6" size="20"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Grid.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasRole('rider') && (
|
||||||
|
<Grid.Item text="配送中心" onClick={() => navTo('/rider/index', true)}>
|
||||||
|
<View className="text-center">
|
||||||
|
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Jdl color="#3b82f6" size="20"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Grid.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(hasRole('staff') || hasRole('admin')) && (
|
||||||
|
<Grid.Item text="门店订单" onClick={() => navTo('/user/store/orders/index', true)}>
|
||||||
|
<View className="text-center">
|
||||||
|
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Shop color="#f59e0b" size="20"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Grid.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Grid.Item text="配送地址" onClick={() => navTo('/user/address/index', true)}>
|
||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
<ShoppingAdd color="#3b82f6" size="20"/>
|
<Location color="#3b82f6" size="20"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Grid.Item>
|
||||||
|
|
||||||
|
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
|
||||||
|
<View className="text-center">
|
||||||
|
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Ask className={'text-cyan-500'} size="20"/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Grid.Item>
|
</Grid.Item>
|
||||||
@@ -71,14 +111,6 @@ const UserCell = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Grid.Item>
|
</Grid.Item>
|
||||||
|
|
||||||
<Grid.Item text="收货地址" onClick={() => navTo('/user/address/index', true)}>
|
|
||||||
<View className="text-center">
|
|
||||||
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
|
||||||
<Location color="#3b82f6" size="20"/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Grid.Item>
|
|
||||||
|
|
||||||
<Grid.Item text={'实名认证'} onClick={() => navTo('/user/userVerify/index', true)}>
|
<Grid.Item text={'实名认证'} onClick={() => navTo('/user/userVerify/index', true)}>
|
||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
@@ -111,13 +143,6 @@ const UserCell = () => {
|
|||||||
{/* </View>*/}
|
{/* </View>*/}
|
||||||
{/*</Grid.Item>*/}
|
{/*</Grid.Item>*/}
|
||||||
|
|
||||||
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
|
|
||||||
<View className="text-center">
|
|
||||||
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
|
||||||
<Ask className={'text-cyan-500'} size="20"/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Grid.Item>
|
|
||||||
|
|
||||||
<Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>
|
<Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>
|
||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
@@ -189,4 +214,3 @@ const UserCell = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default UserCell
|
export default UserCell
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function UserOrder() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View className={'title-bar flex justify-between pt-2'}>
|
<View className={'title-bar flex justify-between pt-2'}>
|
||||||
<Text className={'title font-medium px-4'}>我的订单</Text>
|
<Text className={'title font-medium px-4'}>商城订单</Text>
|
||||||
<View
|
<View
|
||||||
className={'more flex items-center px-2'}
|
className={'more flex items-center px-2'}
|
||||||
onClick={() => navTo('/user/order/order', true)}
|
onClick={() => navTo('/user/order/order', true)}
|
||||||
|
|||||||
@@ -1,33 +1,41 @@
|
|||||||
import {useEffect, useRef} from 'react'
|
import {useEffect, useRef, useState} from 'react'
|
||||||
import {PullToRefresh} from '@nutui/nutui-react-taro'
|
import {PullToRefresh} from '@nutui/nutui-react-taro'
|
||||||
import UserCard from "./components/UserCard";
|
import UserCard from "./components/UserCard";
|
||||||
import UserOrder from "./components/UserOrder";
|
import UserOrder from "./components/UserOrder";
|
||||||
import UserFooter from "./components/UserFooter";
|
import UserFooter from "./components/UserFooter";
|
||||||
import {useUserData} from "@/hooks/useUserData";
|
|
||||||
import {View} from '@tarojs/components';
|
import {View} from '@tarojs/components';
|
||||||
import './user.scss'
|
import './user.scss'
|
||||||
import IsDealer from "./components/IsDealer";
|
import IsDealer from "./components/IsDealer";
|
||||||
import {useThemeStyles} from "@/hooks/useTheme";
|
import {useThemeStyles} from "@/hooks/useTheme";
|
||||||
import UserGrid from "@/pages/user/components/UserGrid";
|
import UserGrid from "@/pages/user/components/UserGrid";
|
||||||
|
import { useDidShow } from '@tarojs/taro'
|
||||||
|
|
||||||
function User() {
|
function User() {
|
||||||
|
|
||||||
const {refresh} = useUserData()
|
|
||||||
const userCardRef = useRef<any>()
|
const userCardRef = useRef<any>()
|
||||||
const themeStyles = useThemeStyles();
|
const themeStyles = useThemeStyles();
|
||||||
|
// TabBar 页在小程序里通常不会销毁;从“注册/申请”页返回时需要触发子组件重新初始化/拉取最新状态。
|
||||||
|
const [dealerViewKey, setDealerViewKey] = useState(0)
|
||||||
|
|
||||||
// 下拉刷新处理
|
// 下拉刷新处理
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
await refresh()
|
|
||||||
// 如果 UserCard 组件有自己的刷新方法,也可以调用
|
|
||||||
if (userCardRef.current?.handleRefresh) {
|
if (userCardRef.current?.handleRefresh) {
|
||||||
await userCardRef.current.handleRefresh()
|
await userCardRef.current.handleRefresh()
|
||||||
}
|
}
|
||||||
|
setDealerViewKey(v => v + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 每次进入/切回个人中心都刷新一次统计(包含水票数量)
|
||||||
|
useDidShow(() => {
|
||||||
|
userCardRef.current?.reloadStats?.()
|
||||||
|
// 个人资料(头像/昵称)可能在其它页面被修改,这里确保返回时立刻刷新
|
||||||
|
userCardRef.current?.reloadUserInfo?.()
|
||||||
|
setDealerViewKey(v => v + 1)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PullToRefresh
|
<PullToRefresh
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
@@ -54,7 +62,7 @@ function User() {
|
|||||||
</View>
|
</View>
|
||||||
<UserCard ref={userCardRef}/>
|
<UserCard ref={userCardRef}/>
|
||||||
<UserOrder/>
|
<UserOrder/>
|
||||||
<IsDealer/>
|
<IsDealer key={dealerViewKey}/>
|
||||||
<UserGrid/>
|
<UserGrid/>
|
||||||
<UserFooter/>
|
<UserFooter/>
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
|
|||||||
@@ -1,28 +1,47 @@
|
|||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from 'react'
|
||||||
import Taro from '@tarojs/taro'
|
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 Agreement = () => {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [content, setContent] = useState<string>('')
|
||||||
|
|
||||||
const [content, setContent] = useState<any>('')
|
const reload = async () => {
|
||||||
const reload = () => {
|
try {
|
||||||
Taro.hideTabBar()
|
Taro.hideTabBar()
|
||||||
setContent('<p>' +
|
} catch (_) {
|
||||||
'<span style="font-size: 14px;">欢迎使用</span>' +
|
// ignore (e.g. H5 / unsupported env)
|
||||||
'<span style="font-size: 14px;"> </span>' +
|
}
|
||||||
'<span style="font-size: 14px;"><strong><span style="color: rgb(255, 0, 0);">【WebSoft】</span></strong></span>' +
|
|
||||||
'<span style="font-size: 14px;">服务协议 </span>' +
|
try {
|
||||||
'</p>')
|
const article = await getByCode('xieyi')
|
||||||
|
setContent(article?.content ? wxParse(article.content) : '<p>暂无协议内容</p>')
|
||||||
|
} catch (e) {
|
||||||
|
// Keep UI usable even if CMS/API fails.
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('load agreement failed', e)
|
||||||
|
setContent('<p>协议内容加载失败</p>')
|
||||||
|
Taro.showToast({ title: '协议加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reload()
|
reload()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loading className={'px-2'}>加载中</Loading>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className={'content text-gray-700 text-sm p-4'}>
|
<View className={'content text-gray-700 text-sm p-4'}>
|
||||||
<RichText nodes={content}/>
|
<RichText nodes={content} />
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '注册账号',
|
navigationBarTitleText: '注册/登录',
|
||||||
navigationBarTextStyle: 'black'
|
navigationBarTextStyle: 'black'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,295 @@
|
|||||||
import {useEffect, useState} from "react";
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import Taro from '@tarojs/taro'
|
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<string | undefined> {
|
||||||
|
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 Register = () => {
|
||||||
const [isAgree, setIsAgree] = useState(false)
|
const [isAgree, setIsAgree] = useState(false)
|
||||||
const reload = () => {
|
const [loading, setLoading] = useState(false)
|
||||||
Taro.hideTabBar()
|
|
||||||
}
|
// 短信验证码登录仅在非微信小程序端展示
|
||||||
|
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(() => {
|
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<string, any> = {}
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'flex flex-col justify-center px-5 pt-3'}>
|
<div className={'flex flex-col justify-center px-5 pt-5'}>
|
||||||
<div className={'text-xl font-bold py-2'}>免费试用14天,快速上手独立站</div>
|
<div className={'text-3xl text-center py-5 font-normal my-6'}>注册/登录</div>
|
||||||
<div className={'text-sm py-1 font-normal text-gray-500'}>建站、选品、营销、支付、物流,全部搞定</div>
|
|
||||||
<div className={'text-sm pb-4 font-normal text-gray-500'}>
|
<div className={'flex flex-col gap-3 bg-green-600 py-1'} style={{ borderRadius: '100px' }}>
|
||||||
WebSoft为您提供独立站的解决方案,提供专业、高效、安全的运营服务。
|
<Button
|
||||||
|
type="primary"
|
||||||
|
fill="solid"
|
||||||
|
color="#07c160"
|
||||||
|
block
|
||||||
|
loading={loading}
|
||||||
|
disabled={!isAgree || loading}
|
||||||
|
open-type="getPhoneNumber"
|
||||||
|
onGetPhoneNumber={handleGetPhoneNumber}
|
||||||
|
>
|
||||||
|
手机号一键注册/登录
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!isWeapp && (
|
||||||
|
<Button type="default" block disabled={!isAgree || loading} onClick={goSmsLogin}>
|
||||||
|
短信验证码注册/登录
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
|
||||||
<Input type="text" placeholder="手机号" maxLength={11} style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
<div className={'mt-6 flex text-sm items-center px-1'}>
|
||||||
|
<Radio style={{ color: '#333333' }} checked={isAgree} onClick={() => setIsAgree(!isAgree)} />
|
||||||
|
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}>
|
||||||
|
勾选表示您已阅读并同意
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
onClick={() => Taro.navigateTo({ url: '/passport/agreement' })}
|
||||||
|
className={'text-blue-600'}
|
||||||
|
>
|
||||||
|
《服务协议及隐私政策》
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
|
||||||
<Input type="password" placeholder="密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
|
||||||
</div>
|
|
||||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
|
||||||
<Input type="password" placeholder="再次输入密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
|
||||||
</div>
|
|
||||||
<div className={'flex justify-center my-5'}>
|
|
||||||
<Button type="info" size={'large'} className={'w-full rounded-lg p-2'} disabled={!isAgree}>免费试用</Button>
|
|
||||||
</div>
|
|
||||||
<div className={'my-2 flex text-sm items-center px-1'}>
|
|
||||||
<Radio style={{color: '#333333'}} checked={isAgree} onClick={() => setIsAgree(!isAgree)}></Radio>
|
|
||||||
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}>勾选表示您已阅读并同意</span>
|
|
||||||
<a onClick={() => Taro.navigateTo({url: '/passport/agreement'})} className={'text-blue-600'}>《服务协议及隐私政策》</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={'w-full fixed bottom-20 my-2 flex justify-center text-sm items-center text-center'}>
|
|
||||||
已有账号?<a className={'text-blue-600'} onClick={() => Taro.navigateBack()}>返回登录</a>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Register
|
export default Register
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Taro from '@tarojs/taro'
|
|||||||
import {Input, Button} from '@nutui/nutui-react-taro'
|
import {Input, Button} from '@nutui/nutui-react-taro'
|
||||||
import {loginBySms, sendSmsCaptcha} from "@/api/passport/login";
|
import {loginBySms, sendSmsCaptcha} from "@/api/passport/login";
|
||||||
import {LoginParam} from "@/api/passport/login/model";
|
import {LoginParam} from "@/api/passport/login/model";
|
||||||
|
import {checkAndHandleInviteRelation, hasPendingInvite, parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite";
|
||||||
|
|
||||||
const SmsLogin = () => {
|
const SmsLogin = () => {
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
@@ -13,6 +14,46 @@ const SmsLogin = () => {
|
|||||||
code: ''
|
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 = () => {
|
const reload = () => {
|
||||||
Taro.hideTabBar()
|
Taro.hideTabBar()
|
||||||
}
|
}
|
||||||
@@ -21,6 +62,19 @@ const SmsLogin = () => {
|
|||||||
reload()
|
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(() => {
|
useEffect(() => {
|
||||||
let timer: NodeJS.Timeout
|
let timer: NodeJS.Timeout
|
||||||
@@ -131,6 +185,15 @@ const SmsLogin = () => {
|
|||||||
code: formData.code
|
code: formData.code
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定
|
||||||
|
if (hasPendingInvite()) {
|
||||||
|
try {
|
||||||
|
await checkAndHandleInviteRelation()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('短信登录后处理邀请关系失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '登录成功',
|
title: '登录成功',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
@@ -138,8 +201,9 @@ const SmsLogin = () => {
|
|||||||
|
|
||||||
// 延迟跳转到首页
|
// 延迟跳转到首页
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.reLaunch({
|
navigateAfterLogin().catch((e) => {
|
||||||
url: '/pages/index/index'
|
console.error('短信登录后跳转失败:', e)
|
||||||
|
Taro.reLaunch({ url: '/pages/index/index' })
|
||||||
})
|
})
|
||||||
}, 1500)
|
}, 1500)
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const UnifiedQRPage: React.FC = () => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
title: '核销成功',
|
title: '核销成功',
|
||||||
content: '是否继续扫码核销其他礼品卡?',
|
content: '是否继续扫码核销其他水票/礼品卡?',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
handleStartScan();
|
handleStartScan();
|
||||||
@@ -179,7 +179,7 @@ const UnifiedQRPage: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text className="text-gray-600 mb-6 block">
|
<Text className="text-gray-600 mb-6 block">
|
||||||
{scanType === ScanType.LOGIN ? '正在确认登录' :
|
{scanType === ScanType.LOGIN ? '正在确认登录' :
|
||||||
scanType === ScanType.VERIFICATION ? '正在核销礼品卡' : '正在处理'}
|
scanType === ScanType.VERIFICATION ? '正在核销' : '正在处理'}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -192,12 +192,29 @@ const UnifiedQRPage: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
{result.type === ScanType.VERIFICATION && result.data && (
|
{result.type === ScanType.VERIFICATION && result.data && (
|
||||||
<View className="bg-green-50 rounded-lg p-3 mb-4">
|
<View className="bg-green-50 rounded-lg p-3 mb-4">
|
||||||
<Text className="text-sm text-green-800 block">
|
{result.data.businessType === 'gift' && result.data.gift && (
|
||||||
礼品卡:{result.data.goodsName || '未知商品'}
|
<>
|
||||||
</Text>
|
<Text className="text-sm text-green-800 block">
|
||||||
<Text className="text-sm text-green-800 block">
|
礼品:{result.data.gift.goodsName || result.data.gift.name || '未知'}
|
||||||
面值:¥{result.data.faceValue}
|
</Text>
|
||||||
</Text>
|
<Text className="text-sm text-green-800 block">
|
||||||
|
面值:¥{result.data.gift.faceValue}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{result.data.businessType === 'ticket' && result.data.ticket && (
|
||||||
|
<>
|
||||||
|
<Text className="text-sm text-green-800 block">
|
||||||
|
水票:{result.data.ticket.templateName || '水票'}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-sm text-green-800 block">
|
||||||
|
本次核销:{result.data.qty || 1} 次
|
||||||
|
</Text>
|
||||||
|
<Text className="text-sm text-green-800 block">
|
||||||
|
剩余可用:{result.data.ticket.availableQty ?? 0} 次
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<View className="mt-2">
|
<View className="mt-2">
|
||||||
@@ -278,9 +295,14 @@ const UnifiedQRPage: React.FC = () => {
|
|||||||
<Text className="text-sm text-gray-800">
|
<Text className="text-sm text-gray-800">
|
||||||
{record.success ? record.message : record.error}
|
{record.success ? record.message : record.error}
|
||||||
</Text>
|
</Text>
|
||||||
{record.success && record.type === ScanType.VERIFICATION && record.data && (
|
{record.success && record.type === ScanType.VERIFICATION && record.data?.businessType === 'gift' && record.data?.gift && (
|
||||||
<Text className="text-xs text-gray-500">
|
<Text className="text-xs text-gray-500">
|
||||||
{record.data.goodsName} - ¥{record.data.faceValue}
|
{record.data.gift.goodsName || record.data.gift.name} - ¥{record.data.gift.faceValue}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{record.success && record.type === ScanType.VERIFICATION && record.data?.businessType === 'ticket' && record.data?.ticket && (
|
||||||
|
<Text className="text-xs text-gray-500">
|
||||||
|
{record.data.ticket.templateName || '水票'} - 本次核销 {record.data.qty || 1} 次
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -304,7 +326,7 @@ const UnifiedQRPage: React.FC = () => {
|
|||||||
• 登录二维码:自动确认网页端登录
|
• 登录二维码:自动确认网页端登录
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs text-blue-700 block mb-1">
|
<Text className="text-xs text-blue-700 block mb-1">
|
||||||
• 核销二维码:门店核销用户礼品卡
|
• 核销二维码:核销用户水票/礼品卡
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs text-blue-700 block">
|
<Text className="text-xs text-blue-700 block">
|
||||||
• 系统会自动识别二维码类型并执行相应操作
|
• 系统会自动识别二维码类型并执行相应操作
|
||||||
|
|||||||
3
src/rider/index.config.ts
Normal file
3
src/rider/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '配送中心'
|
||||||
|
})
|
||||||
0
src/rider/index.scss
Normal file
0
src/rider/index.scss
Normal file
304
src/rider/index.tsx
Normal file
304
src/rider/index.tsx
Normal file
@@ -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 (
|
||||||
|
<View className="p-4">
|
||||||
|
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||||
|
<Text className="text-red-600">{error}</Text>
|
||||||
|
</View>
|
||||||
|
<Button type="primary" onClick={refresh}>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-100 min-h-screen">
|
||||||
|
<View>
|
||||||
|
{/*头部信息*/}
|
||||||
|
{dealerUser && (
|
||||||
|
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
|
||||||
|
{/* 装饰性背景元素 - 小程序兼容版本 */}
|
||||||
|
<View className="absolute w-32 h-32 rounded-full" style={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
top: '-16px',
|
||||||
|
right: '-16px'
|
||||||
|
}}></View>
|
||||||
|
<View className="absolute w-24 h-24 rounded-full" style={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
bottom: '-12px',
|
||||||
|
left: '-12px'
|
||||||
|
}}></View>
|
||||||
|
<View className="absolute w-16 h-16 rounded-full" style={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
top: '60px',
|
||||||
|
left: '120px'
|
||||||
|
}}></View>
|
||||||
|
<View className="flex items-center justify-between relative z-10 mb-4">
|
||||||
|
<Avatar
|
||||||
|
size="50"
|
||||||
|
src={dealerUser?.qrcode}
|
||||||
|
icon={<User/>}
|
||||||
|
className="mr-4"
|
||||||
|
style={{
|
||||||
|
border: '2px solid rgba(255, 255, 255, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View className="flex-1 flex-col">
|
||||||
|
<View className="text-white text-lg font-bold mb-1" style={{
|
||||||
|
}}>
|
||||||
|
{dealerUser?.realName || '分销商'}
|
||||||
|
</View>
|
||||||
|
<View className="text-sm" style={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.8)'
|
||||||
|
}}>
|
||||||
|
ID: {dealerUser.userId}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="text-right hidden">
|
||||||
|
<Text className="text-xs" style={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.9)'
|
||||||
|
}}>加入时间</Text>
|
||||||
|
<Text className="text-xs" style={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)'
|
||||||
|
}}>
|
||||||
|
{formatTime(dealerUser.createTime)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 佣金统计卡片 */}
|
||||||
|
{dealerUser && (
|
||||||
|
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
|
||||||
|
<View className="mb-4">
|
||||||
|
<Text className="font-semibold text-gray-800">工资统计</Text>
|
||||||
|
</View>
|
||||||
|
<View className="grid grid-cols-3 gap-3">
|
||||||
|
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||||
|
background: businessGradients.money.available
|
||||||
|
}}>
|
||||||
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
|
{formatMoney(dealerUser.money)}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>本月配送佣金</Text>
|
||||||
|
</View>
|
||||||
|
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||||
|
background: businessGradients.money.frozen
|
||||||
|
}}>
|
||||||
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
|
{formatMoney(dealerUser.freezeMoney)}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>桶数</Text>
|
||||||
|
</View>
|
||||||
|
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||||
|
background: businessGradients.money.total
|
||||||
|
}}>
|
||||||
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
|
{formatMoney(dealerUser.totalMoney)}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>累计收入</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 团队统计 */}
|
||||||
|
{dealerUser && (
|
||||||
|
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
|
||||||
|
<View className="flex items-center justify-between mb-4">
|
||||||
|
<Text className="font-semibold text-gray-800">我的邀请</Text>
|
||||||
|
<View
|
||||||
|
className="text-gray-400 text-sm flex items-center"
|
||||||
|
onClick={() => navigateToPage('/dealer/team/index')}
|
||||||
|
>
|
||||||
|
<Text>查看详情</Text>
|
||||||
|
<ArrowRight size="12"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="grid grid-cols-3 gap-4">
|
||||||
|
<View className="text-center grid">
|
||||||
|
<Text className="text-xl font-bold text-purple-500 mb-1">
|
||||||
|
{dealerUser.firstNum || 0}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs text-gray-500">一级成员</Text>
|
||||||
|
</View>
|
||||||
|
<View className="text-center grid">
|
||||||
|
<Text className="text-xl font-bold text-indigo-500 mb-1">
|
||||||
|
{dealerUser.secondNum || 0}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs text-gray-500">二级成员</Text>
|
||||||
|
</View>
|
||||||
|
<View className="text-center grid">
|
||||||
|
<Text className="text-xl font-bold text-pink-500 mb-1">
|
||||||
|
{dealerUser.thirdNum || 0}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs text-gray-500">三级成员</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 功能导航 */}
|
||||||
|
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
|
||||||
|
<View className="font-semibold mb-4 text-gray-800">配送工具</View>
|
||||||
|
<ConfigProvider>
|
||||||
|
<Grid
|
||||||
|
columns={4}
|
||||||
|
className="no-border-grid"
|
||||||
|
style={{
|
||||||
|
'--nutui-grid-border-color': 'transparent',
|
||||||
|
'--nutui-grid-item-border-width': '0px',
|
||||||
|
border: 'none'
|
||||||
|
} as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<Grid.Item text="配送订单" onClick={() => navigateToPage('/rider/orders/index')}>
|
||||||
|
<View className="text-center">
|
||||||
|
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Shopping color="#3b82f6" size="20"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Grid.Item>
|
||||||
|
|
||||||
|
<Grid.Item text={'工资明细'} onClick={() => navigateToPage('/rider/withdraw/index')}>
|
||||||
|
<View className="text-center">
|
||||||
|
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Purse color="#10b981" size="20"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Grid.Item>
|
||||||
|
|
||||||
|
<Grid.Item text={'配送小区'} onClick={() => navigateToPage('/rider/team/index')}>
|
||||||
|
<View className="text-center">
|
||||||
|
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
|
<People color="#8b5cf6" size="20"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Grid.Item>
|
||||||
|
|
||||||
|
<Grid.Item text={'仓库地址'} onClick={() => navigateToPage('/rider/qrcode/index')}>
|
||||||
|
<View className="text-center">
|
||||||
|
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Dongdong color="#f59e0b" size="20"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Grid.Item>
|
||||||
|
|
||||||
|
<Grid.Item text={'水票核销'} onClick={() => navigateToPage('/rider/ticket/verification/index?auto=1')}>
|
||||||
|
<View className="text-center">
|
||||||
|
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
|
<Scan color="#06b6d4" size="20" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Grid.Item>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* 第二行功能 */}
|
||||||
|
{/*<Grid*/}
|
||||||
|
{/* columns={4}*/}
|
||||||
|
{/* className="no-border-grid mt-4"*/}
|
||||||
|
{/* style={{*/}
|
||||||
|
{/* '--nutui-grid-border-color': 'transparent',*/}
|
||||||
|
{/* '--nutui-grid-item-border-width': '0px',*/}
|
||||||
|
{/* border: 'none'*/}
|
||||||
|
{/* } as React.CSSProperties}*/}
|
||||||
|
{/*>*/}
|
||||||
|
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
|
||||||
|
{/* <View className="text-center">*/}
|
||||||
|
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||||
|
{/* <Presentation color="#6366f1" size="20"/>*/}
|
||||||
|
{/* </View>*/}
|
||||||
|
{/* </View>*/}
|
||||||
|
{/* </Grid.Item>*/}
|
||||||
|
|
||||||
|
{/* /!* 预留其他功能位置 *!/*/}
|
||||||
|
{/* <Grid.Item text={''}>*/}
|
||||||
|
{/* <View className="text-center">*/}
|
||||||
|
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||||
|
{/* </View>*/}
|
||||||
|
{/* </View>*/}
|
||||||
|
{/* </Grid.Item>*/}
|
||||||
|
|
||||||
|
{/* <Grid.Item text={''}>*/}
|
||||||
|
{/* <View className="text-center">*/}
|
||||||
|
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||||
|
{/* </View>*/}
|
||||||
|
{/* </View>*/}
|
||||||
|
{/* </Grid.Item>*/}
|
||||||
|
|
||||||
|
{/* <Grid.Item text={''}>*/}
|
||||||
|
{/* <View className="text-center">*/}
|
||||||
|
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||||
|
{/* </View>*/}
|
||||||
|
{/* </View>*/}
|
||||||
|
{/* </Grid.Item>*/}
|
||||||
|
{/*</Grid>*/}
|
||||||
|
</ConfigProvider>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部安全区域 */}
|
||||||
|
<View className="h-20"></View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DealerIndex
|
||||||
4
src/rider/orders/index.config.ts
Normal file
4
src/rider/orders/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '送水订单',
|
||||||
|
navigationBarTextStyle: 'black'
|
||||||
|
}
|
||||||
610
src/rider/orders/index.tsx
Normal file
610
src/rider/orders/index.tsx
Normal file
@@ -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<GltTicketOrder[]>([])
|
||||||
|
const [tabIndex, setTabIndex] = useState(0)
|
||||||
|
const [list, setList] = useState<GltTicketOrder[]>([])
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const [deliverDialogVisible, setDeliverDialogVisible] = useState(false)
|
||||||
|
const [deliverSubmitting, setDeliverSubmitting] = useState(false)
|
||||||
|
const [deliverOrder, setDeliverOrder] = useState<GltTicketOrder | null>(null)
|
||||||
|
const [deliverImg, setDeliverImg] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
|
type DeliverConfirmMode = 'photoComplete' | 'waitCustomerConfirm'
|
||||||
|
const [deliverConfirmMode, setDeliverConfirmMode] = useState<DeliverConfirmMode>('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 (
|
||||||
|
<View className="bg-gray-50 min-h-screen p-4">
|
||||||
|
<Text>请先登录</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayList = filterByTab(list)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-50 min-h-screen">
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Tabs
|
||||||
|
align="left"
|
||||||
|
className="fixed left-0"
|
||||||
|
style={{zIndex: 998, borderBottom: '1px solid #e5e5e5'}}
|
||||||
|
tabStyle={{backgroundColor: '#ffffff'}}
|
||||||
|
value={tabIndex}
|
||||||
|
onChange={(paneKey) => setTabIndex(Number(paneKey))}
|
||||||
|
>
|
||||||
|
{riderTabs.map(t => (
|
||||||
|
<TabPane key={t.index} title={loading && tabIndex === t.index ? `${t.title}...` : t.title}></TabPane>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<PullToRefresh
|
||||||
|
onRefresh={async () => {
|
||||||
|
pageRef.current = 1
|
||||||
|
listRef.current = []
|
||||||
|
setList([])
|
||||||
|
setHasMore(true)
|
||||||
|
await reload(true)
|
||||||
|
}}
|
||||||
|
headHeight={60}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{ height: '84vh', width: '100%', padding: '0', overflowY: 'auto', overflowX: 'hidden' }}
|
||||||
|
id="rider-order-scroll"
|
||||||
|
>
|
||||||
|
{error ? (
|
||||||
|
<View className="flex flex-col items-center justify-center h-64">
|
||||||
|
<Text className="text-gray-500 mb-4">{error}</Text>
|
||||||
|
<Button size="small" type="primary" onClick={() => reload(true)}>
|
||||||
|
重新加载
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<InfiniteLoading
|
||||||
|
target="rider-order-scroll"
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={reloadMore}
|
||||||
|
loadingText={
|
||||||
|
<View className="flex justify-center items-center py-4">
|
||||||
|
<Loading />
|
||||||
|
<View className="ml-2">加载中...</View>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
loadMoreText={
|
||||||
|
displayList.length === 0 ? (
|
||||||
|
<Empty style={{ backgroundColor: 'transparent' }} description="暂无配送订单" />
|
||||||
|
) : (
|
||||||
|
<View className="h-24 text-center text-gray-500">没有更多了</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{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 (
|
||||||
|
<Cell key={String(o.id)} style={{ padding: '16px' }}>
|
||||||
|
<View className="w-full">
|
||||||
|
<View className="flex justify-between items-center">
|
||||||
|
<Text className="text-gray-800 font-bold text-sm">
|
||||||
|
{o.userTicketId ? `票号#${o.userTicketId}` : '送水订单'}
|
||||||
|
</Text>
|
||||||
|
<Text className={`${getOrderStatusColor(o)} text-sm font-medium`}>{getOrderStatusText(o)}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="text-gray-400 text-xs mt-1">下单时间:{timeText}</View>
|
||||||
|
|
||||||
|
<View className="mt-3 bg-white rounded-lg">
|
||||||
|
<View className="text-sm text-gray-700">
|
||||||
|
<Text className="text-gray-500">客户:</Text>
|
||||||
|
<Text>{o.nickname || '-'} {o.phone ? `(${o.phone})` : ''}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="text-sm text-gray-700 mt-1">
|
||||||
|
<Text className="text-gray-500">收货地址:</Text>
|
||||||
|
<Text>{addr}</Text>
|
||||||
|
</View>
|
||||||
|
{!!remark && (
|
||||||
|
<View className="text-sm text-gray-700 mt-1">
|
||||||
|
<Text className="text-gray-500">买家留言:</Text>
|
||||||
|
<Text>{remark}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View className="text-sm text-gray-700 mt-1">
|
||||||
|
<Text className="text-gray-500">数量:</Text>
|
||||||
|
<Text>{Number.isFinite(qty) ? qty : '-'}</Text>
|
||||||
|
<Text className="text-gray-500 ml-3">金额:</Text>
|
||||||
|
<Text>¥{o.price || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="text-sm text-gray-700 mt-1">
|
||||||
|
<Text className="text-gray-500">配送时间:</Text>
|
||||||
|
<Text>{o.sendTime ? dayjs(o.sendTime).format('YYYY-MM-DD HH:mm') : '-'}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="text-sm text-gray-700 mt-1">
|
||||||
|
<Text className="text-gray-500">取水点:</Text>
|
||||||
|
<Text>{pickupName || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="text-sm text-gray-700 mt-1">
|
||||||
|
<Text className="text-gray-500">取水地址:</Text>
|
||||||
|
<Text>{pickupAddr || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{!!o.sendStartTime && (
|
||||||
|
<View className="text-sm text-gray-700 mt-1">
|
||||||
|
<Text className="text-gray-500">开始配送:</Text>
|
||||||
|
<Text>{dayjs(o.sendStartTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{!!o.sendEndTime && (
|
||||||
|
<View className="text-sm text-gray-700 mt-1">
|
||||||
|
<Text className="text-gray-500">送达时间:</Text>
|
||||||
|
<Text>{dayjs(o.sendEndTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{!!o.receiveConfirmTime && (
|
||||||
|
<View className="text-sm text-gray-700 mt-1">
|
||||||
|
<Text className="text-gray-500">完成时间:</Text>
|
||||||
|
<Text>{dayjs(o.receiveConfirmTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{o.sendEndImg ? (
|
||||||
|
<View className="text-sm text-gray-700 mt-2">
|
||||||
|
<Text className="text-gray-500">送达照片:</Text>
|
||||||
|
<View className="mt-2">
|
||||||
|
<Image src={o.sendEndImg} width="100%" height="120" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 配送流程 */}
|
||||||
|
<View className="mt-3 bg-gray-50 rounded-lg p-2 text-xs">
|
||||||
|
<Text className="text-gray-600">流程:</Text>
|
||||||
|
<Text className={flow1Done ? 'text-green-600 font-medium' : 'text-gray-400'}>1 派单</Text>
|
||||||
|
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||||
|
<Text className={flow2Done ? 'text-blue-600 font-medium' : 'text-gray-400'}>2 配送中</Text>
|
||||||
|
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||||
|
<Text className={flow3Done ? 'text-purple-600 font-medium' : 'text-gray-400'}>3 送达留档</Text>
|
||||||
|
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||||
|
<Text className={flow4Done ? 'text-green-600 font-medium' : 'text-gray-400'}>4 完成</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-3 flex justify-end">
|
||||||
|
<Space>
|
||||||
|
{!!phoneToCall && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
Taro.makePhoneCall({ phoneNumber: phoneToCall })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
联系客户
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!!addr && addr !== '-' && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
void Taro.setClipboardData({ data: addr })
|
||||||
|
Taro.showToast({ title: '地址已复制', icon: 'none' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
复制地址
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!!storePhone && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
Taro.makePhoneCall({ phoneNumber: storePhone })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
联系门店
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{canStartDeliver(o) && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
void handleStartDeliver(o)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
开始配送
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{canConfirmDelivered(o) && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
openDeliverDialog(o, { mode: 'waitCustomerConfirm' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确认送达
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{canCompleteByPhoto(o) && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
openDeliverDialog(o, { mode: 'photoComplete' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
补传照片完成
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Cell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</InfiniteLoading>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</PullToRefresh>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
title="确认送达"
|
||||||
|
visible={deliverDialogVisible}
|
||||||
|
confirmText={
|
||||||
|
deliverSubmitting
|
||||||
|
? '提交中...'
|
||||||
|
: deliverConfirmMode === 'photoComplete'
|
||||||
|
? '拍照完成'
|
||||||
|
: '确认送达'
|
||||||
|
}
|
||||||
|
cancelText="取消"
|
||||||
|
onConfirm={handleConfirmDelivered}
|
||||||
|
onCancel={() => {
|
||||||
|
if (deliverSubmitting) return
|
||||||
|
setDeliverDialogVisible(false)
|
||||||
|
setDeliverOrder(null)
|
||||||
|
setDeliverImg(undefined)
|
||||||
|
setDeliverConfirmMode('photoComplete')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className="text-sm text-gray-700">
|
||||||
|
<View>到达收货点后,可选择“拍照留档直接完成”或“等待客户确认收货”。</View>
|
||||||
|
|
||||||
|
<View className="mt-3">
|
||||||
|
<RadioGroup value={deliverConfirmMode} onChange={v => setDeliverConfirmMode(v as DeliverConfirmMode)}>
|
||||||
|
<Radio value="photoComplete">拍照留档(直接完成)</Radio>
|
||||||
|
<Radio value="waitCustomerConfirm">客户确认收货(可不拍照)</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</View>
|
||||||
|
<View className="mt-3">
|
||||||
|
<Button size="small" onClick={handleChooseDeliverImg}>
|
||||||
|
{deliverImg ? '重新拍照/上传' : '拍照/上传'}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
{deliverImg && (
|
||||||
|
<View className="mt-3">
|
||||||
|
<Image src={deliverImg} width="100%" height="120" />
|
||||||
|
<View className="mt-2 flex justify-end">
|
||||||
|
<Button size="small" onClick={() => setDeliverImg(undefined)}>
|
||||||
|
移除照片
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View className="mt-3 text-xs text-gray-500">
|
||||||
|
说明:如选择“客户确认收货”,订单进入“待客户确认”;客户在用户端确认收货或超时自动确认(需后端支持)。
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Dialog>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
4
src/rider/ticket/verification/index.config.ts
Normal file
4
src/rider/ticket/verification/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '水票核销'
|
||||||
|
})
|
||||||
|
|
||||||
280
src/rider/ticket/verification/index.tsx
Normal file
280
src/rider/ticket/verification/index.tsx
Normal file
@@ -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<GltUserTicket | null>(null)
|
||||||
|
const [lastQty, setLastQty] = useState<number>(1)
|
||||||
|
const [records, setRecords] = useState<VerifyRecord[]>([])
|
||||||
|
|
||||||
|
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<VerifyRecord, 'id' | 'time'>) => {
|
||||||
|
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<TicketPayload> => {
|
||||||
|
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 (
|
||||||
|
<ConfigProvider>
|
||||||
|
<View className="min-h-screen bg-gray-50 p-4">
|
||||||
|
<Card>
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<View>
|
||||||
|
<Text className="text-base font-bold text-gray-800">水票核销</Text>
|
||||||
|
<View className="mt-1">
|
||||||
|
<Text className="text-xs text-gray-500">
|
||||||
|
扫描用户出示的“水票核销码”完成核销
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Tips className="text-gray-400" size="18" />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-4">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
loading={loading}
|
||||||
|
icon={<Scan />}
|
||||||
|
onClick={handleScan}
|
||||||
|
>
|
||||||
|
扫码核销
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{lastTicket && (
|
||||||
|
<View className="mt-4 bg-gray-50 rounded-lg p-3">
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<Text className="text-sm text-gray-700">最近一次核销</Text>
|
||||||
|
<Text className="text-xs text-gray-500">使用 {lastQty} 次</Text>
|
||||||
|
</View>
|
||||||
|
<View className="mt-2">
|
||||||
|
<Text className="text-sm text-gray-900">
|
||||||
|
{lastTicket.templateName || '水票'}(剩余 {lastTicket.availableQty ?? 0} 次)
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<View className="mt-4">
|
||||||
|
<View className="mb-2">
|
||||||
|
<Text className="text-sm font-semibold text-gray-800">核销记录</Text>
|
||||||
|
<Text className="text-xs text-gray-500 ml-2">仅保留最近10条</Text>
|
||||||
|
</View>
|
||||||
|
{records.length === 0 ? (
|
||||||
|
<View className="bg-white rounded-lg p-4">
|
||||||
|
<Text className="text-sm text-gray-500">暂无记录</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className="space-y-2">
|
||||||
|
{records.map(r => (
|
||||||
|
<View key={r.id} className="bg-white rounded-lg p-3 flex items-start justify-between">
|
||||||
|
<View className="flex-1 pr-3">
|
||||||
|
<View className="flex items-center">
|
||||||
|
{r.success ? (
|
||||||
|
<Success className="text-green-500 mr-2" size="16" />
|
||||||
|
) : (
|
||||||
|
<Failure className="text-red-500 mr-2" size="16" />
|
||||||
|
)}
|
||||||
|
<Text className="text-sm text-gray-900">{r.message}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="mt-1">
|
||||||
|
<Text className="text-xs text-gray-500">
|
||||||
|
{r.time}
|
||||||
|
{r.ticketName ? ` · ${r.ticketName}` : ''}
|
||||||
|
{typeof r.qty === 'number' ? ` · ${r.qty}次` : ''}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{r.userInfo && (
|
||||||
|
<View className="mt-1">
|
||||||
|
<Text className="text-xs text-gray-500">{r.userInfo}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ConfigProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RiderTicketVerificationPage
|
||||||
@@ -42,7 +42,7 @@ function Category() {
|
|||||||
|
|
||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
title: `${nav?.categoryName}_时里院子市集`,
|
title: `${nav?.categoryName}_桂乐淘`,
|
||||||
path: `/shop/category/index?id=${categoryId}`,
|
path: `/shop/category/index?id=${categoryId}`,
|
||||||
success: function () {
|
success: function () {
|
||||||
console.log('分享成功');
|
console.log('分享成功');
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user