Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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'
|
||||
|
||||
// 租户ID - 请根据实际情况修改
|
||||
export const TenantId = '10550';
|
||||
export const TenantId = '10584';
|
||||
// 租户名称
|
||||
export const TenantName = '桂乐淘';
|
||||
// 接口地址 - 请根据实际情况修改
|
||||
export const BaseUrl = API_BASE_URL;
|
||||
// 当前版本
|
||||
|
||||
@@ -3,19 +3,20 @@ export const ENV_CONFIG = {
|
||||
// 开发环境
|
||||
development: {
|
||||
API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
// API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
// API_BASE_URL: 'https://mp-api.websoft.top/api',
|
||||
APP_NAME: '开发环境',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
// 生产环境
|
||||
production: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
APP_NAME: '时里院子市集',
|
||||
API_BASE_URL: 'https://mp-api.websoft.top/api',
|
||||
APP_NAME: '桂乐淘',
|
||||
DEBUG: 'false',
|
||||
},
|
||||
// 测试环境
|
||||
test: {
|
||||
API_BASE_URL: 'https://cms-api.s209.websoft.top/api',
|
||||
API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
// API_BASE_URL: 'https://mp-api.websoft.top/api',
|
||||
APP_NAME: '测试环境',
|
||||
DEBUG: 'true',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "template-10550",
|
||||
"name": "template-10584",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "WebSoft Inc.",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"miniprogramRoot": "dist/",
|
||||
"projectname": "template-10550",
|
||||
"description": "时里院子市集",
|
||||
"appid": "wx5170f9f17a813877",
|
||||
"projectname": "template-10584",
|
||||
"description": "桂乐淘",
|
||||
"appid": "wxad831ba00ad6a026",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"miniprogramRoot": "./",
|
||||
"projectname": "mp-react",
|
||||
"description": "时里院子市集",
|
||||
"description": "桂乐淘",
|
||||
"appid": "touristappid",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
|
||||
@@ -237,7 +237,7 @@ function UserCard() {
|
||||
</div>
|
||||
<div className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/gift/index', true)}>
|
||||
<span className={'text-sm text-gray-500'}>礼品卡</span>
|
||||
<span className={'text-sm text-gray-500'}>水票</span>
|
||||
<span className={'text-xl'}>{giftCount}</span>
|
||||
</div>
|
||||
{/*<div className={'item flex justify-center flex-col items-center'}>*/}
|
||||
|
||||
105
src/api/glt/gltTicketTemplate/index.ts
Normal file
105
src/api/glt/gltTicketTemplate/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
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));
|
||||
}
|
||||
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;
|
||||
}
|
||||
105
src/api/glt/gltUserTicket/index.ts
Normal file
105
src/api/glt/gltUserTicket/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { GltUserTicket, GltUserTicketParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询我的水票
|
||||
*/
|
||||
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));
|
||||
}
|
||||
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;
|
||||
}
|
||||
105
src/api/glt/gltUserTicketLog/index.ts
Normal file
105
src/api/glt/gltUserTicketLog/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
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));
|
||||
}
|
||||
53
src/api/glt/gltUserTicketLog/model/index.ts
Normal file
53
src/api/glt/gltUserTicketLog/model/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
}
|
||||
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 {
|
||||
id?: number;
|
||||
// 仅查询当前分销商的收益/资金明细
|
||||
userId?: number;
|
||||
// 可选:按订单过滤
|
||||
orderId?: number;
|
||||
// 可选:资金流动类型过滤
|
||||
flowType?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ export interface ShopDealerOrder {
|
||||
id?: number;
|
||||
// 买家用户ID
|
||||
userId?: number;
|
||||
nickname?: string;
|
||||
// 订单编号(部分接口会直接返回订单号字符串)
|
||||
orderNo?: string;
|
||||
// 订单ID
|
||||
orderId?: number;
|
||||
// 订单总金额(不含运费)
|
||||
@@ -47,5 +50,7 @@ export interface ShopDealerOrderParam extends PageParam {
|
||||
secondUserId?: number;
|
||||
thirdUserId?: number;
|
||||
userId?: number;
|
||||
// 数据权限/资源ID(通常传当前登录用户ID)
|
||||
resourceId?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,21 @@ import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopDealerWithdraw, ShopDealerWithdrawParam } from './model';
|
||||
|
||||
// WeChat transfer v3: backend may return `package_info` for MiniProgram to open the
|
||||
// "confirm receipt" page via `wx.requestMerchantTransfer`.
|
||||
export type ShopDealerWithdrawCreateResult =
|
||||
| string
|
||||
| {
|
||||
package_info?: string;
|
||||
packageInfo?: string;
|
||||
[k: string]: any;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
// When applyStatus=20, user can "receive" (WeChat confirm receipt flow).
|
||||
export type ShopDealerWithdrawReceiveResult = ShopDealerWithdrawCreateResult;
|
||||
|
||||
/**
|
||||
* 分页查询分销商提现明细表
|
||||
*/
|
||||
@@ -33,11 +48,40 @@ export async function listShopDealerWithdraw(params?: ShopDealerWithdrawParam) {
|
||||
/**
|
||||
* 添加分销商提现明细表
|
||||
*/
|
||||
export async function addShopDealerWithdraw(data: ShopDealerWithdraw) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
export async function addShopDealerWithdraw(data: ShopDealerWithdraw): Promise<ShopDealerWithdrawCreateResult> {
|
||||
const res = await request.post<ApiResult<any>>(
|
||||
'/shop/shop-dealer-withdraw',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
// Some backends return `message`, while WeChat transfer flow returns `data.package_info`.
|
||||
return res.data ?? res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户领取(仅当 applyStatus=20 时)- 后台返回 package_info 供小程序调起确认收款页
|
||||
*/
|
||||
export async function receiveShopDealerWithdraw(id: number): Promise<ShopDealerWithdrawReceiveResult> {
|
||||
const res = await request.post<ApiResult<any>>(
|
||||
'/shop/shop-dealer-withdraw/receive/' + id,
|
||||
{}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data ?? res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取成功回调:前端确认收款后通知后台把状态置为 applyStatus=40
|
||||
*/
|
||||
export async function receiveSuccessShopDealerWithdraw(id: number) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-withdraw/receive-success/' + id,
|
||||
{}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 礼品卡
|
||||
* 水票
|
||||
*/
|
||||
export interface ShopGift {
|
||||
// 礼品卡ID
|
||||
|
||||
@@ -146,4 +146,7 @@ export interface ShopGoodsParam extends PageParam {
|
||||
isShow?: number;
|
||||
stock?: number;
|
||||
keywords?: string;
|
||||
recommend?: number;
|
||||
// 0上架 1下架(以实际后端约定为准)
|
||||
status?: number;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import request from '@/utils/request';
|
||||
import request, { ErrorType, RequestError } from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopOrder, ShopOrderParam, OrderCreateRequest } from './model';
|
||||
|
||||
@@ -113,6 +113,44 @@ export interface WxPayResult {
|
||||
paySign: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单重新发起支付(对“已创建但未支付”的订单生成新的预支付参数,不应重复创建订单)
|
||||
*
|
||||
* 说明:不同后端版本可能暴露不同路径,这里做兼容探测;若全部失败,调用方可自行降级处理。
|
||||
*/
|
||||
export interface OrderPrepayRequest {
|
||||
orderId: number;
|
||||
payType: number;
|
||||
}
|
||||
|
||||
export async function prepayShopOrder(data: OrderPrepayRequest) {
|
||||
const urls = [
|
||||
'/shop/shop-order/pay',
|
||||
'/shop/shop-order/prepay',
|
||||
'/shop/shop-order/repay'
|
||||
];
|
||||
|
||||
let lastError: unknown;
|
||||
let businessError: unknown;
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const res = await request.post<ApiResult<WxPayResult>>(url, data, { showError: false });
|
||||
// request.ts 在 code!=0 时会直接 throw;走到这里通常都是 code===0
|
||||
if (res.code === 0) return res.data;
|
||||
} catch (e) {
|
||||
// 若已命中“业务错误”(例如订单已取消/已支付),优先保留该错误用于向上提示;
|
||||
// 不要被后续的 404/网络错误覆盖掉,避免调用方误判为“不支持该接口”而降级走创建订单。
|
||||
if (!businessError && e instanceof RequestError && e.type === ErrorType.BUSINESS_ERROR) {
|
||||
businessError = e;
|
||||
} else {
|
||||
lastError = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(businessError || lastError || new Error('发起支付失败'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PageParam } from '@/api/index';
|
||||
import {OrderGoods} from "@/api/system/orderGoods/model";
|
||||
import type { ShopOrderGoods } from '@/api/shop/shopOrderGoods/model';
|
||||
|
||||
/**
|
||||
* 订单
|
||||
@@ -27,6 +27,14 @@ export interface ShopOrder {
|
||||
merchantName?: string;
|
||||
// 商户编号
|
||||
merchantCode?: string;
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 使用的优惠券id
|
||||
couponId?: number;
|
||||
// 使用的会员卡id
|
||||
@@ -61,6 +69,8 @@ export interface ShopOrder {
|
||||
sendStartTime?: string;
|
||||
// 配送结束时间
|
||||
sendEndTime?: string;
|
||||
// 配送员送达拍照(选填)
|
||||
sendEndImg?: string;
|
||||
// 发货店铺id
|
||||
expressMerchantId?: number;
|
||||
// 发货店铺
|
||||
@@ -83,6 +93,8 @@ export interface ShopOrder {
|
||||
totalNum?: number;
|
||||
// 教练id
|
||||
coachId?: number;
|
||||
// 商品ID
|
||||
formId?: number;
|
||||
// 支付的用户id
|
||||
payUserId?: number;
|
||||
// 0余额支付, 1微信支付,102微信Native,2会员卡支付,3支付宝,4现金,5POS机,6VIP月卡,7VIP年卡,8VIP次卡,9IC月卡,10IC年卡,11IC次卡,12免费,13VIP充值卡,14IC充值卡,15积分支付,16VIP季卡,17IC季卡,18代付
|
||||
@@ -146,7 +158,7 @@ export interface ShopOrder {
|
||||
// 是否已收到赠品
|
||||
hasTakeGift?: string;
|
||||
// 订单商品项
|
||||
orderGoods?: OrderGoods[];
|
||||
orderGoods?: ShopOrderGoods[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,6 +177,14 @@ export interface OrderGoodsItem {
|
||||
export interface OrderCreateRequest {
|
||||
// 商品信息列表
|
||||
goodsItems: OrderGoodsItem[];
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称(可选)
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 收货地址ID
|
||||
addressId?: number;
|
||||
// 支付方式
|
||||
@@ -197,6 +217,14 @@ export interface OrderGoodsItem {
|
||||
export interface OrderCreateRequest {
|
||||
// 商品信息列表
|
||||
goodsItems: OrderGoodsItem[];
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称(可选)
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 收货地址ID
|
||||
addressId?: number;
|
||||
// 支付方式
|
||||
@@ -223,6 +251,12 @@ export interface ShopOrderParam extends PageParam {
|
||||
payType?: number;
|
||||
isInvoice?: boolean;
|
||||
userId?: number;
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 配送员用户ID
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
keywords?: string;
|
||||
deliveryStatus?: number;
|
||||
statusFilter?: number;
|
||||
|
||||
101
src/api/shop/shopStore/index.ts
Normal file
101
src/api/shop/shopStore/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopStore, ShopStoreParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询门店
|
||||
*/
|
||||
export async function pageShopStore(params: ShopStoreParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStore>>>(
|
||||
'/shop/shop-store/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询门店列表
|
||||
*/
|
||||
export async function listShopStore(params?: ShopStoreParam) {
|
||||
const res = await request.get<ApiResult<ShopStore[]>>(
|
||||
'/shop/shop-store',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加门店
|
||||
*/
|
||||
export async function addShopStore(data: ShopStore) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-store',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改门店
|
||||
*/
|
||||
export async function updateShopStore(data: ShopStore) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-store',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除门店
|
||||
*/
|
||||
export async function removeShopStore(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除门店
|
||||
*/
|
||||
export async function removeBatchShopStore(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询门店
|
||||
*/
|
||||
export async function getShopStore(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStore>>(
|
||||
'/shop/shop-store/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
63
src/api/shop/shopStore/model/index.ts
Normal file
63
src/api/shop/shopStore/model/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 门店
|
||||
*/
|
||||
export interface ShopStore {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
// 店铺名称
|
||||
name?: string;
|
||||
// 门店地址
|
||||
address?: string;
|
||||
// 手机号码
|
||||
phone?: string;
|
||||
// 邮箱
|
||||
email?: string;
|
||||
// 门店经理
|
||||
managerName?: string;
|
||||
// 门店banner
|
||||
shopBanner?: string;
|
||||
// 所在省份
|
||||
province?: string;
|
||||
// 所在城市
|
||||
city?: string;
|
||||
// 所在辖区
|
||||
region?: string;
|
||||
// 经度和纬度
|
||||
lngAndLat?: string;
|
||||
// 位置
|
||||
location?:string;
|
||||
// 区域
|
||||
district?: string;
|
||||
// 轮廓
|
||||
points?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 默认仓库ID(shop_warehouse.id)
|
||||
warehouseId?: number;
|
||||
// 默认仓库名称(可选)
|
||||
warehouseName?: string;
|
||||
// 状态
|
||||
status?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店搜索条件
|
||||
*/
|
||||
export interface ShopStoreParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
101
src/api/shop/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));
|
||||
}
|
||||
67
src/api/shop/shopStoreRider/model/index.ts
Normal file
67
src/api/shop/shopStoreRider/model/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
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;
|
||||
// 备注
|
||||
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/shopWarehouse/index.ts
Normal file
101
src/api/shop/shopWarehouse/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopWarehouse, ShopWarehouseParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询仓库
|
||||
*/
|
||||
export async function pageShopWarehouse(params: ShopWarehouseParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopWarehouse>>>(
|
||||
'/shop/shop-warehouse/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询仓库列表
|
||||
*/
|
||||
export async function listShopWarehouse(params?: ShopWarehouseParam) {
|
||||
const res = await request.get<ApiResult<ShopWarehouse[]>>(
|
||||
'/shop/shop-warehouse',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加仓库
|
||||
*/
|
||||
export async function addShopWarehouse(data: ShopWarehouse) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改仓库
|
||||
*/
|
||||
export async function updateShopWarehouse(data: ShopWarehouse) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除仓库
|
||||
*/
|
||||
export async function removeShopWarehouse(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除仓库
|
||||
*/
|
||||
export async function removeBatchShopWarehouse(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询仓库
|
||||
*/
|
||||
export async function getShopWarehouse(id: number) {
|
||||
const res = await request.get<ApiResult<ShopWarehouse>>(
|
||||
'/shop/shop-warehouse/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
53
src/api/shop/shopWarehouse/model/index.ts
Normal file
53
src/api/shop/shopWarehouse/model/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 仓库
|
||||
*/
|
||||
export interface ShopWarehouse {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
// 仓库名称
|
||||
name?: string;
|
||||
// 唯一标识
|
||||
code?: string;
|
||||
// 类型 中心仓,区域仓,门店仓
|
||||
type?: string;
|
||||
// 仓库地址
|
||||
address?: string;
|
||||
// 真实姓名
|
||||
realName?: string;
|
||||
// 联系电话
|
||||
phone?: string;
|
||||
// 省份
|
||||
province?: string;
|
||||
// 城市
|
||||
city: undefined,
|
||||
// 区域
|
||||
region: undefined,
|
||||
// 经纬度
|
||||
lngAndLat?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 状态
|
||||
status?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仓库搜索条件
|
||||
*/
|
||||
export interface ShopWarehouseParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
@@ -30,3 +30,18 @@ export async function updateUserRole(data: UserRole) {
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户角色
|
||||
* 说明:部分后端实现为 POST 新增、PUT 修改;这里补齐 API 以便新用户无角色时可以创建默认角色。
|
||||
*/
|
||||
export async function addUserRole(data: UserRole) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/system/user-role',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
@@ -43,6 +43,15 @@ export interface UserOrderStats {
|
||||
total: number
|
||||
}
|
||||
|
||||
// 用户卡片统计(个人中心头部:余额/积分/优惠券/水票)
|
||||
export interface UserCardStats {
|
||||
balance: string
|
||||
points: number
|
||||
coupons: number
|
||||
giftCards: number
|
||||
lastUpdateTime?: string
|
||||
}
|
||||
|
||||
// 用户完整数据
|
||||
export interface UserDashboard {
|
||||
balance: UserBalance
|
||||
@@ -108,6 +117,17 @@ export async function getUserOrderStats() {
|
||||
return Promise.reject(new Error(res.message))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户卡片统计(一次性返回余额/积分/可用优惠券/未使用礼品卡数量)
|
||||
*/
|
||||
export async function getUserCardStats() {
|
||||
const res = await request.get<ApiResult<UserCardStats>>('/user/card/stats')
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data
|
||||
}
|
||||
return Promise.reject(new Error(res.message))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户完整仪表板数据(一次性获取所有数据)
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,6 @@ export default {
|
||||
"root": "passport",
|
||||
"pages": [
|
||||
"login",
|
||||
"register",
|
||||
"forget",
|
||||
"setting",
|
||||
"agreement",
|
||||
@@ -54,10 +53,14 @@ export default {
|
||||
"wallet/wallet",
|
||||
"coupon/index",
|
||||
"points/points",
|
||||
"gift/index",
|
||||
"gift/redeem",
|
||||
"gift/detail",
|
||||
"ticket/index",
|
||||
"ticket/detail",
|
||||
// "gift/index",
|
||||
// "gift/redeem",
|
||||
// "gift/detail",
|
||||
// "gift/add",
|
||||
"store/verification",
|
||||
"store/orders/index",
|
||||
"theme/index",
|
||||
"poster/poster",
|
||||
"chat/conversation/index",
|
||||
@@ -73,6 +76,7 @@ export default {
|
||||
"apply/add",
|
||||
"withdraw/index",
|
||||
"orders/index",
|
||||
"capital/index",
|
||||
"team/index",
|
||||
"qrcode/index",
|
||||
"invite-stats/index",
|
||||
@@ -90,6 +94,20 @@ export default {
|
||||
'comments/index',
|
||||
'search/index']
|
||||
},
|
||||
{
|
||||
"root": "store",
|
||||
"pages": [
|
||||
"index",
|
||||
"orders/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "rider",
|
||||
"pages": [
|
||||
"index",
|
||||
"orders/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "admin",
|
||||
"pages": [
|
||||
@@ -116,12 +134,6 @@ export default {
|
||||
selectedIconPath: "assets/tabbar/home-active.png",
|
||||
text: "首页",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/category/index",
|
||||
iconPath: "assets/tabbar/category.png",
|
||||
selectedIconPath: "assets/tabbar/category-active.png",
|
||||
text: "基地生活",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/cart/cart",
|
||||
iconPath: "assets/tabbar/cart.png",
|
||||
@@ -144,6 +156,9 @@ export default {
|
||||
permission: {
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||
},
|
||||
"scope.writePhotosAlbum": {
|
||||
"desc": "用于保存小程序码到相册,方便分享给好友"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ function Category() {
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: `${nav?.categoryName}_时里院子市集`,
|
||||
title: `${nav?.categoryName}_桂乐淘`,
|
||||
path: `/shop/category/index?id=${categoryId}`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface GiftCardProps {
|
||||
faceValue?: string
|
||||
/** 商品原价 */
|
||||
originalPrice?: string
|
||||
/** 礼品卡类型:10-实物礼品卡 20-虚拟礼品卡 30-服务礼品卡 */
|
||||
/** 礼品卡类型:10-礼品劵 20-虚拟礼品卡 30-服务礼品卡 */
|
||||
type?: number
|
||||
/** 状态:0-未使用 1-已使用 2-失效 */
|
||||
status?: number
|
||||
@@ -112,10 +112,10 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 10: return '礼品劵'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
default: return '水票'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const GiftCardGuide: React.FC<GiftCardGuideProps> = ({
|
||||
title: '礼品卡类型说明',
|
||||
icon: <Gift size="24" className="text-purple-500" />,
|
||||
content: [
|
||||
'🎁 实物礼品卡:需到指定地址领取商品',
|
||||
'🎁 礼品劵:需到指定地址领取商品',
|
||||
'💻 虚拟礼品卡:自动发放到账户余额',
|
||||
'🛎️ 服务礼品卡:联系客服预约服务',
|
||||
'⏰ 注意查看有效期,过期无法使用'
|
||||
|
||||
@@ -28,10 +28,10 @@ const GiftCardShare: React.FC<GiftCardShareProps> = ({
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (giftCard.type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 10: return '礼品劵'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
default: return '水票'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
||||
setTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '是否继续扫码核销其他礼品卡?',
|
||||
content: '是否继续扫码核销其他水票/礼品卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleClick(); // 递归调用继续扫码
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '邀请注册',
|
||||
navigationBarTitleText: '注册会员',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
|
||||
@@ -10,7 +10,9 @@ import {updateUser} from "@/api/system/user";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite";
|
||||
import {addShopDealerUser} from "@/api/shop/shopDealerUser";
|
||||
import {listUserRole, updateUserRole} from "@/api/system/userRole";
|
||||
import {addUserRole, listUserRole, updateUserRole} from "@/api/system/userRole";
|
||||
import { listRoles } from "@/api/system/role";
|
||||
import type { UserRole } from "@/api/system/userRole/model";
|
||||
|
||||
// 类型定义
|
||||
interface ChooseAvatarEvent {
|
||||
@@ -26,7 +28,7 @@ interface InputEvent {
|
||||
}
|
||||
|
||||
const AddUserAddress = () => {
|
||||
const {user, loginUser} = useUser()
|
||||
const {user, loginUser, fetchUserInfo} = useUser()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [FormData, setFormData] = useState<User>()
|
||||
const formRef = useRef<any>(null)
|
||||
@@ -127,7 +129,7 @@ const AddUserAddress = () => {
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
const submitSucceed = async (values: User) => {
|
||||
try {
|
||||
// 验证必填字段
|
||||
if (!values.phone && !FormData?.phone) {
|
||||
@@ -176,12 +178,27 @@ const AddUserAddress = () => {
|
||||
}
|
||||
console.log(values,FormData)
|
||||
|
||||
const roles = await listUserRole({userId: user?.userId})
|
||||
console.log(roles, 'roles...')
|
||||
if (!user?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息缺失,请先登录',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let roles: UserRole[] = [];
|
||||
try {
|
||||
roles = await listUserRole({userId: user.userId})
|
||||
console.log(roles, 'roles...')
|
||||
} catch (e) {
|
||||
// 新用户/权限限制时可能查不到角色列表,不影响基础注册流程
|
||||
console.warn('查询用户角色失败,将尝试直接写入默认角色:', e)
|
||||
roles = []
|
||||
}
|
||||
|
||||
// 准备提交的数据
|
||||
await updateUser({
|
||||
userId: user?.userId,
|
||||
userId: user.userId,
|
||||
nickname: values.realName || FormData?.nickname,
|
||||
phone: values.phone || FormData?.phone,
|
||||
avatar: values.avatar || FormData?.avatar,
|
||||
@@ -189,17 +206,52 @@ const AddUserAddress = () => {
|
||||
});
|
||||
|
||||
await addShopDealerUser({
|
||||
userId: user?.userId,
|
||||
userId: user.userId,
|
||||
realName: values.realName || FormData?.nickname,
|
||||
mobile: values.phone || FormData?.phone,
|
||||
refereeId: values.refereeId || FormData?.refereeId
|
||||
refereeId: Number(values.refereeId) || Number(FormData?.refereeId)
|
||||
})
|
||||
|
||||
if (roles.length > 0) {
|
||||
await updateUserRole({
|
||||
...roles[0],
|
||||
roleId: 1848
|
||||
})
|
||||
// 角色为空时这里会导致“注册成功但没有角色”,这里做一次兜底写入默认 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 +261,8 @@ const AddUserAddress = () => {
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
// “我的”是 tabBar 页面,注册完成后直接切到“我的”
|
||||
Taro.switchTab({ url: '/pages/user/user' });
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
@@ -382,9 +435,9 @@ const AddUserAddress = () => {
|
||||
>
|
||||
<View className={'bg-gray-100 h-3'}></View>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>
|
||||
<Input placeholder="邀请人ID" disabled={true}/>
|
||||
</Form.Item>
|
||||
{/*<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>*/}
|
||||
{/* <Input placeholder="邀请人ID" disabled={false}/>*/}
|
||||
{/*</Form.Item>*/}
|
||||
<Form.Item name="phone" label="手机号" initialValue={FormData?.phone} required>
|
||||
<View className="flex items-center justify-between">
|
||||
<Input
|
||||
|
||||
4
src/dealer/capital/index.config.ts
Normal file
4
src/dealer/capital/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '收益明细'
|
||||
})
|
||||
|
||||
2
src/dealer/capital/index.scss
Normal file
2
src/dealer/capital/index.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
/* Intentionally empty: styling is done via utility classes. */
|
||||
|
||||
199
src/dealer/capital/index.tsx
Normal file
199
src/dealer/capital/index.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react'
|
||||
import {View, Text, ScrollView} from '@tarojs/components'
|
||||
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {pageShopDealerCapital} from '@/api/shop/shopDealerCapital'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import type {ShopDealerCapital} from '@/api/shop/shopDealerCapital/model'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const DealerCapital: React.FC = () => {
|
||||
const {dealerUser} = useDealerUser()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [records, setRecords] = useState<ShopDealerCapital[]>([])
|
||||
|
||||
const getFlowTypeText = (flowType?: number) => {
|
||||
switch (flowType) {
|
||||
case 10:
|
||||
return '佣金收入'
|
||||
case 20:
|
||||
return '提现支出'
|
||||
case 30:
|
||||
return '转账支出'
|
||||
case 40:
|
||||
return '转账收入'
|
||||
default:
|
||||
return '资金变动'
|
||||
}
|
||||
}
|
||||
|
||||
const getFlowTypeTag = (flowType?: number) => {
|
||||
// 收入:success;支出:danger;其它:default
|
||||
if (flowType === 10 || flowType === 40) return 'success'
|
||||
if (flowType === 20 || flowType === 30) return 'danger'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
const formatMoney = (flowType?: number, money?: string) => {
|
||||
const isIncome = flowType === 10 || flowType === 40
|
||||
const isExpense = flowType === 20 || flowType === 30
|
||||
const sign = isIncome ? '+' : isExpense ? '-' : ''
|
||||
return `${sign}${money || '0.00'}`
|
||||
}
|
||||
|
||||
const fetchRecords = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true)
|
||||
} else if (page === 1) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoadingMore(true)
|
||||
}
|
||||
|
||||
const result = await pageShopDealerCapital({
|
||||
page,
|
||||
limit: PAGE_SIZE,
|
||||
// 只显示与当前登录用户相关的收益明细
|
||||
userId: dealerUser.userId
|
||||
})
|
||||
|
||||
const list = result?.list || []
|
||||
if (page === 1) {
|
||||
setRecords(list)
|
||||
} else {
|
||||
setRecords(prev => [...prev, ...list])
|
||||
}
|
||||
|
||||
setHasMore(list.length === PAGE_SIZE)
|
||||
setCurrentPage(page)
|
||||
} catch (error) {
|
||||
console.error('获取收益明细失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取收益明细失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setRefreshing(false)
|
||||
setLoadingMore(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await fetchRecords(1, true)
|
||||
}
|
||||
|
||||
const handleLoadMore = async () => {
|
||||
if (!loadingMore && hasMore) {
|
||||
await fetchRecords(currentPage + 1)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchRecords(1)
|
||||
}
|
||||
}, [fetchRecords, dealerUser?.userId])
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50">
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshing}
|
||||
pullingText="下拉刷新"
|
||||
canReleaseText="释放刷新"
|
||||
refreshingText="刷新中..."
|
||||
completeText="刷新完成"
|
||||
>
|
||||
<ScrollView
|
||||
scrollY
|
||||
className="h-screen"
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={50}
|
||||
>
|
||||
<View className="p-4">
|
||||
{loading && records.length === 0 ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : records.length > 0 ? (
|
||||
<>
|
||||
{records.map((item) => (
|
||||
<View key={item.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-1">
|
||||
<Text className="font-semibold text-gray-800">
|
||||
{item.describe || '收益明细'}
|
||||
</Text>
|
||||
<Tag type={getFlowTypeTag(item.flowType)}>
|
||||
{getFlowTypeText(item.flowType)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center mb-1">
|
||||
<Text className="text-sm text-gray-400">
|
||||
佣金收入
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-sm font-semibold ${
|
||||
item.flowType === 10 || item.flowType === 40 ? 'text-green-600' :
|
||||
item.flowType === 20 || item.flowType === 30 ? 'text-red-500' :
|
||||
'text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{formatMoney(item.flowType, item.money)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-sm text-gray-400">
|
||||
{/*用户:{item.userId ?? '-'}*/}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">
|
||||
{item.createTime || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{loadingMore && (
|
||||
<View className="text-center py-4">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-1 text-sm">加载更多...</Text>
|
||||
</View>
|
||||
)}
|
||||
{!hasMore && records.length > 0 && (
|
||||
<View className="text-center py-4">
|
||||
<Text className="text-gray-400 text-sm">没有更多数据了</Text>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无收益明细"/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerCapital
|
||||
@@ -108,7 +108,7 @@ const DealerIndex: React.FC = () => {
|
||||
<View className="text-sm" style={{
|
||||
color: 'rgba(255, 255, 255, 0.8)'
|
||||
}}>
|
||||
ID: {dealerUser.userId} | 推荐人: {dealerUser.refereeId || '无'}
|
||||
ID: {dealerUser.userId}
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-right hidden">
|
||||
@@ -132,7 +132,7 @@ const DealerIndex: React.FC = () => {
|
||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-3">
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.available
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
@@ -140,7 +140,7 @@ const DealerIndex: React.FC = () => {
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.frozen
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
@@ -148,7 +148,7 @@ const DealerIndex: React.FC = () => {
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.total
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
|
||||
@@ -24,7 +24,8 @@ const DealerOrders: React.FC = () => {
|
||||
|
||||
// 获取订单数据
|
||||
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||
if (!dealerUser?.userId) return
|
||||
// 需要当前登录用户ID(用于 resourceId 参数)
|
||||
if (!dealerUser || !dealerUser.userId) return
|
||||
|
||||
try {
|
||||
if (isRefresh) {
|
||||
@@ -37,14 +38,17 @@ const DealerOrders: React.FC = () => {
|
||||
|
||||
const result = await pageShopDealerOrder({
|
||||
page,
|
||||
limit: 10
|
||||
limit: 10,
|
||||
// 后端需要 resourceId=当前登录用户ID 才能正确过滤分销订单
|
||||
resourceId: dealerUser.userId
|
||||
})
|
||||
|
||||
if (result?.list) {
|
||||
const newOrders = result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
// 优先使用接口返回的订单号;没有则降级展示 orderId
|
||||
orderNo: order.orderNo ?? (order.orderId != null ? String(order.orderId) : undefined),
|
||||
customerName: `${order.nickname}${order.userId}`,
|
||||
userCommission: order.firstMoney || '0.00'
|
||||
}))
|
||||
|
||||
@@ -102,32 +106,37 @@ const DealerOrders: React.FC = () => {
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
const handleGoCapital = () => {
|
||||
Taro.navigateTo({url: '/dealer/capital/index'})
|
||||
}
|
||||
|
||||
const renderOrderItem = (order: OrderWithDetails) => (
|
||||
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View
|
||||
key={order.id}
|
||||
className="bg-white rounded-lg p-4 mb-3 shadow-sm"
|
||||
onClick={handleGoCapital}
|
||||
>
|
||||
<View className="flex justify-between items-start mb-1">
|
||||
<Text className="font-semibold text-gray-800">
|
||||
订单号:{order.orderNo}
|
||||
订单号:{order.orderNo || '-'}
|
||||
</Text>
|
||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
||||
{getStatusText(order.isSettled, order.isInvalid)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center mb-1">
|
||||
<Text className="text-sm text-gray-400">
|
||||
订单金额:¥{order.orderPrice || '0.00'}
|
||||
</Text>
|
||||
<Text className="text-sm text-orange-500 font-semibold">
|
||||
我的佣金:¥{order.userCommission}
|
||||
</Text>
|
||||
</View>
|
||||
{/*<View className="flex justify-between items-center mb-1">*/}
|
||||
{/* <Text className="text-sm text-gray-400">*/}
|
||||
{/* 订单金额:¥{order.orderPrice || '0.00'}*/}
|
||||
{/* </Text>*/}
|
||||
{/*</View>*/}
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-sm text-gray-400">
|
||||
客户:{order.customerName}
|
||||
{order.createTime}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">
|
||||
{order.createTime}
|
||||
订单金额:¥{order.orderPrice || '0.00'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {businessGradients} from '@/styles/gradients'
|
||||
const DealerQrcode: React.FC = () => {
|
||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [saving, setSaving] = useState<boolean>(false)
|
||||
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||
const {dealerUser} = useDealerUser()
|
||||
@@ -67,6 +68,66 @@ const DealerQrcode: React.FC = () => {
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
const isAlbumAuthError = (errMsg?: string) => {
|
||||
if (!errMsg) return false
|
||||
// WeChat uses variants like: "saveImageToPhotosAlbum:fail auth deny",
|
||||
// "saveImageToPhotosAlbum:fail auth denied", "authorize:fail auth deny"
|
||||
return (
|
||||
errMsg.includes('auth deny') ||
|
||||
errMsg.includes('auth denied') ||
|
||||
errMsg.includes('authorize') ||
|
||||
errMsg.includes('scope.writePhotosAlbum')
|
||||
)
|
||||
}
|
||||
|
||||
const ensureWriteAlbumPermission = async (): Promise<boolean> => {
|
||||
try {
|
||||
const setting = await Taro.getSetting()
|
||||
if (setting?.authSetting?.['scope.writePhotosAlbum']) return true
|
||||
|
||||
await Taro.authorize({scope: 'scope.writePhotosAlbum'})
|
||||
return true
|
||||
} catch (error: any) {
|
||||
const modal = await Taro.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册,请在设置中开启相册权限',
|
||||
confirmText: '去设置'
|
||||
})
|
||||
if (modal.confirm) {
|
||||
await Taro.openSetting()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const downloadImageToLocalPath = async (url: string): Promise<string> => {
|
||||
// saveImageToPhotosAlbum must receive a local temp path (e.g. `http://tmp/...` or `wxfile://...`).
|
||||
// Some environments may return a non-existing temp path from getImageInfo, so we verify.
|
||||
if (url.startsWith('http://tmp/') || url.startsWith('wxfile://')) {
|
||||
return url
|
||||
}
|
||||
|
||||
const token = Taro.getStorageSync('access_token')
|
||||
const tenantId = Taro.getStorageSync('TenantId')
|
||||
const header: Record<string, string> = {}
|
||||
if (token) header.Authorization = token
|
||||
if (tenantId) header.TenantId = tenantId
|
||||
|
||||
// 先下载到本地临时文件再保存到相册
|
||||
const res = await Taro.downloadFile({url, header})
|
||||
if (res.statusCode !== 200 || !res.tempFilePath) {
|
||||
throw new Error(`图片下载失败(${res.statusCode || 'unknown'})`)
|
||||
}
|
||||
|
||||
// Double-check file exists to avoid: saveImageToPhotosAlbum:fail no such file or directory
|
||||
try {
|
||||
await Taro.getFileInfo({filePath: res.tempFilePath})
|
||||
} catch (_) {
|
||||
throw new Error('图片临时文件不存在,请重试')
|
||||
}
|
||||
return res.tempFilePath
|
||||
}
|
||||
|
||||
// 保存小程序码到相册
|
||||
const saveMiniProgramCode = async () => {
|
||||
if (!miniProgramCodeUrl) {
|
||||
@@ -78,39 +139,64 @@ const DealerQrcode: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 先下载图片到本地
|
||||
const res = await Taro.downloadFile({
|
||||
url: miniProgramCodeUrl
|
||||
})
|
||||
if (saving) return
|
||||
setSaving(true)
|
||||
Taro.showLoading({title: '保存中...'})
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
// 保存到相册
|
||||
await Taro.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath
|
||||
})
|
||||
const hasPermission = await ensureWriteAlbumPermission()
|
||||
if (!hasPermission) return
|
||||
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
let filePath = await downloadImageToLocalPath(miniProgramCodeUrl)
|
||||
try {
|
||||
await Taro.saveImageToPhotosAlbum({filePath})
|
||||
} catch (e: any) {
|
||||
const msg = e?.errMsg || e?.message || ''
|
||||
// Fallback: some devices/clients may fail to save directly from a temp path.
|
||||
if (
|
||||
msg.includes('no such file or directory') &&
|
||||
(filePath.startsWith('http://tmp/') || filePath.startsWith('wxfile://'))
|
||||
) {
|
||||
const saved = (await Taro.saveFile({tempFilePath: filePath})) as unknown as { savedFilePath?: string }
|
||||
if (saved?.savedFilePath) {
|
||||
filePath = saved.savedFilePath
|
||||
}
|
||||
await Taro.saveImageToPhotosAlbum({filePath})
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error: any) {
|
||||
if (error.errMsg?.includes('auth deny')) {
|
||||
Taro.showModal({
|
||||
const errMsg = error?.errMsg || error?.message
|
||||
if (errMsg?.includes('cancel')) {
|
||||
Taro.showToast({title: '已取消', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
if (isAlbumAuthError(errMsg)) {
|
||||
const modal = await Taro.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.openSetting()
|
||||
}
|
||||
}
|
||||
confirmText: '去设置'
|
||||
})
|
||||
if (modal.confirm) {
|
||||
await Taro.openSetting()
|
||||
}
|
||||
} else {
|
||||
Taro.showToast({
|
||||
// Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
|
||||
await Taro.showModal({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
content: errMsg || '保存失败,请稍后重试',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +212,7 @@ const DealerQrcode: React.FC = () => {
|
||||
//
|
||||
// const inviteText = `🎉 邀请您加入我的团队!
|
||||
//
|
||||
// 扫描小程序码或搜索"时里院子市集"小程序,即可享受优质商品和服务!
|
||||
// 扫描小程序码或搜索"桂乐淘"小程序,即可享受优质商品和服务!
|
||||
//
|
||||
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
||||
// 🎁 新用户专享优惠等你来拿
|
||||
@@ -258,7 +344,7 @@ const DealerQrcode: React.FC = () => {
|
||||
block
|
||||
icon={<Download/>}
|
||||
onClick={saveMiniProgramCode}
|
||||
disabled={!miniProgramCodeUrl || loading}
|
||||
disabled={!miniProgramCodeUrl || loading || saving}
|
||||
>
|
||||
保存小程序码到相册
|
||||
</Button>
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react'
|
||||
import DealerWithdraw from '../index'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import * as withdrawAPI from '@/api/shop/shopDealerWithdraw'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/hooks/useDealerUser')
|
||||
jest.mock('@/api/shop/shopDealerWithdraw')
|
||||
jest.mock('@tarojs/taro', () => ({
|
||||
showToast: jest.fn(),
|
||||
getStorageSync: jest.fn(() => 123),
|
||||
}))
|
||||
|
||||
const mockUseDealerUser = useDealerUser as jest.MockedFunction<typeof useDealerUser>
|
||||
const mockAddShopDealerWithdraw = withdrawAPI.addShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.addShopDealerWithdraw>
|
||||
const mockPageShopDealerWithdraw = withdrawAPI.pageShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.pageShopDealerWithdraw>
|
||||
|
||||
describe('DealerWithdraw', () => {
|
||||
const mockDealerUser = {
|
||||
userId: 123,
|
||||
money: '10000.00',
|
||||
realName: '测试用户',
|
||||
mobile: '13800138000'
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseDealerUser.mockReturnValue({
|
||||
dealerUser: mockDealerUser,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: jest.fn()
|
||||
})
|
||||
|
||||
mockPageShopDealerWithdraw.mockResolvedValue({
|
||||
list: [],
|
||||
count: 0
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('应该正确显示可提现余额', () => {
|
||||
const { getByText } = render(<DealerWithdraw />)
|
||||
expect(getByText('10000.00')).toBeInTheDocument()
|
||||
expect(getByText('可提现余额')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('应该验证最低提现金额', async () => {
|
||||
mockAddShopDealerWithdraw.mockResolvedValue('success')
|
||||
|
||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||
|
||||
// 输入低于最低金额的数值
|
||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||
fireEvent.change(amountInput, { target: { value: '50' } })
|
||||
|
||||
// 选择提现方式
|
||||
const wechatRadio = getByText('微信钱包')
|
||||
fireEvent.click(wechatRadio)
|
||||
|
||||
// 提交表单
|
||||
const submitButton = getByText('申请提现')
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||
title: '最低提现金额为100元',
|
||||
icon: 'error'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('应该验证提现金额不超过可用余额', async () => {
|
||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||
|
||||
// 输入超过可用余额的金额
|
||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||
fireEvent.change(amountInput, { target: { value: '20000' } })
|
||||
|
||||
// 选择提现方式
|
||||
const wechatRadio = getByText('微信钱包')
|
||||
fireEvent.click(wechatRadio)
|
||||
|
||||
// 提交表单
|
||||
const submitButton = getByText('申请提现')
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||
title: '提现金额超过可用余额',
|
||||
icon: 'error'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('应该验证支付宝账户信息完整性', async () => {
|
||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||
|
||||
// 输入有效金额
|
||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||
fireEvent.change(amountInput, { target: { value: '1000' } })
|
||||
|
||||
// 选择支付宝提现
|
||||
const alipayRadio = getByText('支付宝')
|
||||
fireEvent.click(alipayRadio)
|
||||
|
||||
// 只填写账号,不填写姓名
|
||||
const accountInput = getByPlaceholderText('请输入支付宝账号')
|
||||
fireEvent.change(accountInput, { target: { value: 'test@alipay.com' } })
|
||||
|
||||
// 提交表单
|
||||
const submitButton = getByText('申请提现')
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||
title: '请填写完整的支付宝信息',
|
||||
icon: 'error'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('应该成功提交微信提现申请', async () => {
|
||||
mockAddShopDealerWithdraw.mockResolvedValue('success')
|
||||
|
||||
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||
|
||||
// 输入有效金额
|
||||
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||
fireEvent.change(amountInput, { target: { value: '1000' } })
|
||||
|
||||
// 选择微信提现
|
||||
const wechatRadio = getByText('微信钱包')
|
||||
fireEvent.click(wechatRadio)
|
||||
|
||||
// 提交表单
|
||||
const submitButton = getByText('申请提现')
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddShopDealerWithdraw).toHaveBeenCalledWith({
|
||||
userId: 123,
|
||||
money: '1000',
|
||||
payType: 10,
|
||||
applyStatus: 10,
|
||||
platform: 'MiniProgram'
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||
title: '提现申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('快捷金额按钮应该正常工作', () => {
|
||||
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
|
||||
|
||||
// 点击快捷金额按钮
|
||||
const quickAmountButton = getByText('500')
|
||||
fireEvent.click(quickAmountButton)
|
||||
|
||||
// 验证金额输入框的值
|
||||
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
|
||||
expect(amountInput.value).toBe('500')
|
||||
})
|
||||
|
||||
test('全部按钮应该设置为可用余额', () => {
|
||||
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
|
||||
|
||||
// 点击全部按钮
|
||||
const allButton = getByText('全部')
|
||||
fireEvent.click(allButton)
|
||||
|
||||
// 验证金额输入框的值
|
||||
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
|
||||
expect(amountInput.value).toBe('10000.00')
|
||||
})
|
||||
})
|
||||
@@ -1,13 +1,11 @@
|
||||
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {
|
||||
Cell,
|
||||
Space,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
CellGroup,
|
||||
Radio,
|
||||
Tabs,
|
||||
Tag,
|
||||
Empty,
|
||||
@@ -18,19 +16,91 @@ import {Wallet} from '@nutui/icons-react-taro'
|
||||
import {businessGradients} from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
|
||||
import {
|
||||
pageShopDealerWithdraw,
|
||||
addShopDealerWithdraw,
|
||||
receiveShopDealerWithdraw,
|
||||
receiveSuccessShopDealerWithdraw
|
||||
} from '@/api/shop/shopDealerWithdraw'
|
||||
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
|
||||
|
||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||
accountDisplay?: string
|
||||
// Backend may include these fields for WeChat "confirm receipt" flow after approval.
|
||||
package_info?: string
|
||||
packageInfo?: string
|
||||
package?: string
|
||||
}
|
||||
|
||||
const extractPackageInfo = (result: unknown): string | null => {
|
||||
if (typeof result === 'string') return result
|
||||
if (!result || typeof result !== 'object') return null
|
||||
const r = result as any
|
||||
return (
|
||||
r.package_info ??
|
||||
r.packageInfo ??
|
||||
r.package ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
const canRequestMerchantTransferConfirm = (): boolean => {
|
||||
try {
|
||||
if (typeof (Taro as any).getEnv === 'function' && (Taro as any).ENV_TYPE) {
|
||||
const env = (Taro as any).getEnv()
|
||||
if (env !== (Taro as any).ENV_TYPE.WEAPP) return false
|
||||
}
|
||||
|
||||
const api =
|
||||
(globalThis as any).wx?.requestMerchantTransfer ||
|
||||
(Taro as any).requestMerchantTransfer
|
||||
|
||||
return typeof api === 'function'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const requestMerchantTransferConfirm = (packageInfo: string): Promise<any> => {
|
||||
if (!canRequestMerchantTransferConfirm()) {
|
||||
return Promise.reject(new Error('请在微信小程序内完成收款确认'))
|
||||
}
|
||||
|
||||
// Backend may wrap/format base64 with newlines; WeChat API requires a clean string.
|
||||
const cleanPackageInfo = String(packageInfo).replace(/\s+/g, '')
|
||||
|
||||
const api =
|
||||
(globalThis as any).wx?.requestMerchantTransfer ||
|
||||
(Taro as any).requestMerchantTransfer
|
||||
|
||||
if (typeof api !== 'function') {
|
||||
return Promise.reject(new Error('当前环境不支持商家转账收款确认(缺少 requestMerchantTransfer)'))
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
api({
|
||||
// WeChat API uses `package`, backend returns `package_info`.
|
||||
package: cleanPackageInfo,
|
||||
mchId: '1737910695',
|
||||
appId: 'wxad831ba00ad6a026',
|
||||
success: (res: any) => resolve(res),
|
||||
fail: (err: any) => reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Some backends may return money fields as number; keep internal usage always as string.
|
||||
const normalizeMoneyString = (money: unknown) => {
|
||||
if (money === null || money === undefined || money === '') return '0.00'
|
||||
return typeof money === 'string' ? money : String(money)
|
||||
}
|
||||
|
||||
const DealerWithdraw: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string | number>('0')
|
||||
const [selectedAccount, setSelectedAccount] = useState('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||
const [claimingId, setClaimingId] = useState<number | null>(null)
|
||||
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
|
||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||
const formRef = useRef<any>(null)
|
||||
@@ -52,7 +122,7 @@ const DealerWithdraw: React.FC = () => {
|
||||
const fetchBalance = useCallback(async () => {
|
||||
console.log(dealerUser, 'dealerUser...')
|
||||
try {
|
||||
setAvailableAmount(dealerUser?.money || '0.00')
|
||||
setAvailableAmount(normalizeMoneyString(dealerUser?.money))
|
||||
} catch (error) {
|
||||
console.error('获取余额失败:', error)
|
||||
}
|
||||
@@ -120,7 +190,7 @@ const DealerWithdraw: React.FC = () => {
|
||||
case 40:
|
||||
return '已到账'
|
||||
case 20:
|
||||
return '审核通过'
|
||||
return '待领取'
|
||||
case 10:
|
||||
return '待审核'
|
||||
case 30:
|
||||
@@ -135,7 +205,7 @@ const DealerWithdraw: React.FC = () => {
|
||||
case 40:
|
||||
return 'success'
|
||||
case 20:
|
||||
return 'success'
|
||||
return 'info'
|
||||
case 10:
|
||||
return 'warning'
|
||||
case 30:
|
||||
@@ -154,17 +224,9 @@ const DealerWithdraw: React.FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!values.accountType) {
|
||||
Taro.showToast({
|
||||
title: '请选择提现方式',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证提现金额
|
||||
const amount = parseFloat(values.amount)
|
||||
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
||||
const amount = parseFloat(String(values.amount))
|
||||
const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, ''))
|
||||
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
Taro.showToast({
|
||||
@@ -175,11 +237,11 @@ const DealerWithdraw: React.FC = () => {
|
||||
}
|
||||
|
||||
if (amount < 100) {
|
||||
Taro.showToast({
|
||||
title: '最低提现金额为100元',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
// Taro.showToast({
|
||||
// title: '最低提现金额为100元',
|
||||
// icon: 'error'
|
||||
// })
|
||||
// return
|
||||
}
|
||||
|
||||
if (amount > available) {
|
||||
@@ -190,57 +252,27 @@ const DealerWithdraw: React.FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证账户信息
|
||||
if (values.accountType === 'alipay') {
|
||||
if (!values.account || !values.accountName) {
|
||||
Taro.showToast({
|
||||
title: '请填写完整的支付宝信息',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
} else if (values.accountType === 'bank') {
|
||||
if (!values.account || !values.accountName || !values.bankName) {
|
||||
Taro.showToast({
|
||||
title: '请填写完整的银行卡信息',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true)
|
||||
|
||||
const withdrawData: ShopDealerWithdraw = {
|
||||
userId: dealerUser.userId,
|
||||
money: values.amount,
|
||||
payType: values.accountType === 'wechat' ? 10 :
|
||||
values.accountType === 'alipay' ? 20 : 30,
|
||||
// Only support WeChat wallet withdrawals.
|
||||
payType: 10,
|
||||
applyStatus: 10, // 待审核
|
||||
platform: 'MiniProgram'
|
||||
}
|
||||
|
||||
// 根据提现方式设置账户信息
|
||||
if (values.accountType === 'alipay') {
|
||||
withdrawData.alipayAccount = values.account
|
||||
withdrawData.alipayName = values.accountName
|
||||
} else if (values.accountType === 'bank') {
|
||||
withdrawData.bankCard = values.account
|
||||
withdrawData.bankAccount = values.accountName
|
||||
withdrawData.bankName = values.bankName || '银行卡'
|
||||
}
|
||||
|
||||
// Security flow:
|
||||
// 1) user submits => applyStatus=10 (待审核)
|
||||
// 2) backend审核通过 => applyStatus=20 (待领取)
|
||||
// 3) user goes to records to "领取" => applyStatus=40 (已到账)
|
||||
await addShopDealerWithdraw(withdrawData)
|
||||
|
||||
Taro.showToast({
|
||||
title: '提现申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
Taro.showToast({title: '提现申请已提交,等待审核', icon: 'success'})
|
||||
|
||||
// 重置表单
|
||||
formRef.current?.resetFields()
|
||||
setSelectedAccount('')
|
||||
|
||||
// 刷新数据
|
||||
await handleRefresh()
|
||||
@@ -259,6 +291,65 @@ const DealerWithdraw: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleClaim = async (record: WithdrawRecordWithDetails) => {
|
||||
if (!record?.id) {
|
||||
Taro.showToast({title: '记录不存在', icon: 'error'})
|
||||
return
|
||||
}
|
||||
|
||||
if (record.applyStatus !== 20) {
|
||||
Taro.showToast({title: '当前状态不可领取', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
if (record.payType !== 10) {
|
||||
Taro.showToast({title: '仅支持微信提现领取', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
if (claimingId !== null) return
|
||||
|
||||
try {
|
||||
setClaimingId(record.id)
|
||||
|
||||
if (!canRequestMerchantTransferConfirm()) {
|
||||
throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作')
|
||||
}
|
||||
|
||||
const receiveResult = await receiveShopDealerWithdraw(record.id)
|
||||
const packageInfo = extractPackageInfo(receiveResult)
|
||||
if (!packageInfo) {
|
||||
throw new Error('后台未返回 package_info,无法领取,请联系管理员')
|
||||
}
|
||||
|
||||
try {
|
||||
await requestMerchantTransferConfirm(packageInfo)
|
||||
} catch (e: any) {
|
||||
const msg = String(e?.errMsg || e?.message || '')
|
||||
if (/cancel/i.test(msg)) {
|
||||
Taro.showToast({title: '已取消领取', icon: 'none'})
|
||||
return
|
||||
}
|
||||
throw new Error(msg || '领取失败,请稍后重试')
|
||||
}
|
||||
|
||||
try {
|
||||
await receiveSuccessShopDealerWithdraw(record.id)
|
||||
Taro.showToast({title: '领取成功', icon: 'success'})
|
||||
} catch (e: any) {
|
||||
console.warn('领取成功,但状态同步失败:', e)
|
||||
Taro.showToast({title: '已收款,状态更新失败,请稍后刷新', icon: 'none'})
|
||||
} finally {
|
||||
await handleRefresh()
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('领取失败:', e)
|
||||
Taro.showToast({title: e?.message || '领取失败', icon: 'error'})
|
||||
} finally {
|
||||
setClaimingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
const quickAmounts = ['100', '300', '500', '1000']
|
||||
|
||||
const setQuickAmount = (amount: string) => {
|
||||
@@ -266,13 +357,13 @@ const DealerWithdraw: React.FC = () => {
|
||||
}
|
||||
|
||||
const setAllAmount = () => {
|
||||
formRef.current?.setFieldsValue({amount: availableAmount.replace(/,/g, '')})
|
||||
formRef.current?.setFieldsValue({amount: normalizeMoneyString(availableAmount).replace(/,/g, '')})
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (money?: string) => {
|
||||
if (!money) return '0.00'
|
||||
return parseFloat(money).toFixed(2)
|
||||
const formatMoney = (money?: unknown) => {
|
||||
const n = parseFloat(normalizeMoneyString(money).replace(/,/g, ''))
|
||||
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||||
}
|
||||
|
||||
const renderWithdrawForm = () => (
|
||||
@@ -318,14 +409,6 @@ const DealerWithdraw: React.FC = () => {
|
||||
<Input
|
||||
placeholder="请输入提现金额"
|
||||
type="number"
|
||||
onChange={(value) => {
|
||||
// 实时验证提现金额
|
||||
const amount = parseFloat(value)
|
||||
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
||||
if (!isNaN(amount) && amount > available) {
|
||||
// 可以在这里添加实时提示,但不阻止输入
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -353,54 +436,11 @@ const DealerWithdraw: React.FC = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Form.Item name="accountType" label="提现方式" required>
|
||||
<Radio.Group value={selectedAccount} onChange={() => setSelectedAccount}>
|
||||
<Cell.Group>
|
||||
<Cell>
|
||||
<Radio value="wechat">微信钱包</Radio>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Radio value="alipay">支付宝</Radio>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Radio value="bank">银行卡</Radio>
|
||||
</Cell>
|
||||
</Cell.Group>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{selectedAccount === 'alipay' && (
|
||||
<>
|
||||
<Form.Item name="account" label="支付宝账号" required>
|
||||
<Input placeholder="请输入支付宝账号"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="支付宝姓名" required>
|
||||
<Input placeholder="请输入支付宝实名姓名"/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedAccount === 'bank' && (
|
||||
<>
|
||||
<Form.Item name="bankName" label="开户银行" required>
|
||||
<Input placeholder="请输入开户银行名称"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="account" label="银行卡号" required>
|
||||
<Input placeholder="请输入银行卡号"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="开户姓名" required>
|
||||
<Input placeholder="请输入银行卡开户姓名"/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedAccount === 'wechat' && (
|
||||
<View className="px-4 py-2">
|
||||
<Text className="text-sm text-gray-500">
|
||||
微信钱包提现将直接转入您的微信零钱
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className="px-4 py-2">
|
||||
<Text className="text-sm text-gray-500">
|
||||
提现方式:微信钱包(提交后进入“待审核”,审核通过后请到“提现记录”点击“领取到微信零钱”完成收款确认)
|
||||
</Text>
|
||||
</View>
|
||||
</CellGroup>
|
||||
|
||||
<View className="mt-6 px-4">
|
||||
@@ -409,7 +449,7 @@ const DealerWithdraw: React.FC = () => {
|
||||
type="primary"
|
||||
nativeType="submit"
|
||||
loading={submitting}
|
||||
disabled={submitting || !selectedAccount}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? '提交中...' : '申请提现'}
|
||||
</Button>
|
||||
@@ -433,35 +473,53 @@ const DealerWithdraw: React.FC = () => {
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : withdrawRecords.length > 0 ? (
|
||||
withdrawRecords.map(record => (
|
||||
<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">
|
||||
<Space>
|
||||
<Text className="font-semibold text-gray-800 mb-1">
|
||||
提现金额:¥{record.money}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
提现账户:{record.accountDisplay}
|
||||
</Text>
|
||||
</Space>
|
||||
<Tag type={getStatusColor(record.applyStatus)}>
|
||||
{getStatusText(record.applyStatus)}
|
||||
</Tag>
|
||||
</View>
|
||||
withdrawRecords.map(record => (
|
||||
<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">
|
||||
<Space>
|
||||
<Text className="font-semibold text-gray-800 mb-1">
|
||||
提现金额:¥{record.money}
|
||||
</Text>
|
||||
{/*<Text className="text-sm text-gray-500">*/}
|
||||
{/* 提现账户:{record.accountDisplay}*/}
|
||||
{/*</Text>*/}
|
||||
</Space>
|
||||
<Tag background="#999999" type={getStatusColor(record.applyStatus)} plain>
|
||||
{getStatusText(record.applyStatus)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="text-xs text-gray-400">
|
||||
<Text>申请时间:{record.createTime}</Text>
|
||||
{record.auditTime && (
|
||||
<Text className="block mt-1">
|
||||
审核时间:{new Date(record.auditTime).toLocaleString()}
|
||||
</Text>
|
||||
|
||||
{record.applyStatus === 20 && record.payType === 10 && (
|
||||
<View className="flex mb-5 justify-center">
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
loading={claimingId === record.id}
|
||||
disabled={claimingId !== null}
|
||||
onClick={() => handleClaim(record)}
|
||||
>
|
||||
立即领取
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
{record.rejectReason && (
|
||||
<Text className="block mt-1 text-red-500">
|
||||
驳回原因:{record.rejectReason}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<View className="text-xs text-gray-400">
|
||||
<Text>创建时间:{record.createTime}</Text>
|
||||
{record.auditTime && (
|
||||
<Text className="block mt-1">
|
||||
审核时间:{record.auditTime}
|
||||
</Text>
|
||||
)}
|
||||
{record.rejectReason && (
|
||||
<Text className="block mt-1 text-red-500">
|
||||
驳回原因:{record.rejectReason}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { UserOrderStats } from '@/api/user';
|
||||
import { getUserOrderStats, UserOrderStats } from '@/api/user';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {pageShopOrder} from "@/api/shop/shopOrder";
|
||||
|
||||
/**
|
||||
* 订单统计Hook
|
||||
@@ -31,20 +30,17 @@ export const useOrderStats = () => {
|
||||
if(!Taro.getStorageSync('UserId')){
|
||||
return false;
|
||||
}
|
||||
// TODO 读取订单数量
|
||||
const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0})
|
||||
const paid = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 1})
|
||||
const shipped = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 3})
|
||||
const completed = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 5})
|
||||
const refund = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 6})
|
||||
const total = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId')})
|
||||
|
||||
// 聚合接口:一次请求返回各状态数量(后台按用户做了缓存)
|
||||
const stats = await getUserOrderStats();
|
||||
|
||||
setOrderStats({
|
||||
pending: pending?.count || 0,
|
||||
paid: paid?.count || 0,
|
||||
shipped: shipped?.count || 0,
|
||||
completed: completed?.count || 0,
|
||||
refund: refund?.count || 0,
|
||||
total: total?.count || 0
|
||||
pending: stats?.pending || 0,
|
||||
paid: stats?.paid || 0,
|
||||
shipped: stats?.shipped || 0,
|
||||
completed: stats?.completed || 0,
|
||||
refund: stats?.refund || 0,
|
||||
total: stats?.total || 0
|
||||
})
|
||||
|
||||
if (showToast) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { gradientThemes, GradientTheme, gradientUtils } from '@/styles/gradients'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { gradientThemes, type GradientTheme, gradientUtils } from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface UseThemeReturn {
|
||||
@@ -14,28 +14,42 @@ export interface UseThemeReturn {
|
||||
* 提供主题切换和状态管理功能
|
||||
*/
|
||||
export const useTheme = (): UseThemeReturn => {
|
||||
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(gradientThemes[0])
|
||||
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(true)
|
||||
|
||||
// 获取当前主题
|
||||
const getCurrentTheme = (): GradientTheme => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
|
||||
if (savedTheme === 'auto') {
|
||||
// 自动主题:根据用户ID生成
|
||||
const userId = Taro.getStorageSync('userId') || '1'
|
||||
return gradientUtils.getThemeByUserId(userId)
|
||||
} else {
|
||||
// 手动选择的主题
|
||||
return gradientThemes.find(t => t.name === savedTheme) || gradientThemes[0]
|
||||
const getSavedThemeName = useCallback((): string => {
|
||||
try {
|
||||
return Taro.getStorageSync('user_theme') || 'nature'
|
||||
} catch {
|
||||
return 'nature'
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getStoredUserId = useCallback((): number => {
|
||||
try {
|
||||
const raw = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId')
|
||||
const asNumber = typeof raw === 'number' ? raw : parseInt(String(raw || '1'), 10)
|
||||
return Number.isFinite(asNumber) ? asNumber : 1
|
||||
} catch {
|
||||
return 1
|
||||
}
|
||||
}, [])
|
||||
|
||||
const resolveTheme = useCallback(
|
||||
(themeName: string): GradientTheme => {
|
||||
if (themeName === 'auto') {
|
||||
return gradientUtils.getThemeByUserId(getStoredUserId())
|
||||
}
|
||||
return gradientThemes.find(t => t.name === themeName) || gradientUtils.getThemeByName('nature') || gradientThemes[0]
|
||||
},
|
||||
[getStoredUserId]
|
||||
)
|
||||
|
||||
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(() => getSavedThemeName() === 'auto')
|
||||
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(() => resolveTheme(getSavedThemeName()))
|
||||
|
||||
// 初始化主题
|
||||
useEffect(() => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
const savedTheme = getSavedThemeName()
|
||||
setIsAutoTheme(savedTheme === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
setCurrentTheme(resolveTheme(savedTheme))
|
||||
}, [])
|
||||
|
||||
// 设置主题
|
||||
@@ -43,7 +57,7 @@ export const useTheme = (): UseThemeReturn => {
|
||||
try {
|
||||
Taro.setStorageSync('user_theme', themeName)
|
||||
setIsAutoTheme(themeName === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
setCurrentTheme(resolveTheme(themeName))
|
||||
} catch (error) {
|
||||
console.error('保存主题失败:', error)
|
||||
}
|
||||
@@ -51,7 +65,7 @@ export const useTheme = (): UseThemeReturn => {
|
||||
|
||||
// 刷新主题(用于自动主题模式下用户信息变更时)
|
||||
const refreshTheme = () => {
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
setCurrentTheme(resolveTheme(getSavedThemeName()))
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
parseQRContent
|
||||
} from '@/api/passport/qr-login';
|
||||
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
||||
import { getGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket';
|
||||
import { useUser } from "@/hooks/useUser";
|
||||
import { isValidJSON } from "@/utils/jsonUtils";
|
||||
import dayjs from 'dayjs';
|
||||
@@ -29,6 +30,15 @@ export enum ScanType {
|
||||
UNKNOWN = 'unknown' // 未知类型
|
||||
}
|
||||
|
||||
type VerificationBusinessType = 'gift' | 'ticket';
|
||||
|
||||
interface TicketVerificationPayload {
|
||||
userTicketId: number;
|
||||
qty?: number;
|
||||
userId?: number;
|
||||
t?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码结果
|
||||
*/
|
||||
@@ -73,7 +83,11 @@ export function useUnifiedQRScan() {
|
||||
// 1. 检查是否为JSON格式(核销二维码)
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
// Allow plaintext (non-encrypted) ticket verification payload for debugging/internal use.
|
||||
if (json.userTicketId) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
}
|
||||
@@ -130,35 +144,79 @@ export function useUnifiedQRScan() {
|
||||
throw new Error('您没有核销权限');
|
||||
}
|
||||
|
||||
let code = '';
|
||||
let businessType: VerificationBusinessType = 'gift';
|
||||
let decryptedOrRaw = '';
|
||||
|
||||
// 判断是否为加密的JSON格式
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
// 解密获取核销码
|
||||
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
|
||||
businessType = json.businessType;
|
||||
// 解密获取核销内容
|
||||
const decryptedData = await decryptQrData({
|
||||
token: json.token,
|
||||
encryptedData: json.data
|
||||
});
|
||||
|
||||
if (decryptedData) {
|
||||
code = decryptedData.toString();
|
||||
decryptedOrRaw = decryptedData.toString();
|
||||
} else {
|
||||
throw new Error('解密失败');
|
||||
}
|
||||
} else if (json.userTicketId) {
|
||||
businessType = 'ticket';
|
||||
decryptedOrRaw = scanResult.trim();
|
||||
}
|
||||
} else {
|
||||
// 直接使用扫码结果作为核销码
|
||||
code = scanResult.trim();
|
||||
// 直接使用扫码结果作为核销内容
|
||||
decryptedOrRaw = scanResult.trim();
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
if (!decryptedOrRaw) {
|
||||
throw new Error('无法获取有效的核销码');
|
||||
}
|
||||
|
||||
// 验证核销码
|
||||
const gift = await getShopGiftByCode(code);
|
||||
if (businessType === 'ticket') {
|
||||
if (!isValidJSON(decryptedOrRaw)) {
|
||||
throw new Error('水票核销信息格式错误');
|
||||
}
|
||||
const payload = JSON.parse(decryptedOrRaw) as TicketVerificationPayload;
|
||||
const userTicketId = Number(payload.userTicketId);
|
||||
const qty = Math.max(1, Number(payload.qty || 1));
|
||||
if (!Number.isFinite(userTicketId) || userTicketId <= 0) {
|
||||
throw new Error('水票核销信息无效');
|
||||
}
|
||||
|
||||
const ticket = await getGltUserTicket(userTicketId);
|
||||
if (!ticket) throw new Error('水票不存在');
|
||||
if (ticket.status === 1) throw new Error('该水票已冻结');
|
||||
const available = Number(ticket.availableQty || 0);
|
||||
const used = Number(ticket.usedQty || 0);
|
||||
if (available < qty) throw new Error('水票可用次数不足');
|
||||
|
||||
await updateGltUserTicket({
|
||||
...ticket,
|
||||
availableQty: available - qty,
|
||||
usedQty: used + qty
|
||||
});
|
||||
|
||||
return {
|
||||
type: ScanType.VERIFICATION,
|
||||
data: {
|
||||
businessType: 'ticket',
|
||||
ticket: {
|
||||
...ticket,
|
||||
availableQty: available - qty,
|
||||
usedQty: used + qty
|
||||
},
|
||||
qty
|
||||
},
|
||||
message: `核销成功(已使用${qty}次)`
|
||||
};
|
||||
}
|
||||
|
||||
// 验证礼品卡核销码
|
||||
const gift = await getShopGiftByCode(decryptedOrRaw);
|
||||
|
||||
if (!gift) {
|
||||
throw new Error('核销码无效');
|
||||
@@ -187,7 +245,7 @@ export function useUnifiedQRScan() {
|
||||
|
||||
return {
|
||||
type: ScanType.VERIFICATION,
|
||||
data: gift,
|
||||
data: { businessType: 'gift', gift },
|
||||
message: '核销成功'
|
||||
};
|
||||
}, [isAdmin]);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {pageShopUserCoupon} from "@/api/shop/shopUserCoupon";
|
||||
import {pageShopGift} from "@/api/shop/shopGift";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {getUserInfo} from "@/api/layout";
|
||||
import { getUserCardStats } from '@/api/user'
|
||||
|
||||
interface UserData {
|
||||
balance: number
|
||||
balance: string
|
||||
points: number
|
||||
coupons: number
|
||||
giftCards: number
|
||||
@@ -24,7 +22,7 @@ interface UseUserDataReturn {
|
||||
loading: boolean
|
||||
error: string | null
|
||||
refresh: () => Promise<void>
|
||||
updateBalance: (newBalance: number) => void
|
||||
updateBalance: (newBalance: string) => void
|
||||
updatePoints: (newPoints: number) => void
|
||||
}
|
||||
|
||||
@@ -43,18 +41,14 @@ export const useUserData = (): UseUserDataReturn => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 并发请求所有数据
|
||||
const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
|
||||
getUserInfo(),
|
||||
pageShopUserCoupon({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0}),
|
||||
pageShopGift({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0})
|
||||
])
|
||||
// 聚合接口:一次请求返回余额/积分/优惠券/礼品卡统计(后端可按用户做缓存)
|
||||
const stats = await getUserCardStats()
|
||||
|
||||
const newData: UserData = {
|
||||
balance: userDataRes?.balance || 0.00,
|
||||
points: userDataRes?.points || 0,
|
||||
coupons: couponsRes?.count || 0,
|
||||
giftCards: giftCardsRes?.count || 0,
|
||||
balance: stats?.balance || '0.00',
|
||||
points: stats?.points || 0,
|
||||
coupons: stats?.coupons || 0,
|
||||
giftCards: stats?.giftCards || 0,
|
||||
orders: {
|
||||
pending: 0,
|
||||
paid: 0,
|
||||
@@ -78,7 +72,7 @@ export const useUserData = (): UseUserDataReturn => {
|
||||
}, [fetchUserData])
|
||||
|
||||
// 更新余额(本地更新,避免频繁请求)
|
||||
const updateBalance = useCallback((newBalance: number) => {
|
||||
const updateBalance = useCallback((newBalance: string) => {
|
||||
setData(prev => prev ? { ...prev, balance: newBalance } : null)
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ function Cart() {
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: '购物车 - 时里院子市集',
|
||||
title: '购物车 - 桂乐淘',
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
},
|
||||
|
||||
@@ -49,7 +49,7 @@ function Category() {
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: `${nav?.categoryName}_时里院子市集`,
|
||||
title: `${nav?.categoryName}_桂乐淘`,
|
||||
path: `/shop/category/index?id=${categoryId}`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
|
||||
@@ -6,13 +6,38 @@ import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getCmsAdByCode} from "@/api/cms/cmsAd";
|
||||
import navTo from "@/utils/common";
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
|
||||
type AdImage = {
|
||||
url?: string
|
||||
path?: string
|
||||
title?: string
|
||||
// Compatible keys (some backends use different fields)
|
||||
src?: string
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
function normalizeAdImages(ad?: CmsAd): AdImage[] {
|
||||
const list = ad?.imageList
|
||||
if (Array.isArray(list) && list.length) return list as AdImage[]
|
||||
|
||||
// Some APIs only return `images` as a JSON string.
|
||||
const raw = ad?.images
|
||||
if (!raw) return []
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
return Array.isArray(parsed) ? (parsed as AdImage[]) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function toNumberPx(input: unknown, fallback: number) {
|
||||
const n = typeof input === 'number' ? input : Number.parseInt(String(input ?? ''), 10)
|
||||
return Number.isFinite(n) ? n : fallback
|
||||
}
|
||||
|
||||
const MyPage = () => {
|
||||
const [carouselData, setCarouselData] = useState<CmsAd>()
|
||||
const [hotToday, setHotToday] = useState<CmsAd>()
|
||||
const [item, setItem] = useState<CmsArticle>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
// const [disableSwiper, setDisableSwiper] = useState(false)
|
||||
|
||||
@@ -21,24 +46,21 @@ const MyPage = () => {
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
setLoading(true)
|
||||
// 轮播图
|
||||
const flash = await getCmsAdByCode('flash')
|
||||
// 今日热卖
|
||||
const hotToday = await getCmsAdByCode('hot_today')
|
||||
// 时里动态
|
||||
const news = await pageCmsArticle({limit:1,recommend:1})
|
||||
// 赋值
|
||||
if(flash){
|
||||
setCarouselData(flash)
|
||||
}
|
||||
if(hotToday){
|
||||
setHotToday(hotToday)
|
||||
}
|
||||
if(news && news.list.length > 0){
|
||||
setItem(news.list[0])
|
||||
const [flashRes] = await Promise.allSettled([
|
||||
getCmsAdByCode('mp-ad'),
|
||||
getCmsAdByCode('hot_today'),
|
||||
pageCmsArticle({ limit: 1, recommend: 1 }),
|
||||
])
|
||||
|
||||
if (flashRes.status === 'fulfilled') {
|
||||
console.log('flashflashflash', flashRes.value)
|
||||
setCarouselData(flashRes.value)
|
||||
} else {
|
||||
console.error('Failed to fetch flash:', flashRes.reason)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Banner数据加载失败:', error)
|
||||
} finally {
|
||||
@@ -47,11 +69,13 @@ const MyPage = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
loadData().then()
|
||||
}, [])
|
||||
|
||||
// 轮播图高度,默认300px
|
||||
const carouselHeight = carouselData?.height || 300;
|
||||
const carouselHeight = toNumberPx(carouselData?.height, 300)
|
||||
const carouselImages = normalizeAdImages(carouselData)
|
||||
console.log(carouselImages,'carouselImages')
|
||||
|
||||
// 骨架屏组件
|
||||
const BannerSkeleton = () => (
|
||||
@@ -100,94 +124,42 @@ const MyPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex p-2 justify-between" style={{height: `${carouselHeight}px`}}>
|
||||
{/* 左侧轮播图区域 */}
|
||||
<View
|
||||
style={{width: '50%', height: '100%'}}
|
||||
className="banner-swiper-container"
|
||||
>
|
||||
<Swiper
|
||||
defaultValue={0}
|
||||
height={carouselHeight}
|
||||
indicator
|
||||
autoPlay
|
||||
duration={3000}
|
||||
style={{
|
||||
height: `${carouselHeight}px`,
|
||||
touchAction: 'pan-y' // 关键修改:允许垂直滑动
|
||||
}}
|
||||
disableTouch={false}
|
||||
direction="horizontal"
|
||||
className="custom-swiper"
|
||||
>
|
||||
{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'}>
|
||||
<Swiper
|
||||
defaultValue={0}
|
||||
height={carouselHeight}
|
||||
indicator
|
||||
autoPlay
|
||||
duration={3000}
|
||||
style={{
|
||||
height: `${carouselHeight}px`,
|
||||
touchAction: 'pan-y' // 关键修改:允许垂直滑动
|
||||
}}
|
||||
disableTouch={false}
|
||||
direction="horizontal"
|
||||
className="custom-swiper"
|
||||
>
|
||||
{carouselImages.map((img, index) => {
|
||||
const src = img.url || img.src || img.imageUrl
|
||||
if (!src) return null
|
||||
return (
|
||||
<Swiper.Item key={index} style={{ touchAction: 'pan-x pan-y' }}>
|
||||
<Image
|
||||
width={'100%'}
|
||||
height={94}
|
||||
src={item?.image}
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={src}
|
||||
mode={'scaleToFill'}
|
||||
onClick={() => (img.path ? navTo(`${img.path}`) : undefined)}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '4px'
|
||||
height: `${carouselHeight}px`,
|
||||
borderRadius: '4px',
|
||||
touchAction: 'manipulation' // 关键修改:优化触摸操作
|
||||
}}
|
||||
onClick={() => navTo('cms/detail/index?id=' + item?.articleId)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Swiper.Item>
|
||||
)
|
||||
})}
|
||||
</Swiper>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro';
|
||||
import {Button, Space, Sticky} from '@nutui/nutui-react-taro'
|
||||
import {Button, Sticky, Popup, Cell, CellGroup} from '@nutui/nutui-react-taro'
|
||||
import {TriangleDown} from '@nutui/icons-react-taro'
|
||||
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
|
||||
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {TenantId, TenantName} from "@/config/app";
|
||||
import {getOrganization} from "@/api/system/organization";
|
||||
import {myUserVerify} from "@/api/system/userVerify";
|
||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
||||
@@ -12,17 +12,141 @@ import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite";
|
||||
import {View,Text} from '@tarojs/components'
|
||||
import MySearch from "./MySearch";
|
||||
import './Header.scss';
|
||||
import {User} from "@/api/system/user/model";
|
||||
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
||||
import type {ShopStore} from "@/api/shop/shopStore/model";
|
||||
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
||||
|
||||
const Header = (_: any) => {
|
||||
// 使用新的useShopInfo Hook
|
||||
const {
|
||||
getWebsiteName,
|
||||
getWebsiteLogo
|
||||
} = useShopInfo();
|
||||
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(true)
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||
const [userInfo] = useState<User>()
|
||||
|
||||
// 门店选择:用于首页展示“最近门店”,并在下单时写入订单 storeId
|
||||
const [storePopupVisible, setStorePopupVisible] = useState(false)
|
||||
const [stores, setStores] = useState<ShopStore[]>([])
|
||||
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
|
||||
const [userLocation, setUserLocation] = useState<{lng: number; lat: number} | null>(null)
|
||||
|
||||
const getTenantName = () => {
|
||||
return userInfo?.tenantName || TenantName
|
||||
}
|
||||
|
||||
const parseStoreCoords = (s: ShopStore): {lng: number; lat: number} | null => {
|
||||
const raw = (s.lngAndLat || s.location || '').trim()
|
||||
if (!raw) return null
|
||||
|
||||
const parts = raw.split(/[,\s]+/).filter(Boolean)
|
||||
if (parts.length < 2) return null
|
||||
|
||||
const a = parseFloat(parts[0])
|
||||
const b = parseFloat(parts[1])
|
||||
if (Number.isNaN(a) || Number.isNaN(b)) return null
|
||||
|
||||
// 常见格式是 "lng,lat";这里做一个简单兜底(经度范围更宽)
|
||||
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90
|
||||
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180
|
||||
if (looksLikeLngLat) return {lng: a, lat: b}
|
||||
if (looksLikeLatLng) return {lng: b, lat: a}
|
||||
return null
|
||||
}
|
||||
|
||||
const distanceMeters = (a: {lng: number; lat: number}, b: {lng: number; lat: number}) => {
|
||||
const toRad = (x: number) => (x * Math.PI) / 180
|
||||
const R = 6371000 // meters
|
||||
const dLat = toRad(b.lat - a.lat)
|
||||
const dLng = toRad(b.lng - a.lng)
|
||||
const lat1 = toRad(a.lat)
|
||||
const lat2 = toRad(b.lat)
|
||||
const sin1 = Math.sin(dLat / 2)
|
||||
const sin2 = Math.sin(dLng / 2)
|
||||
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2
|
||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)))
|
||||
}
|
||||
|
||||
const formatDistance = (meters?: number) => {
|
||||
if (meters === undefined || Number.isNaN(meters)) return ''
|
||||
if (meters < 1000) return `${Math.round(meters)}m`
|
||||
return `${(meters / 1000).toFixed(1)}km`
|
||||
}
|
||||
|
||||
const getStoreDistance = (s: ShopStore) => {
|
||||
if (!userLocation) return undefined
|
||||
const coords = parseStoreCoords(s)
|
||||
if (!coords) return undefined
|
||||
return distanceMeters(userLocation, coords)
|
||||
}
|
||||
|
||||
const initStoreSelection = async () => {
|
||||
// 先读取本地已选门店,避免页面首屏抖动
|
||||
const stored = getSelectedStoreFromStorage()
|
||||
if (stored?.id) {
|
||||
setSelectedStore(stored)
|
||||
}
|
||||
|
||||
// 拉取门店列表(失败时允许用户手动重试/继续使用本地门店)
|
||||
let list: ShopStore[] = []
|
||||
try {
|
||||
list = await listShopStore()
|
||||
} catch (e) {
|
||||
console.error('获取门店列表失败:', e)
|
||||
list = []
|
||||
}
|
||||
const usable = (list || []).filter(s => s?.isDelete !== 1)
|
||||
setStores(usable)
|
||||
|
||||
// 尝试获取定位,用于计算最近门店
|
||||
let loc: {lng: number; lat: number} | null = null
|
||||
try {
|
||||
const r = await Taro.getLocation({type: 'gcj02'})
|
||||
loc = {lng: r.longitude, lat: r.latitude}
|
||||
} catch (e) {
|
||||
// 不强制定位授权;无定位时仍允许用户手动选择
|
||||
console.warn('获取定位失败,将不显示最近门店距离:', e)
|
||||
}
|
||||
setUserLocation(loc)
|
||||
|
||||
const ensureStoreDetail = async (s: ShopStore) => {
|
||||
if (!s?.id) return s
|
||||
// 如果后端已经返回默认仓库等字段,就不额外请求
|
||||
if (s.warehouseId) return s
|
||||
try {
|
||||
const full = await getShopStore(s.id)
|
||||
return full || s
|
||||
} catch (_e) {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// 若用户没有选过门店,则自动选择最近门店(或第一个)
|
||||
const alreadySelected = stored?.id
|
||||
if (alreadySelected || usable.length === 0) return
|
||||
|
||||
let autoPick: ShopStore | undefined
|
||||
if (loc) {
|
||||
autoPick = [...usable]
|
||||
.map(s => {
|
||||
const coords = parseStoreCoords(s)
|
||||
const d = coords ? distanceMeters(loc, coords) : undefined
|
||||
return {s, d}
|
||||
})
|
||||
.sort((x, y) => (x.d ?? Number.POSITIVE_INFINITY) - (y.d ?? Number.POSITIVE_INFINITY))[0]?.s
|
||||
} else {
|
||||
autoPick = usable[0]
|
||||
}
|
||||
|
||||
if (autoPick?.id) {
|
||||
const full = await ensureStoreDetail(autoPick)
|
||||
setSelectedStore(full)
|
||||
saveSelectedStoreToStorage(full)
|
||||
}
|
||||
}
|
||||
|
||||
const reload = async () => {
|
||||
Taro.getSystemInfo({
|
||||
@@ -182,6 +306,7 @@ const Header = (_: any) => {
|
||||
|
||||
useEffect(() => {
|
||||
reload().then()
|
||||
initStoreSelection().then()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -211,31 +336,95 @@ const Header = (_: any) => {
|
||||
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 ? (
|
||||
<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 />*/}
|
||||
<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>
|
||||
</Popup>
|
||||
</Sticky>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ function MySearch(props: any) {
|
||||
|
||||
|
||||
return (
|
||||
<div className={'z-50 left-0 w-full'}>
|
||||
<div className={'z-50 left-0 w-full hidden'}>
|
||||
<div className={'px-2'}>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -4,6 +4,376 @@ page {
|
||||
background: linear-gradient(to bottom, #e9fff2, #ffffff);
|
||||
}
|
||||
|
||||
.home-page {
|
||||
padding: 24rpx 24rpx calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.home-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 28rpx;
|
||||
background: linear-gradient(180deg, #bfefff 0%, #eafaff 40%, #fff7ec 100%);
|
||||
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.home-hero__bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(360rpx 240rpx at 18% 16%, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)),
|
||||
radial-gradient(320rpx 220rpx at 84% 18%, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0)),
|
||||
linear-gradient(180deg, rgba(0, 207, 255, 0.12), rgba(0, 0, 0, 0));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.home-hero__content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
padding: 26rpx 24rpx 28rpx;
|
||||
min-height: 320rpx;
|
||||
}
|
||||
|
||||
.home-hero__left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.home-hero__topRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.home-hero__brand {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 214, 84, 0.92);
|
||||
color: #2a2a2a;
|
||||
font-weight: 700;
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-hero__brandText {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-hero__tag {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 10rpx 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(90deg, #22d64a 0%, #7df4b0 100%);
|
||||
box-shadow: 0 14rpx 24rpx rgba(36, 202, 148, 0.22);
|
||||
}
|
||||
|
||||
.home-hero__tagText {
|
||||
font-size: 56rpx;
|
||||
font-weight: 900;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-hero__date {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.home-hero__dateText {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.home-hero__headline {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.home-hero__headlineText {
|
||||
display: block;
|
||||
font-size: 42rpx;
|
||||
font-weight: 900;
|
||||
color: #0b0b0b;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.home-hero__right {
|
||||
width: 200rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.home-hero__bottle {
|
||||
position: relative;
|
||||
width: 190rpx;
|
||||
height: 250rpx;
|
||||
border-radius: 28rpx;
|
||||
background:
|
||||
radial-gradient(240rpx 360rpx at 60% 30%, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.18)),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.1));
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.65);
|
||||
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.home-hero__bottleCap {
|
||||
position: absolute;
|
||||
top: 14rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 88rpx;
|
||||
height: 26rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(180deg, #d7e6f3, #b0cadd);
|
||||
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.home-hero__bottleLabel {
|
||||
position: absolute;
|
||||
left: 18rpx;
|
||||
right: 18rpx;
|
||||
bottom: 30rpx;
|
||||
padding: 12rpx 12rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(90deg, rgba(0, 150, 255, 0.18), rgba(0, 255, 210, 0.18));
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.home-hero__bottleLabelText {
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: rgba(0, 80, 140, 0.95);
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ticket-card {
|
||||
margin-top: 18rpx;
|
||||
border-radius: 22rpx;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.ticket-card__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 18rpx 20rpx;
|
||||
background: linear-gradient(90deg, #22d64a 0%, #7df4b0 100%);
|
||||
}
|
||||
|
||||
.ticket-card__title {
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.ticket-card__count {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.ticket-card__countNum {
|
||||
color: #ffffff;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.ticket-card__body {
|
||||
padding: 20rpx 10rpx 22rpx;
|
||||
}
|
||||
|
||||
.shortcut-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.shortcut-grid__item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.shortcut-grid__icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
color: #20c26a;
|
||||
border: 2rpx solid rgba(32, 194, 106, 0.35);
|
||||
}
|
||||
|
||||
.shortcut-grid__text {
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.home-tabs {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.home-tabs__inner {
|
||||
display: flex;
|
||||
gap: 18rpx;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
.home-tabs__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.home-tabs__item--active {
|
||||
background: rgba(32, 194, 106, 0.16);
|
||||
}
|
||||
|
||||
.home-tabs__itemText {
|
||||
font-size: 28rpx;
|
||||
color: #2a2a2a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.home-tabs__item--active .home-tabs__itemText {
|
||||
color: #16b65a;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.goods-grid {
|
||||
margin-top: 18rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.goods-card {
|
||||
border-radius: 22rpx;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.goods-card__imgWrap {
|
||||
padding: 18rpx 18rpx 0;
|
||||
}
|
||||
|
||||
.goods-card__img {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
.goods-card__body {
|
||||
padding: 18rpx 18rpx 20rpx;
|
||||
}
|
||||
|
||||
.goods-card__title {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #1c1c1c;
|
||||
min-height: 72rpx;
|
||||
}
|
||||
|
||||
.goods-card__meta {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.goods-card__sold {
|
||||
font-size: 22rpx;
|
||||
color: #9a9a9a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.goods-card__price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
color: #27c86b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.goods-card__priceUnit {
|
||||
font-size: 22rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.goods-card__priceValue {
|
||||
font-size: 36rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.goods-card__actions {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.goods-card__btn {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.goods-card__btn--ghost {
|
||||
border: 2rpx solid rgba(32, 194, 106, 0.7);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.goods-card__btn--primary {
|
||||
background: linear-gradient(90deg, #24d34c 0%, #6df09a 100%);
|
||||
}
|
||||
|
||||
.goods-card__btnText {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #18b85a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.goods-card__btnText--primary {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.buy-btn{
|
||||
height: 70px;
|
||||
background: linear-gradient(to bottom, #1cd98a, #24ca94);
|
||||
@@ -73,4 +443,4 @@ page {
|
||||
.nut-swiper,
|
||||
.nut-swiper-item {
|
||||
-webkit-overflow-scrolling: touch; /* iOS平台启用硬件加速滚动 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import Header from './Header';
|
||||
import BestSellers from './BestSellers';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {useShareAppMessage} from "@tarojs/taro"
|
||||
import {useEffect, useState} from "react";
|
||||
import {getShopInfo} from "@/api/layout";
|
||||
import Menu from "./Menu";
|
||||
import Banner from "./Banner";
|
||||
import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite";
|
||||
import Header from './Header'
|
||||
import Banner from './Banner'
|
||||
import Taro, { useShareAppMessage } from '@tarojs/taro'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { Cart, Coupon, Gift, Ticket } from '@nutui/icons-react-taro'
|
||||
import { getShopInfo } from '@/api/layout'
|
||||
import { checkAndHandleInviteRelation, hasPendingInvite } from '@/utils/invite'
|
||||
import { pageShopGoods } from '@/api/shop/shopGoods'
|
||||
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'
|
||||
import './index.scss'
|
||||
|
||||
function Home() {
|
||||
// 吸顶状态
|
||||
// const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||
// Tabs粘性状态
|
||||
const [_, setTabsStickyStatus] = useState<boolean>(false)
|
||||
const [activeTabKey, setActiveTabKey] = useState('recommend')
|
||||
const [goodsList, setGoodsList] = useState<ShopGoods[]>([])
|
||||
|
||||
useShareAppMessage(() => {
|
||||
// 获取当前用户ID,用于生成邀请链接
|
||||
@@ -85,9 +84,7 @@ function Home() {
|
||||
// }
|
||||
|
||||
// 处理Tabs粘性状态变化
|
||||
const handleTabsStickyChange = (isSticky: boolean) => {
|
||||
setTabsStickyStatus(isSticky)
|
||||
}
|
||||
// const handleTabsStickyChange = (isSticky: boolean) => {}
|
||||
|
||||
const reload = () => {
|
||||
|
||||
@@ -152,16 +149,169 @@ function Home() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
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: () => Taro.navigateTo({ url: '/user/ticket/index' }),
|
||||
},
|
||||
{
|
||||
key: 'order',
|
||||
title: '立即订水',
|
||||
icon: <Cart size={30} />,
|
||||
onClick: () => Taro.navigateTo({ url: '/shop/goodsDetail/index?id=10074' }),
|
||||
},
|
||||
{
|
||||
key: 'invite',
|
||||
title: '邀请有礼',
|
||||
icon: <Gift size={30} />,
|
||||
onClick: () => Taro.navigateTo({ url: '/dealer/qrcode/index' }),
|
||||
},
|
||||
{
|
||||
key: 'coupon',
|
||||
title: '领券中心',
|
||||
icon: <Coupon size={30} />,
|
||||
onClick: () => Taro.navigateTo({ url: '/coupon/index' }),
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const visibleGoods = useMemo(() => {
|
||||
// 先按效果图展示两列卡片,数据不够时也保持布局稳定
|
||||
const list = goodsList || []
|
||||
if (list.length <= 6) return list
|
||||
return list.slice(0, 6)
|
||||
}, [goodsList])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Header区域 - 现在由Header组件内部处理吸顶逻辑 */}
|
||||
<Header />
|
||||
|
||||
<div className={'flex flex-col mt-12'}>
|
||||
<Menu/>
|
||||
<Banner/>
|
||||
<BestSellers onStickyChange={handleTabsStickyChange}/>
|
||||
</div>
|
||||
<View className="home-page">
|
||||
{/* 顶部活动主视觉:使用 Banner 组件 */}
|
||||
<Banner />
|
||||
|
||||
{/* 电子水票 */}
|
||||
<View className="ticket-card">
|
||||
<View className="ticket-card__head">
|
||||
<Text className="ticket-card__title">电子水票</Text>
|
||||
<Text className="ticket-card__count">
|
||||
您还有 <Text className="ticket-card__countNum">0</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={() => Taro.navigateTo({ url: '/user/coupon/index' })}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ const IsDealer = () => {
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Reward className={'text-orange-100 '} size={16}/>
|
||||
<Text style={{fontSize: '16px'}}
|
||||
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '入驻申请'}</Text>
|
||||
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '门店入驻'}</Text>
|
||||
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
|
||||
</View>
|
||||
}
|
||||
@@ -75,8 +75,8 @@ const IsDealer = () => {
|
||||
title={
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Reward className={'text-orange-100 '} size={16}/>
|
||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '开通VIP'}</Text>
|
||||
<Text className={'text-white opacity-80 pl-3'}>{config?.vipComments || '享优惠'}</Text>
|
||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '门店入驻'}</Text>
|
||||
<Text className={'text-white opacity-80 pl-3'}>{config?.vipComments || ''}</Text>
|
||||
</View>
|
||||
}
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
|
||||
@@ -11,15 +11,21 @@ import {useUserData} from "@/hooks/useUserData";
|
||||
import {getStoredInviteParams} from "@/utils/invite";
|
||||
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
||||
import {useThemeStyles} from "@/hooks/useTheme";
|
||||
import {getRootDomain} from "@/utils/domain";
|
||||
|
||||
const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
const {data, refresh} = useUserData()
|
||||
const {getDisplayName, getRoleName} = useUser();
|
||||
const {getDisplayName} = useUser();
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||
const [userInfo, setUserInfo] = useState<User>()
|
||||
|
||||
const themeStyles = useThemeStyles();
|
||||
|
||||
// 角色名称:优先取用户 roles 数组的第一个角色名称
|
||||
const getRoleName = () => {
|
||||
return userInfo?.roles?.[0]?.roleName || userInfo?.roleName || '注册用户'
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
await refresh()
|
||||
@@ -65,6 +71,8 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
setUserInfo(data)
|
||||
setIsLogin(true);
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
|
||||
refresh().then()
|
||||
|
||||
// 获取openId
|
||||
if (!data.openid) {
|
||||
@@ -162,6 +170,8 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
Taro.setStorageSync('UserId', res.data.data.user.userId)
|
||||
setUserInfo(res.data.data.user)
|
||||
setIsLogin(true)
|
||||
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
|
||||
refresh().then()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -189,7 +199,9 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
/>
|
||||
<View className={'flex flex-col'}>
|
||||
<Text style={{color: '#ffffff'}}>{getDisplayName() || '点击登录'}</Text>
|
||||
<View><Tag type="success">{getRoleName()}</Tag></View>
|
||||
{getRootDomain() && (
|
||||
<View><Tag type="success">{getRoleName()}</Tag></View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -221,10 +233,27 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
console.log('统一扫码成功:', result);
|
||||
// 根据扫码类型给出不同的提示
|
||||
if (result.type === 'verification') {
|
||||
// 核销成功,可以显示更多信息或跳转到详情页
|
||||
const businessType = result?.data?.businessType;
|
||||
if (businessType === 'gift' && result?.data?.gift) {
|
||||
const gift = result.data.gift;
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: `已成功核销:${gift.goodsName || gift.name || '礼品'},面值¥${gift.faceValue}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (businessType === 'ticket' && result?.data?.ticket) {
|
||||
const ticket = result.data.ticket;
|
||||
const qty = result.data.qty || 1;
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: `已成功核销:${ticket.templateName || '水票'},本次使用${qty}次,剩余可用${ticket.availableQty ?? 0}次`
|
||||
});
|
||||
return;
|
||||
}
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}`
|
||||
content: '已成功核销'
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -252,7 +281,7 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
</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-xs text-gray-200'} style={themeStyles.textColor}>水票</Text>
|
||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.giftCards || 0}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -11,13 +11,14 @@ import {
|
||||
People,
|
||||
// AfterSaleService,
|
||||
Logout,
|
||||
ShoppingAdd,
|
||||
Shop,
|
||||
Jdl,
|
||||
Service
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
|
||||
const UserCell = () => {
|
||||
const {logoutUser} = useUser();
|
||||
const {logoutUser, hasRole} = useUser();
|
||||
|
||||
const onLogout = () => {
|
||||
Taro.showModal({
|
||||
@@ -49,10 +50,49 @@ const UserCell = () => {
|
||||
border: 'none'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Grid.Item text="企业采购" onClick={() => navTo('/user/poster/poster', true)}>
|
||||
|
||||
{hasRole('store') && (
|
||||
<Grid.Item text="门店中心" onClick={() => navTo('/store/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Shop color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
)}
|
||||
|
||||
{hasRole('rider') && (
|
||||
<Grid.Item text="配送中心" onClick={() => navTo('/rider/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Jdl color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
)}
|
||||
|
||||
{(hasRole('staff') || hasRole('admin')) && (
|
||||
<Grid.Item text="门店订单" onClick={() => navTo('/user/store/orders/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Shop color="#f59e0b" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
)}
|
||||
|
||||
<Grid.Item text="收货地址" onClick={() => navTo('/user/address/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<ShoppingAdd color="#3b82f6" size="20"/>
|
||||
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Location color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Ask className={'text-cyan-500'} size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
@@ -71,14 +111,6 @@ const UserCell = () => {
|
||||
</Button>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text="收货地址" onClick={() => navTo('/user/address/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Location color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'实名认证'} onClick={() => navTo('/user/userVerify/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
@@ -111,13 +143,6 @@ const UserCell = () => {
|
||||
{/* </View>*/}
|
||||
{/*</Grid.Item>*/}
|
||||
|
||||
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Ask className={'text-cyan-500'} size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>
|
||||
<View className="text-center">
|
||||
@@ -189,4 +214,3 @@ const UserCell = () => {
|
||||
)
|
||||
}
|
||||
export default UserCell
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import {PullToRefresh} from '@nutui/nutui-react-taro'
|
||||
import UserCard from "./components/UserCard";
|
||||
import UserOrder from "./components/UserOrder";
|
||||
import UserFooter from "./components/UserFooter";
|
||||
import {useUserData} from "@/hooks/useUserData";
|
||||
import {View} from '@tarojs/components';
|
||||
import './user.scss'
|
||||
import IsDealer from "./components/IsDealer";
|
||||
@@ -12,14 +11,11 @@ import UserGrid from "@/pages/user/components/UserGrid";
|
||||
|
||||
function User() {
|
||||
|
||||
const {refresh} = useUserData()
|
||||
const userCardRef = useRef<any>()
|
||||
const themeStyles = useThemeStyles();
|
||||
|
||||
// 下拉刷新处理
|
||||
const handleRefresh = async () => {
|
||||
await refresh()
|
||||
// 如果 UserCard 组件有自己的刷新方法,也可以调用
|
||||
if (userCardRef.current?.handleRefresh) {
|
||||
await userCardRef.current.handleRefresh()
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
|
||||
|
||||
const Register = () => {
|
||||
const [isAgree, setIsAgree] = useState(false)
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-col justify-center px-5 pt-3'}>
|
||||
<div className={'text-xl font-bold py-2'}>免费试用14天,快速上手独立站</div>
|
||||
<div className={'text-sm py-1 font-normal text-gray-500'}>建站、选品、营销、支付、物流,全部搞定</div>
|
||||
<div className={'text-sm pb-4 font-normal text-gray-500'}>
|
||||
WebSoft为您提供独立站的解决方案,提供专业、高效、安全的运营服务。
|
||||
</div>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input type="text" placeholder="手机号" maxLength={11} 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 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Register
|
||||
@@ -3,6 +3,7 @@ import Taro from '@tarojs/taro'
|
||||
import {Input, Button} from '@nutui/nutui-react-taro'
|
||||
import {loginBySms, sendSmsCaptcha} from "@/api/passport/login";
|
||||
import {LoginParam} from "@/api/passport/login/model";
|
||||
import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite";
|
||||
|
||||
const SmsLogin = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
@@ -131,6 +132,15 @@ const SmsLogin = () => {
|
||||
code: formData.code
|
||||
})
|
||||
|
||||
// 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定
|
||||
if (hasPendingInvite()) {
|
||||
try {
|
||||
await checkAndHandleInviteRelation()
|
||||
} catch (e) {
|
||||
console.error('短信登录后处理邀请关系失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
|
||||
@@ -43,7 +43,7 @@ const UnifiedQRPage: React.FC = () => {
|
||||
setTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '是否继续扫码核销其他礼品卡?',
|
||||
content: '是否继续扫码核销其他水票/礼品卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleStartScan();
|
||||
@@ -179,7 +179,7 @@ const UnifiedQRPage: React.FC = () => {
|
||||
</Text>
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
{scanType === ScanType.LOGIN ? '正在确认登录' :
|
||||
scanType === ScanType.VERIFICATION ? '正在核销礼品卡' : '正在处理'}
|
||||
scanType === ScanType.VERIFICATION ? '正在核销' : '正在处理'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
@@ -192,12 +192,29 @@ const UnifiedQRPage: React.FC = () => {
|
||||
</Text>
|
||||
{result.type === ScanType.VERIFICATION && result.data && (
|
||||
<View className="bg-green-50 rounded-lg p-3 mb-4">
|
||||
<Text className="text-sm text-green-800 block">
|
||||
礼品卡:{result.data.goodsName || '未知商品'}
|
||||
</Text>
|
||||
<Text className="text-sm text-green-800 block">
|
||||
面值:¥{result.data.faceValue}
|
||||
</Text>
|
||||
{result.data.businessType === 'gift' && result.data.gift && (
|
||||
<>
|
||||
<Text className="text-sm text-green-800 block">
|
||||
礼品:{result.data.gift.goodsName || result.data.gift.name || '未知'}
|
||||
</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 className="mt-2">
|
||||
@@ -278,9 +295,14 @@ const UnifiedQRPage: React.FC = () => {
|
||||
<Text className="text-sm text-gray-800">
|
||||
{record.success ? record.message : record.error}
|
||||
</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">
|
||||
{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>
|
||||
)}
|
||||
</View>
|
||||
@@ -304,7 +326,7 @@ const UnifiedQRPage: React.FC = () => {
|
||||
• 登录二维码:自动确认网页端登录
|
||||
</Text>
|
||||
<Text className="text-xs text-blue-700 block mb-1">
|
||||
• 核销二维码:门店核销用户礼品卡
|
||||
• 核销二维码:核销用户水票/礼品卡
|
||||
</Text>
|
||||
<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
295
src/rider/index.tsx
Normal file
295
src/rider/index.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
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
|
||||
} 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>
|
||||
|
||||
{/* 第二行功能 */}
|
||||
{/*<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'
|
||||
}
|
||||
390
src/rider/orders/index.tsx
Normal file
390
src/rider/orders/index.tsx
Normal file
@@ -0,0 +1,390 @@
|
||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Tabs, TabPane, Cell, Space, Button, Dialog, Image, Empty, InfiniteLoading} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import dayjs from 'dayjs'
|
||||
import {pageShopOrder, updateShopOrder} from '@/api/shop/shopOrder'
|
||||
import type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model'
|
||||
import {uploadFile} from '@/api/system/file'
|
||||
|
||||
export default function RiderOrders() {
|
||||
|
||||
const riderId = useMemo(() => {
|
||||
const v = Number(Taro.getStorageSync('UserId'))
|
||||
return Number.isFinite(v) && v > 0 ? v : undefined
|
||||
}, [])
|
||||
|
||||
const pageRef = useRef(1)
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
const [list, setList] = useState<ShopOrder[]>([])
|
||||
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<ShopOrder | null>(null)
|
||||
const [deliverImg, setDeliverImg] = useState<string | undefined>(undefined)
|
||||
|
||||
// 前端展示用:后台可配置实际自动确认收货时长
|
||||
const AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK = 24
|
||||
|
||||
const riderTabs = useMemo(
|
||||
() => [
|
||||
{index: 0, title: '全部', statusFilter: -1},
|
||||
{index: 1, title: '配送中', statusFilter: 3}, // 后端:deliveryStatus=20
|
||||
{index: 2, title: '待客户确认', statusFilter: 3}, // 同上,前端再按 sendEndTime 细分
|
||||
{index: 3, title: '已完成', statusFilter: 5}, // 后端:orderStatus=1
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const isAbnormalOrder = (order: ShopOrder) => {
|
||||
const s = order.orderStatus
|
||||
return s === 2 || s === 3 || s === 4 || s === 5 || s === 6 || s === 7
|
||||
}
|
||||
|
||||
const getOrderStatusText = (order: ShopOrder) => {
|
||||
if (order.orderStatus === 2) return '已取消'
|
||||
if (order.orderStatus === 3) return '取消中'
|
||||
if (order.orderStatus === 4) return '退款申请中'
|
||||
if (order.orderStatus === 5) return '退款被拒绝'
|
||||
if (order.orderStatus === 6) return '退款成功'
|
||||
if (order.orderStatus === 7) return '客户申请退款'
|
||||
if (!order.payStatus) return '未付款'
|
||||
if (order.orderStatus === 1) return '已完成'
|
||||
|
||||
// 配送员页:用 sendEndTime 表示“已送达收货点”
|
||||
if (order.deliveryStatus === 20) {
|
||||
if (order.sendEndTime) return '待客户确认收货'
|
||||
return '配送中'
|
||||
}
|
||||
if (order.deliveryStatus === 10) return '待发货'
|
||||
if (order.deliveryStatus === 30) return '部分发货'
|
||||
return '处理中'
|
||||
}
|
||||
|
||||
const getOrderStatusColor = (order: ShopOrder) => {
|
||||
if (isAbnormalOrder(order)) return 'text-orange-500'
|
||||
if (order.orderStatus === 1) return 'text-green-600'
|
||||
if (order.sendEndTime) return 'text-purple-600'
|
||||
return 'text-blue-600'
|
||||
}
|
||||
|
||||
const canConfirmDelivered = (order: ShopOrder) => {
|
||||
if (!order.payStatus) return false
|
||||
if (order.orderStatus === 1) return false
|
||||
if (isAbnormalOrder(order)) return false
|
||||
// 只允许在“配送中”阶段确认送达
|
||||
if (order.deliveryStatus !== 20) return false
|
||||
return !order.sendEndTime
|
||||
}
|
||||
|
||||
const filterByTab = useCallback(
|
||||
(orders: ShopOrder[]) => {
|
||||
if (tabIndex === 1) {
|
||||
// 配送中:未确认送达
|
||||
return orders.filter(o => o.deliveryStatus === 20 && !o.sendEndTime && !isAbnormalOrder(o) && o.orderStatus !== 1)
|
||||
}
|
||||
if (tabIndex === 2) {
|
||||
// 待客户确认:已确认送达
|
||||
return orders.filter(o => o.deliveryStatus === 20 && !!o.sendEndTime && !isAbnormalOrder(o) && o.orderStatus !== 1)
|
||||
}
|
||||
if (tabIndex === 3) {
|
||||
return orders.filter(o => o.orderStatus === 1)
|
||||
}
|
||||
return orders
|
||||
},
|
||||
[tabIndex]
|
||||
)
|
||||
|
||||
const reload = useCallback(
|
||||
async (resetPage = false) => {
|
||||
if (!riderId) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const currentPage = resetPage ? 1 : pageRef.current
|
||||
const currentTab = riderTabs.find(t => t.index === tabIndex) || riderTabs[0]
|
||||
|
||||
const params: ShopOrderParam = {
|
||||
page: currentPage,
|
||||
riderId,
|
||||
statusFilter: currentTab.statusFilter,
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await pageShopOrder(params)
|
||||
const incoming = (res?.list || []) as ShopOrder[]
|
||||
setList(prev => (resetPage ? incoming : prev.concat(incoming)))
|
||||
setHasMore(incoming.length >= 10)
|
||||
pageRef.current = currentPage
|
||||
} catch (e) {
|
||||
console.error('加载配送订单失败:', e)
|
||||
setError('加载失败,请重试')
|
||||
setHasMore(false)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[riderId, riderTabs, tabIndex]
|
||||
)
|
||||
|
||||
const reloadMore = useCallback(async () => {
|
||||
if (loading || !hasMore) return
|
||||
pageRef.current += 1
|
||||
await reload(false)
|
||||
}, [hasMore, loading, reload])
|
||||
|
||||
const openDeliverDialog = (order: ShopOrder) => {
|
||||
setDeliverOrder(order)
|
||||
setDeliverImg(order.sendEndImg)
|
||||
setDeliverDialogVisible(true)
|
||||
}
|
||||
|
||||
const handleChooseDeliverImg = async () => {
|
||||
try {
|
||||
const file = await uploadFile()
|
||||
setDeliverImg(file?.url)
|
||||
} catch (e) {
|
||||
console.error('上传送达照片失败:', e)
|
||||
Taro.showToast({title: '上传失败,请重试', icon: 'none'})
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmDelivered = async () => {
|
||||
if (!deliverOrder?.orderId) return
|
||||
if (deliverSubmitting) return
|
||||
setDeliverSubmitting(true)
|
||||
try {
|
||||
await updateShopOrder({
|
||||
orderId: deliverOrder.orderId,
|
||||
// 用于前端/后端识别“配送员已送达收货点”
|
||||
sendEndTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
sendEndImg: deliverImg,
|
||||
})
|
||||
Taro.showToast({title: '已确认送达', icon: 'success'})
|
||||
setDeliverDialogVisible(false)
|
||||
setDeliverOrder(null)
|
||||
setDeliverImg(undefined)
|
||||
pageRef.current = 1
|
||||
await reload(true)
|
||||
} catch (e) {
|
||||
console.error('确认送达失败:', e)
|
||||
Taro.showToast({title: '确认送达失败', icon: 'none'})
|
||||
} finally {
|
||||
setDeliverSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
pageRef.current = 1
|
||||
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>
|
||||
|
||||
<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={<>加载中</>}
|
||||
loadMoreText={
|
||||
displayList.length === 0 ? (
|
||||
<Empty style={{backgroundColor: 'transparent'}} description="暂无配送订单"/>
|
||||
) : (
|
||||
<View className="h-24">没有更多了</View>
|
||||
)
|
||||
}
|
||||
>
|
||||
{displayList.map((o, idx) => {
|
||||
const phoneToCall = o.phone || o.mobile
|
||||
const flow1Done = !!o.riderId
|
||||
const flow2Done = o.deliveryStatus === 20 || o.deliveryStatus === 30
|
||||
const flow3Done = !!o.sendEndTime
|
||||
const flow4Done = o.orderStatus === 1
|
||||
// 直接使用订单分页接口返回的 orderGoods
|
||||
const goodsList = o.orderGoods || []
|
||||
const goodsNameList = goodsList
|
||||
.map(g => g?.goodsName || (g as any)?.goodsTitle || (g as any)?.title || (g as any)?.name)
|
||||
.filter(Boolean) as string[]
|
||||
const goodsSummary = goodsNameList.length
|
||||
? `${goodsNameList.slice(0, 3).join('、')}${goodsList.length > 3 ? ` 等${goodsList.length}件` : ''}`
|
||||
: (o.title || '-')
|
||||
|
||||
const autoConfirmAt = o.sendEndTime
|
||||
? dayjs(o.sendEndTime).add(AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK, 'hour')
|
||||
: null
|
||||
const autoConfirmLeftMin = autoConfirmAt ? autoConfirmAt.diff(dayjs(), 'minute') : null
|
||||
|
||||
return (
|
||||
<Cell
|
||||
key={`${o.orderId || idx}`}
|
||||
style={{padding: '16px'}}
|
||||
onClick={() => o.orderId && Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${o.orderId}`})}
|
||||
>
|
||||
<View className="w-full">
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-gray-800 font-bold text-sm">{o.orderNo || `订单#${o.orderId}`}</Text>
|
||||
<Text className={`${getOrderStatusColor(o)} text-sm font-medium`}>{getOrderStatusText(o)}</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-gray-400 text-xs mt-1">
|
||||
下单时间:{o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'}
|
||||
</View>
|
||||
|
||||
<View className="mt-3 bg-white rounded-lg">
|
||||
<View className="text-sm text-gray-700">
|
||||
<Text className="text-gray-500">收货点:</Text>
|
||||
<Text>{o.selfTakeMerchantName || o.address || '-'}</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">客户:</Text>
|
||||
<Text>{o.realName || '-'} {o.phone ? `(${o.phone})` : ''}</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">金额:</Text>
|
||||
<Text>¥{o.payPrice || o.totalPrice || '-'}</Text>
|
||||
<Text className="text-gray-500 ml-3">数量:</Text>
|
||||
<Text>{o.totalNum ?? '-'}</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">商品:</Text>
|
||||
<Text>{goodsSummary}</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>
|
||||
)}
|
||||
</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 ? (flow3Done ? 'text-green-600 font-medium' : 'text-blue-600 font-medium') : 'text-gray-400'}>2 配送中</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow3Done ? (flow4Done ? 'text-green-600 font-medium' : '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>
|
||||
|
||||
{o.sendEndTime && o.orderStatus !== 1 && autoConfirmAt && (
|
||||
<View className="mt-1 text-gray-500">
|
||||
若客户未确认,预计 {autoConfirmAt.format('YYYY-MM-DD HH:mm')} 自动确认收货(以后台配置为准)
|
||||
{typeof autoConfirmLeftMin === 'number' && autoConfirmLeftMin > 0 ? `,约剩余 ${Math.ceil(autoConfirmLeftMin / 60)} 小时` : ''}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="mt-3 flex justify-end">
|
||||
<Space>
|
||||
{!!phoneToCall && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
Taro.makePhoneCall({phoneNumber: phoneToCall})
|
||||
}}
|
||||
>
|
||||
联系客户
|
||||
</Button>
|
||||
)}
|
||||
{canConfirmDelivered(o) && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
openDeliverDialog(o)
|
||||
}}
|
||||
>
|
||||
确认送达
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
</Cell>
|
||||
)
|
||||
})}
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Dialog
|
||||
title="确认送达"
|
||||
visible={deliverDialogVisible}
|
||||
confirmText={deliverSubmitting ? '提交中...' : '确认送达'}
|
||||
cancelText="取消"
|
||||
onConfirm={handleConfirmDelivered}
|
||||
onCancel={() => {
|
||||
if (deliverSubmitting) return
|
||||
setDeliverDialogVisible(false)
|
||||
setDeliverOrder(null)
|
||||
setDeliverImg(undefined)
|
||||
}}
|
||||
>
|
||||
<View className="text-sm text-gray-700">
|
||||
<View>到达收货点后,可选拍照留存,再点确认送达。</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>
|
||||
</Dialog>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -42,7 +42,7 @@ function Category() {
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: `${nav?.categoryName}_时里院子市集`,
|
||||
title: `${nav?.categoryName}_桂乐淘`,
|
||||
path: `/shop/category/index?id=${categoryId}`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '注册账号',
|
||||
navigationBarTitleText: '立即订水',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
0
src/shop/gift/index.tsx
Normal file
0
src/shop/gift/index.tsx
Normal file
@@ -15,6 +15,7 @@ import SpecSelector from "@/components/SpecSelector";
|
||||
import "./index.scss";
|
||||
import {useCart} from "@/hooks/useCart";
|
||||
import {useConfig} from "@/hooks/useConfig";
|
||||
import {parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite";
|
||||
|
||||
const GoodsDetail = () => {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(44);
|
||||
@@ -39,6 +40,24 @@ const GoodsDetail = () => {
|
||||
const {cartCount, addToCart} = useCart()
|
||||
const {config} = useConfig()
|
||||
|
||||
// 如果从分享链接进入(携带 inviter/source/t),且当前未登录,则暂存邀请信息用于注册后绑定关系
|
||||
useEffect(() => {
|
||||
try {
|
||||
const currentUserId = Taro.getStorageSync('UserId')
|
||||
if (currentUserId) return
|
||||
|
||||
const inviteParams = parseInviteParams({query: router?.params})
|
||||
if (inviteParams?.inviter) {
|
||||
saveInviteParams(inviteParams)
|
||||
trackInviteSource(inviteParams.source || 'share', parseInt(inviteParams.inviter))
|
||||
}
|
||||
} catch (e) {
|
||||
// 邀请参数解析/存储失败不影响正常浏览商品
|
||||
console.error('商品详情页处理邀请参数失败:', e)
|
||||
}
|
||||
// router 在 Taro 中可能不稳定;这里仅在 goodsId 变化时尝试处理一次即可
|
||||
}, [goodsId])
|
||||
|
||||
// 处理加入购物车
|
||||
const handleAddToCart = () => {
|
||||
if (!goods) return;
|
||||
@@ -186,9 +205,15 @@ const GoodsDetail = () => {
|
||||
|
||||
// 分享给好友
|
||||
useShareAppMessage(() => {
|
||||
const inviter = Taro.getStorageSync('UserId')
|
||||
const sharePath =
|
||||
inviter
|
||||
? `/shop/goodsDetail/index?id=${goodsId}&inviter=${inviter}&source=goods_share&t=${Date.now()}`
|
||||
: `/shop/goodsDetail/index?id=${goodsId}`
|
||||
|
||||
return {
|
||||
title: goods?.name || '精选商品',
|
||||
path: `/shop/goodsDetail/index?id=${goodsId}`,
|
||||
path: sharePath,
|
||||
imageUrl: goods?.image ? `${goods.image}?x-oss-process=image/resize,w_500,h_400,m_fill` : undefined, // 分享图片,调整为5:4比例
|
||||
success: function (res: any) {
|
||||
console.log('分享成功', res);
|
||||
|
||||
@@ -437,7 +437,7 @@ const OrderConfirm = () => {
|
||||
quantity,
|
||||
address.id,
|
||||
{
|
||||
comments: '时里院子市集',
|
||||
comments: '桂乐淘',
|
||||
deliveryType: 0,
|
||||
buyerRemarks: orderRemark,
|
||||
// 🔧 确保 couponId 是正确的数字类型,且不传递 undefined
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Cell, CellGroup, Image, Space, Button} from '@nutui/nutui-react-taro'
|
||||
import {Cell, CellGroup, Image, Space, Button, Dialog} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopOrder} from "@/api/shop/shopOrder/model";
|
||||
@@ -13,6 +13,7 @@ import './index.scss'
|
||||
const OrderDetail = () => {
|
||||
const [order, setOrder] = useState<ShopOrder | null>(null);
|
||||
const [orderGoodsList, setOrderGoodsList] = useState<ShopOrderGoods[]>([]);
|
||||
const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
|
||||
const router = Taro.getCurrentInstance().router;
|
||||
const orderId = router?.params?.orderId;
|
||||
|
||||
@@ -67,6 +68,25 @@ const OrderDetail = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 确认收货(客户)
|
||||
const handleConfirmReceive = async () => {
|
||||
if (!order?.orderId) return
|
||||
try {
|
||||
setConfirmReceiveDialogVisible(false)
|
||||
await updateShopOrder({
|
||||
orderId: order.orderId,
|
||||
deliveryStatus: order.deliveryStatus, // 10未发货 20已发货 30部分发货
|
||||
orderStatus: 1 // 已完成
|
||||
})
|
||||
Taro.showToast({title: '确认收货成功', icon: 'success'})
|
||||
setOrder(prev => (prev ? {...prev, orderStatus: 1} : prev))
|
||||
} catch (e) {
|
||||
console.error('确认收货失败:', e)
|
||||
Taro.showToast({title: '确认收货失败', icon: 'none'})
|
||||
setConfirmReceiveDialogVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
const getOrderStatusText = (order: ShopOrder) => {
|
||||
// 优先检查订单状态
|
||||
if (order.orderStatus === 2) return '已取消';
|
||||
@@ -81,8 +101,15 @@ const OrderDetail = () => {
|
||||
|
||||
// 已付款后检查发货状态
|
||||
if (order.deliveryStatus === 10) return '待发货';
|
||||
if (order.deliveryStatus === 20) return '待收货';
|
||||
if (order.deliveryStatus === 30) return '已收货';
|
||||
if (order.deliveryStatus === 20) {
|
||||
// 若订单有配送员,则以配送员送达时间作为“可确认收货”的依据
|
||||
if (order.riderId) {
|
||||
if (order.sendEndTime && order.orderStatus !== 1) return '待确认收货';
|
||||
return '配送中';
|
||||
}
|
||||
return '待收货';
|
||||
}
|
||||
if (order.deliveryStatus === 30) return '部分发货';
|
||||
|
||||
// 最后检查订单完成状态
|
||||
if (order.orderStatus === 1) return '已完成';
|
||||
@@ -133,6 +160,15 @@ const OrderDetail = () => {
|
||||
return <div>加载中...</div>;
|
||||
}
|
||||
|
||||
const currentUserId = Number(Taro.getStorageSync('UserId'))
|
||||
const isOwner = !!currentUserId && currentUserId === order.userId
|
||||
const canConfirmReceive =
|
||||
isOwner &&
|
||||
order.payStatus &&
|
||||
order.orderStatus !== 1 &&
|
||||
order.deliveryStatus === 20 &&
|
||||
(!order.riderId || !!order.sendEndTime)
|
||||
|
||||
return (
|
||||
<div className={'order-detail-page'}>
|
||||
{/* 支付倒计时显示 - 详情页实时更新 */}
|
||||
@@ -190,11 +226,25 @@ const OrderDetail = () => {
|
||||
{!order.payStatus && <Button onClick={() => console.log('取消订单')}>取消订单</Button>}
|
||||
{!order.payStatus && <Button type="primary" onClick={() => console.log('立即支付')}>立即支付</Button>}
|
||||
{order.orderStatus === 1 && <Button onClick={handleApplyRefund}>申请退款</Button>}
|
||||
{order.deliveryStatus === 20 &&
|
||||
<Button type="primary" onClick={() => console.log('确认收货')}>确认收货</Button>}
|
||||
{canConfirmReceive && (
|
||||
<Button type="primary" onClick={() => setConfirmReceiveDialogVisible(true)}>
|
||||
确认收货
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Dialog
|
||||
title="确认收货"
|
||||
visible={confirmReceiveDialogVisible}
|
||||
confirmText="确认收货"
|
||||
cancelText="我再想想"
|
||||
onConfirm={handleConfirmReceive}
|
||||
onCancel={() => setConfirmReceiveDialogVisible(false)}
|
||||
>
|
||||
确定已经收到商品了吗?确认后订单将完成。
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
3
src/store/index.config.ts
Normal file
3
src/store/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '门店中心'
|
||||
})
|
||||
0
src/store/index.scss
Normal file
0
src/store/index.scss
Normal file
281
src/store/index.tsx
Normal file
281
src/store/index.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {Avatar, Button, ConfigProvider, Grid} from '@nutui/nutui-react-taro'
|
||||
import {Location, Scan, Shop, Shopping, User} from '@nutui/icons-react-taro'
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {useThemeStyles} from '@/hooks/useTheme'
|
||||
import {useUser} from '@/hooks/useUser'
|
||||
import {getSelectedStoreFromStorage} from '@/utils/storeSelection'
|
||||
import {listShopStoreUser} from '@/api/shop/shopStoreUser'
|
||||
import {getShopStore} from '@/api/shop/shopStore'
|
||||
import type {ShopStore as ShopStoreModel} from '@/api/shop/shopStore/model'
|
||||
|
||||
const StoreIndex: React.FC = () => {
|
||||
const themeStyles = useThemeStyles()
|
||||
const {isLoggedIn, loading: userLoading, getAvatarUrl, getDisplayName, getRoleName, hasRole} = useUser()
|
||||
|
||||
const [boundStoreId, setBoundStoreId] = useState<number | undefined>(undefined)
|
||||
const [selectedStore, setSelectedStore] = useState<ShopStoreModel | null>(getSelectedStoreFromStorage())
|
||||
const [store, setStore] = useState<ShopStoreModel | null>(selectedStore)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const storeId = boundStoreId || selectedStore?.id
|
||||
|
||||
const parseStoreCoords = (s: ShopStoreModel): {lng: number; lat: number} | null => {
|
||||
const raw = (s.lngAndLat || s.location || '').trim()
|
||||
if (!raw) return null
|
||||
|
||||
const parts = raw.split(/[,\s]+/).filter(Boolean)
|
||||
if (parts.length < 2) return null
|
||||
|
||||
const a = parseFloat(parts[0])
|
||||
const b = parseFloat(parts[1])
|
||||
if (Number.isNaN(a) || Number.isNaN(b)) return null
|
||||
|
||||
// 常见格式是 "lng,lat";这里做一个简单兜底
|
||||
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90
|
||||
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180
|
||||
if (looksLikeLngLat) return {lng: a, lat: b}
|
||||
if (looksLikeLatLng) return {lng: b, lat: a}
|
||||
return null
|
||||
}
|
||||
|
||||
const navigateToPage = (url: string) => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.showToast({title: '请先登录', icon: 'none', duration: 1500})
|
||||
return
|
||||
}
|
||||
Taro.navigateTo({url})
|
||||
}
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
try {
|
||||
const latestSelectedStore = getSelectedStoreFromStorage()
|
||||
setSelectedStore(latestSelectedStore)
|
||||
|
||||
const userIdRaw = Number(Taro.getStorageSync('UserId'))
|
||||
const userId = Number.isFinite(userIdRaw) && userIdRaw > 0 ? userIdRaw : undefined
|
||||
|
||||
let foundStoreId: number | undefined = undefined
|
||||
if (userId) {
|
||||
// 优先按“店员绑定关系”确定门店归属
|
||||
try {
|
||||
const list = await listShopStoreUser({userId})
|
||||
const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId)
|
||||
foundStoreId = first?.storeId
|
||||
setBoundStoreId(foundStoreId)
|
||||
} catch {
|
||||
// fallback to SelectedStore
|
||||
foundStoreId = undefined
|
||||
setBoundStoreId(undefined)
|
||||
}
|
||||
} else {
|
||||
foundStoreId = undefined
|
||||
setBoundStoreId(undefined)
|
||||
}
|
||||
|
||||
const nextStoreId = (foundStoreId || latestSelectedStore?.id)
|
||||
if (!nextStoreId) {
|
||||
setStore(latestSelectedStore)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取门店详情(用于展示门店名称/地址/仓库等)
|
||||
const full = await getShopStore(nextStoreId)
|
||||
setStore(full || (latestSelectedStore?.id === nextStoreId ? latestSelectedStore : ({id: nextStoreId} as ShopStoreModel)))
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || '获取门店信息失败'
|
||||
setError(msg)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 返回/切换到该页面时,同步最新的已选门店与绑定门店
|
||||
useDidShow(() => {
|
||||
refresh().catch(() => {})
|
||||
})
|
||||
|
||||
const openStoreLocation = () => {
|
||||
if (!store?.id) {
|
||||
return Taro.showToast({title: '请先选择门店', icon: 'none'})
|
||||
}
|
||||
const coords = parseStoreCoords(store)
|
||||
if (!coords) {
|
||||
return Taro.showToast({title: '门店未配置定位', icon: 'none'})
|
||||
}
|
||||
Taro.openLocation({
|
||||
latitude: coords.lat,
|
||||
longitude: coords.lng,
|
||||
name: store.name || '门店',
|
||||
address: store.address || ''
|
||||
})
|
||||
}
|
||||
|
||||
if (!isLoggedIn && !userLoading) {
|
||||
return (
|
||||
<View className="bg-gray-100 min-h-screen p-4">
|
||||
<View className="bg-white rounded-xl p-4">
|
||||
<Text className="text-gray-700">请先登录后再进入门店中心</Text>
|
||||
<View className="mt-3">
|
||||
<Button type="primary" onClick={() => Taro.navigateTo({url: '/passport/login'})}>
|
||||
去登录
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-100 min-h-screen">
|
||||
{/* 头部信息 */}
|
||||
<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">
|
||||
<Avatar
|
||||
size="50"
|
||||
src={getAvatarUrl()}
|
||||
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">
|
||||
{getDisplayName()}
|
||||
</View>
|
||||
<View className="text-sm" style={{color: 'rgba(255, 255, 255, 0.8)'}}>
|
||||
{hasRole('store') ? '门店' : hasRole('rider') ? '配送员' : getRoleName()}
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
size="small"
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.18)',
|
||||
color: '#fff',
|
||||
border: '1px solid rgba(255, 255, 255, 0.25)'
|
||||
}}
|
||||
loading={loading}
|
||||
onClick={refresh}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 门店信息 */}
|
||||
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10 bg-white">
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="font-semibold text-gray-400">当前门店</Text>
|
||||
<View
|
||||
className="text-gray-400 text-sm"
|
||||
onClick={() => Taro.switchTab({url: '/pages/index/index'})}
|
||||
>
|
||||
切换门店
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{!storeId ? (
|
||||
<View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
未选择门店,请先去首页选择门店。
|
||||
</Text>
|
||||
<View className="mt-3">
|
||||
<Button type="primary" size="small" onClick={() => Taro.switchTab({url: '/pages/index/index'})}>
|
||||
去首页选择门店
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
<View className="text-base font-medium text-gray-900">
|
||||
{store?.name || `门店ID: ${storeId}`}
|
||||
</View>
|
||||
{!!store?.address && (
|
||||
<View className="text-sm text-gray-600 mt-1">
|
||||
{store.address}
|
||||
</View>
|
||||
)}
|
||||
{!!store?.warehouseName && (
|
||||
<View className="text-sm text-gray-500 mt-1">
|
||||
默认仓库:{store.warehouseName}
|
||||
</View>
|
||||
)}
|
||||
{!!error && (
|
||||
<View className="mt-2">
|
||||
<Text className="text-sm text-red-600">{error}</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('/store/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('/user/store/verification')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Scan color="#10b981" size="20" />
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text="门店导航" onClick={openStoreLocation}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Location color="#f59e0b" size="20" />
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text="首页选店" onClick={() => Taro.switchTab({url: '/pages/index/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">
|
||||
<Shop color="#8b5cf6" size="20" />
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
</Grid>
|
||||
</ConfigProvider>
|
||||
</View>
|
||||
|
||||
<View className="h-20"></View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default StoreIndex
|
||||
4
src/store/orders/index.config.ts
Normal file
4
src/store/orders/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '门店订单',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
83
src/store/orders/index.tsx
Normal file
83
src/store/orders/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Button} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import OrderList from '@/user/order/components/OrderList'
|
||||
import {getSelectedStoreFromStorage} from '@/utils/storeSelection'
|
||||
import {listShopStoreUser} from '@/api/shop/shopStoreUser'
|
||||
|
||||
export default function StoreOrders() {
|
||||
const [boundStoreId, setBoundStoreId] = useState<number | undefined>(undefined)
|
||||
|
||||
const isLoggedIn = useMemo(() => {
|
||||
return !!Taro.getStorageSync('access_token') && !!Taro.getStorageSync('UserId')
|
||||
}, [])
|
||||
|
||||
const selectedStore = useMemo(() => getSelectedStoreFromStorage(), [])
|
||||
const storeId = boundStoreId || selectedStore?.id
|
||||
|
||||
useEffect(() => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// 优先按“店员绑定关系”确定门店归属:门店看到的是自己的订单
|
||||
const userId = Number(Taro.getStorageSync('UserId'))
|
||||
if (!Number.isFinite(userId) || userId <= 0) return
|
||||
listShopStoreUser({userId}).then(list => {
|
||||
const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId)
|
||||
if (first?.storeId) setBoundStoreId(first.storeId)
|
||||
}).catch(() => {
|
||||
// fallback to SelectedStore
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen p-4">
|
||||
<View className="bg-white rounded-lg p-4">
|
||||
<Text className="text-sm text-gray-700">请先登录</Text>
|
||||
<View className="mt-3">
|
||||
<Button type="primary" size="small" onClick={() => Taro.navigateTo({url: '/passport/login'})}>
|
||||
去登录
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
|
||||
<View className="px-3">
|
||||
<View className="bg-white rounded-lg p-3 mb-3">
|
||||
<Text className="text-sm text-gray-400">当前门店:</Text>
|
||||
<Text className="text-base font-medium">
|
||||
{boundStoreId
|
||||
? (selectedStore?.id === boundStoreId ? (selectedStore?.name || `门店ID: ${boundStoreId}`) : `门店ID: ${boundStoreId}`)
|
||||
: (selectedStore?.name || '未选择门店')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{!storeId ? (
|
||||
<View className="bg-white rounded-lg p-4">
|
||||
<Text className="text-sm text-gray-600">
|
||||
请先在首页左上角选择门店,再查看门店订单。
|
||||
</Text>
|
||||
<View className="mt-3">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => Taro.switchTab({url: '/pages/index/index'})}
|
||||
>
|
||||
去首页选择门店
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<OrderList mode="store" baseParams={{storeId}} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/** 礼品卡类型枚举 */
|
||||
export enum GiftCardType {
|
||||
/** 实物礼品卡 */
|
||||
/** 礼品劵 */
|
||||
PHYSICAL = 10,
|
||||
/** 虚拟礼品卡 */
|
||||
VIRTUAL = 20,
|
||||
|
||||
@@ -43,10 +43,10 @@ const GiftCardDetail = () => {
|
||||
// 获取礼品卡类型文本
|
||||
const getGiftTypeText = (type?: number) => {
|
||||
switch (type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 10: return '礼品劵'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
default: return '水票'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的礼品卡',
|
||||
navigationBarTitleText: '我的水票',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Button, Empty, ConfigProvider,SearchBar, InfiniteLoading, Loading, PullToRefresh, Tabs, TabPane} from '@nutui/nutui-react-taro'
|
||||
import {Gift, Retweet, Board, QrCode} from '@nutui/icons-react-taro'
|
||||
import {Gift, Retweet, QrCode} from '@nutui/icons-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopGift} from "@/api/shop/shopGift/model";
|
||||
import {getUserGifts} from "@/api/shop/shopGift";
|
||||
@@ -24,7 +24,7 @@ const GiftCardManage = () => {
|
||||
// sortOrder: 'desc' as 'asc' | 'desc'
|
||||
// })
|
||||
|
||||
// 获取礼品卡状态过滤条件
|
||||
// 获取水票状态过滤条件
|
||||
const getStatusFilter = () => {
|
||||
switch (String(activeTab)) {
|
||||
case '0': // 未使用
|
||||
@@ -52,7 +52,7 @@ const GiftCardManage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 根据状态过滤条件加载礼品卡
|
||||
// 根据状态过滤条件加载水票
|
||||
const loadGiftsByStatus = async (statusFilter: any) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -72,9 +72,9 @@ const GiftCardManage = () => {
|
||||
setHasMore(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取礼品卡失败:', error)
|
||||
console.error('获取水票失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取礼品卡失败',
|
||||
title: '获取水票失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
@@ -125,9 +125,9 @@ const GiftCardManage = () => {
|
||||
// setTotal(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取礼品卡失败:', error)
|
||||
console.error('获取水票失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取礼品卡失败',
|
||||
title: '获取水票失败',
|
||||
icon: 'error'
|
||||
});
|
||||
} finally {
|
||||
@@ -162,11 +162,11 @@ const GiftCardManage = () => {
|
||||
loadGiftsByStatus(statusFilter)
|
||||
}
|
||||
|
||||
// 转换礼品卡数据为GiftCard组件所需格式
|
||||
// 转换水票数据为GiftCard组件所需格式
|
||||
const transformGiftData = (gift: ShopGift): GiftCardProps => {
|
||||
return {
|
||||
id: gift.id || 0,
|
||||
name: gift.name || '礼品卡', // 礼品卡名称
|
||||
name: gift.name || '水票', // 水票名称
|
||||
goodsName: gift.goodsName, // 商品名称(新增字段)
|
||||
description: gift.description || gift.instructions, // 使用说明作为描述
|
||||
code: gift.code,
|
||||
@@ -180,23 +180,23 @@ const GiftCardManage = () => {
|
||||
contactInfo: gift.contactInfo,
|
||||
// 添加商品信息
|
||||
goodsInfo: {
|
||||
// 如果有商品名称或商品ID,说明是关联商品的礼品卡
|
||||
// 如果有商品名称或商品ID,说明是关联商品的水票
|
||||
...((gift.goodsName || gift.goodsId) && {
|
||||
specification: `礼品卡面值:¥${gift.faceValue}`,
|
||||
specification: `水票面值:¥${gift.faceValue}`,
|
||||
category: getTypeText(gift.type),
|
||||
tags: [
|
||||
getTypeText(gift.type),
|
||||
gift.status === 0 ? '未使用' : gift.status === 1 ? '已使用' : '失效',
|
||||
...(gift.goodsName ? ['商品礼品卡'] : [])
|
||||
...(gift.goodsName ? ['商品水票'] : [])
|
||||
].filter(Boolean),
|
||||
instructions: gift.instructions ? [gift.instructions] : [
|
||||
'请在有效期内使用',
|
||||
'出示兑换码即可使用',
|
||||
'不可兑换现金',
|
||||
...(gift.goodsName ? ['此礼品卡关联具体商品'] : [])
|
||||
...(gift.goodsName ? ['此水票关联具体商品'] : [])
|
||||
],
|
||||
notices: [
|
||||
'礼品卡一经使用不可退换',
|
||||
'水票一经使用不可退换',
|
||||
'请妥善保管兑换码',
|
||||
'如有疑问请联系客服',
|
||||
...(gift.goodsName ? ['商品以实际为准'] : [])
|
||||
@@ -213,34 +213,34 @@ const GiftCardManage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取礼品卡类型文本
|
||||
// 获取水票类型文本
|
||||
const getTypeText = (type?: number): string => {
|
||||
switch (type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
case 10: return '实物水票'
|
||||
case 20: return '虚拟水票'
|
||||
case 30: return '服务水票'
|
||||
default: return '水票'
|
||||
}
|
||||
}
|
||||
|
||||
// 根据礼品卡类型获取主题色
|
||||
// 根据水票类型获取主题色
|
||||
const getThemeByType = (type?: number): 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple' => {
|
||||
switch (type) {
|
||||
case 10: return 'gold' // 实物礼品卡 - 金色
|
||||
case 20: return 'blue' // 虚拟礼品卡 - 蓝色
|
||||
case 30: return 'green' // 服务礼品卡 - 绿色
|
||||
case 10: return 'gold' // 实物水票 - 金色
|
||||
case 20: return 'blue' // 虚拟水票 - 蓝色
|
||||
case 30: return 'green' // 服务水票 - 绿色
|
||||
default: return 'purple' // 默认使用紫色主题,更美观
|
||||
}
|
||||
}
|
||||
|
||||
// 使用礼品卡
|
||||
// 使用水票
|
||||
const handleUseGift = (gift: ShopGift) => {
|
||||
Taro.showModal({
|
||||
title: '使用礼品卡',
|
||||
title: '使用水票',
|
||||
content: `确定要使用"${gift.name}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 跳转到礼品卡使用页面
|
||||
// 跳转到水票使用页面
|
||||
Taro.navigateTo({
|
||||
url: `/user/gift/use?id=${gift.id}`
|
||||
})
|
||||
@@ -249,35 +249,35 @@ const GiftCardManage = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 礼品卡点击事件
|
||||
// 水票点击事件
|
||||
const handleGiftClick = (gift: GiftCardProps, index: number) => {
|
||||
console.log(gift.code)
|
||||
const originalGift = list[index]
|
||||
if (originalGift) {
|
||||
// 显示礼品卡详情
|
||||
// 显示水票详情
|
||||
handleGiftDetail(originalGift)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示礼品卡详情
|
||||
// 显示水票详情
|
||||
const handleGiftDetail = (gift: ShopGift) => {
|
||||
// 跳转到礼品卡详情页
|
||||
// 跳转到水票详情页
|
||||
Taro.navigateTo({
|
||||
url: `/user/gift/detail?id=${gift.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 加载礼品卡统计数据
|
||||
// 加载水票统计数据
|
||||
// const loadGiftStats = async () => {
|
||||
// try {
|
||||
// // 并行获取各状态的礼品卡数量
|
||||
// // 并行获取各状态的水票数量
|
||||
// const [availableRes, usedRes, expiredRes] = await Promise.all([
|
||||
// getUserGifts({ page: 1, limit: 1, useStatus: 0 }),
|
||||
// getUserGifts({ page: 1, limit: 1, useStatus: 1 }),
|
||||
// getUserGifts({ page: 1, limit: 1, useStatus: 2 })
|
||||
// ])
|
||||
//
|
||||
// // 计算总价值(仅可用礼品卡)
|
||||
// // 计算总价值(仅可用水票)
|
||||
// const availableGifts = await getUserGifts({ page: 1, limit: 100, useStatus: 0 })
|
||||
// const totalValue = availableGifts?.list?.reduce((sum, gift) => {
|
||||
// return sum + parseFloat(gift.faceValue || '0')
|
||||
@@ -290,7 +290,7 @@ const GiftCardManage = () => {
|
||||
// totalValue
|
||||
// })
|
||||
// } catch (error) {
|
||||
// console.error('获取礼品卡统计失败:', error)
|
||||
// console.error('获取水票统计失败:', error)
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -300,21 +300,21 @@ const GiftCardManage = () => {
|
||||
// available: '0',
|
||||
// used: '1',
|
||||
// expired: '2',
|
||||
// total: '0' // 总价值点击跳转到可用礼品卡
|
||||
// total: '0' // 总价值点击跳转到可用水票
|
||||
// }
|
||||
// if (tabMap[type]) {
|
||||
// handleTabChange(tabMap[type])
|
||||
// }
|
||||
// }
|
||||
|
||||
// 兑换礼品卡
|
||||
// 兑换水票
|
||||
const handleRedeemGift = () => {
|
||||
Taro.navigateTo({
|
||||
url: '/user/gift/redeem'
|
||||
})
|
||||
}
|
||||
|
||||
// 扫码兑换礼品卡
|
||||
// 扫码兑换水票
|
||||
const handleScanRedeem = () => {
|
||||
Taro.scanCode({
|
||||
success: (res) => {
|
||||
@@ -377,14 +377,14 @@ const GiftCardManage = () => {
|
||||
>
|
||||
扫码
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Board />}
|
||||
onClick={() => setShowGuide(true)}
|
||||
>
|
||||
帮助
|
||||
</Button>
|
||||
{/*<Button*/}
|
||||
{/* size="small"*/}
|
||||
{/* fill="outline"*/}
|
||||
{/* icon={<Board />}*/}
|
||||
{/* onClick={() => setShowGuide(true)}*/}
|
||||
{/*>*/}
|
||||
{/* 帮助*/}
|
||||
{/*</Button>*/}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -400,7 +400,7 @@ const GiftCardManage = () => {
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 礼品卡列表 */}
|
||||
{/* 水票列表 */}
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
headHeight={60}
|
||||
@@ -410,9 +410,9 @@ const GiftCardManage = () => {
|
||||
<View className="flex flex-col justify-center items-center" style={{height: '500px'}}>
|
||||
<Empty
|
||||
description={
|
||||
activeTab === '0' ? "暂无未使用礼品卡" :
|
||||
activeTab === '1' ? "暂无已使用礼品卡" :
|
||||
"暂无失效礼品卡"
|
||||
activeTab === '0' ? "暂无未使用水票" :
|
||||
activeTab === '1' ? "暂无已使用水票" :
|
||||
"暂无失效水票"
|
||||
}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>
|
||||
@@ -450,14 +450,14 @@ const GiftCardManage = () => {
|
||||
<View className="text-gray-400 mb-4">
|
||||
<Gift size="48" />
|
||||
</View>
|
||||
<View className="text-gray-500 mb-2">暂无未使用礼品卡</View>
|
||||
<View className="text-gray-500 mb-2">暂无未使用水票</View>
|
||||
<View className="flex gap-2 justify-center">
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={handleRedeemGift}
|
||||
>
|
||||
兑换礼品卡
|
||||
兑换水票
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
|
||||
@@ -130,7 +130,7 @@ const GiftCardRedeem = () => {
|
||||
const transformGiftData = (gift: ShopGift) => {
|
||||
return {
|
||||
id: gift.id || 0,
|
||||
name: gift.name || '礼品卡',
|
||||
name: gift.name || '水票',
|
||||
description: gift.description,
|
||||
code: gift.code,
|
||||
goodsImage: gift.goodsImage,
|
||||
|
||||
@@ -121,7 +121,7 @@ const GiftCardUse = () => {
|
||||
const transformGiftData = (gift: ShopGift) => {
|
||||
return {
|
||||
id: gift.id || 0,
|
||||
name: gift.name || '礼品卡',
|
||||
name: gift.name || '水票',
|
||||
description: gift.description,
|
||||
code: gift.code,
|
||||
goodsImage: gift.goodsImage,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {Avatar, Cell, Space, Empty, Tabs, Button, TabPane, Image, Dialog} from '@nutui/nutui-react-taro'
|
||||
import {useEffect, useState, useCallback, CSSProperties} from "react";
|
||||
import {useEffect, useState, useCallback, useRef, CSSProperties} from "react";
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro';
|
||||
import {InfiniteLoading} from '@nutui/nutui-react-taro'
|
||||
import dayjs from "dayjs";
|
||||
import {pageShopOrder, updateShopOrder, createOrder} from "@/api/shop/shopOrder";
|
||||
import {ShopOrder, ShopOrderParam} from "@/api/shop/shopOrder/model";
|
||||
import {pageShopOrder, updateShopOrder, createOrder, getShopOrder, prepayShopOrder} from "@/api/shop/shopOrder";
|
||||
import {OrderCreateRequest, ShopOrder, ShopOrderParam} from "@/api/shop/shopOrder/model";
|
||||
import {listShopOrderGoods} from "@/api/shop/shopOrderGoods";
|
||||
import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
|
||||
import {copyText} from "@/utils/common";
|
||||
import PaymentCountdown from "@/components/PaymentCountdown";
|
||||
import {PaymentType} from "@/utils/payment";
|
||||
import {goTo} from "@/utils/navigation";
|
||||
import {ErrorType, RequestError} from "@/utils/request";
|
||||
|
||||
// 判断订单是否支付已过期
|
||||
const isPaymentExpired = (createTime: string, timeoutHours: number = 24): boolean => {
|
||||
@@ -78,22 +78,24 @@ const tabs = [
|
||||
}
|
||||
]
|
||||
|
||||
// 扩展订单接口,包含商品信息
|
||||
interface OrderWithGoods extends ShopOrder {
|
||||
orderGoods?: ShopOrderGoods[];
|
||||
}
|
||||
|
||||
interface OrderListProps {
|
||||
onReload?: () => void;
|
||||
searchParams?: ShopOrderParam;
|
||||
showSearch?: boolean;
|
||||
onSearchParamsChange?: (params: ShopOrderParam) => void; // 新增:通知父组件参数变化
|
||||
// 订单视图模式:用户/门店/骑手
|
||||
mode?: 'user' | 'store' | 'rider';
|
||||
// 固定过滤条件(例如 storeId / riderId),会合并到每次请求里
|
||||
baseParams?: ShopOrderParam;
|
||||
// 只读模式:隐藏“支付/取消/确认收货/退款”等用户操作按钮
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
function OrderList(props: OrderListProps) {
|
||||
const [list, setList] = useState<OrderWithGoods[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [list, setList] = useState<ShopOrder[]>([])
|
||||
const pageRef = useRef(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [payingOrderId, setPayingOrderId] = useState<number | null>(null)
|
||||
// 根据传入的statusFilter设置初始tab索引
|
||||
const getInitialTabIndex = () => {
|
||||
if (props.searchParams?.statusFilter !== undefined) {
|
||||
@@ -113,6 +115,7 @@ function OrderList(props: OrderListProps) {
|
||||
const [orderToCancel, setOrderToCancel] = useState<ShopOrder | null>(null)
|
||||
const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
|
||||
const [orderToConfirmReceive, setOrderToConfirmReceive] = useState<ShopOrder | null>(null)
|
||||
const isReadOnly = props.readOnly || props.mode === 'store' || props.mode === 'rider'
|
||||
|
||||
// 获取订单状态文本
|
||||
const getOrderStatusText = (order: ShopOrder) => {
|
||||
@@ -129,8 +132,15 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
// 已付款后检查发货状态
|
||||
if (order.deliveryStatus === 10) return '待发货';
|
||||
if (order.deliveryStatus === 20) return '待收货';
|
||||
if (order.deliveryStatus === 30) return '已完成';
|
||||
if (order.formId === 10074) return '已完成';
|
||||
if (order.deliveryStatus === 20) {
|
||||
// 若订单没有配送员,沿用原“待收货”语义
|
||||
if (!order.riderId) return '待收货';
|
||||
// 配送员确认送达后(sendEndTime有值),才进入“待确认收货”
|
||||
if (order.sendEndTime && order.orderStatus !== 1) return '待确认收货';
|
||||
return '配送中';
|
||||
}
|
||||
if (order.deliveryStatus === 30) return '部分发货';
|
||||
|
||||
// 最后检查订单完成状态
|
||||
if (order.orderStatus === 1) return '已完成';
|
||||
@@ -153,8 +163,12 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
// 已付款后检查发货状态
|
||||
if (order.deliveryStatus === 10) return 'text-blue-500'; // 待发货
|
||||
if (order.deliveryStatus === 20) return 'text-purple-500'; // 待收货
|
||||
if (order.deliveryStatus === 30) return 'text-green-500'; // 已收货
|
||||
if (order.deliveryStatus === 20) {
|
||||
if (!order.riderId) return 'text-purple-500'; // 待收货
|
||||
if (order.sendEndTime && order.orderStatus !== 1) return 'text-purple-500'; // 待确认收货
|
||||
return 'text-blue-500'; // 配送中
|
||||
}
|
||||
if (order.deliveryStatus === 30) return 'text-blue-500'; // 部分发货
|
||||
|
||||
// 最后检查订单完成状态
|
||||
if (order.orderStatus === 1) return 'text-green-600'; // 已完成
|
||||
@@ -165,9 +179,13 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
// 使用后端统一的 statusFilter 进行筛选
|
||||
const getOrderStatusParams = (index: string | number) => {
|
||||
let params: ShopOrderParam = {};
|
||||
// 添加用户ID过滤
|
||||
params.userId = Taro.getStorageSync('UserId');
|
||||
let params: ShopOrderParam = {
|
||||
...(props.baseParams || {})
|
||||
};
|
||||
// 默认是用户视图:添加 userId 过滤;门店/骑手视图由 baseParams 控制
|
||||
if (!props.mode || props.mode === 'user') {
|
||||
params.userId = Taro.getStorageSync('UserId');
|
||||
}
|
||||
|
||||
// 获取当前tab的statusFilter配置
|
||||
const currentTab = tabs.find(tab => tab.index === Number(index));
|
||||
@@ -183,12 +201,12 @@ function OrderList(props: OrderListProps) {
|
||||
const reload = useCallback(async (resetPage = false, targetPage?: number) => {
|
||||
setLoading(true);
|
||||
setError(null); // 清除之前的错误
|
||||
const currentPage = resetPage ? 1 : (targetPage || page);
|
||||
const currentPage = resetPage ? 1 : (targetPage || pageRef.current);
|
||||
const statusParams = getOrderStatusParams(tapIndex);
|
||||
// 合并搜索条件,tab的statusFilter优先级更高
|
||||
const searchConditions: any = {
|
||||
page: currentPage,
|
||||
userId: statusParams.userId, // 用户ID
|
||||
...statusParams,
|
||||
...props.searchParams, // 搜索关键词等其他条件
|
||||
};
|
||||
|
||||
@@ -205,50 +223,26 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
try {
|
||||
const res = await pageShopOrder(searchConditions);
|
||||
let newList: OrderWithGoods[];
|
||||
|
||||
if (res?.list && res?.list.length > 0) {
|
||||
// 批量获取订单商品信息,限制并发数量
|
||||
const batchSize = 3; // 限制并发数量为3
|
||||
const ordersWithGoods: OrderWithGoods[] = [];
|
||||
|
||||
for (let i = 0; i < res.list.length; i += batchSize) {
|
||||
const batch = res.list.slice(i, i + batchSize);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (order) => {
|
||||
try {
|
||||
const orderGoods = await listShopOrderGoods({orderId: order.orderId});
|
||||
return {
|
||||
...order,
|
||||
orderGoods: orderGoods || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取订单商品失败:', error);
|
||||
return {
|
||||
...order,
|
||||
orderGoods: []
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
ordersWithGoods.push(...batchResults);
|
||||
}
|
||||
// 订单分页接口已返回 orderGoods:列表直接使用该字段
|
||||
const incoming = res.list as ShopOrder[];
|
||||
|
||||
// 使用函数式更新避免依赖 list
|
||||
setList(prevList => {
|
||||
const newList = resetPage ? ordersWithGoods : (prevList || []).concat(ordersWithGoods);
|
||||
const newList = resetPage ? incoming : (prevList || []).concat(incoming);
|
||||
return newList;
|
||||
});
|
||||
|
||||
// 正确判断是否还有更多数据
|
||||
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
|
||||
const hasMoreData = incoming.length >= 10; // 假设每页10条数据
|
||||
setHasMore(hasMoreData);
|
||||
} else {
|
||||
setList(prevList => resetPage ? [] : prevList);
|
||||
setHasMore(false);
|
||||
}
|
||||
|
||||
setPage(currentPage);
|
||||
pageRef.current = currentPage;
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('加载订单失败:', error);
|
||||
@@ -260,14 +254,14 @@ function OrderList(props: OrderListProps) {
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}, [tapIndex, page, props.searchParams]); // 移除 list 依赖
|
||||
}, [tapIndex, props.searchParams]); // 移除 list/page 依赖,避免useEffect触发循环
|
||||
|
||||
const reloadMore = useCallback(async () => {
|
||||
if (loading || !hasMore) return; // 防止重复加载
|
||||
const nextPage = page + 1;
|
||||
setPage(nextPage);
|
||||
const nextPage = pageRef.current + 1;
|
||||
pageRef.current = nextPage;
|
||||
await reload(false, nextPage);
|
||||
}, [loading, hasMore, page, reload]);
|
||||
}, [loading, hasMore, reload]);
|
||||
|
||||
// 确认收货 - 显示确认对话框
|
||||
const confirmReceive = (order: ShopOrder) => {
|
||||
@@ -284,7 +278,7 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
await updateShopOrder({
|
||||
...orderToConfirmReceive,
|
||||
deliveryStatus: 20, // 已收货
|
||||
deliveryStatus: orderToConfirmReceive.deliveryStatus, // 10未发货 20已发货 30部分发货(收货由orderStatus控制)
|
||||
orderStatus: 1 // 已完成
|
||||
});
|
||||
|
||||
@@ -325,7 +319,7 @@ function OrderList(props: OrderListProps) {
|
||||
});
|
||||
|
||||
// 更新本地状态
|
||||
setDataSource(prev => prev.map(item =>
|
||||
setList(prev => prev.map(item =>
|
||||
item.orderId === order.orderId ? {...item, orderStatus: 4} : item
|
||||
));
|
||||
|
||||
@@ -353,47 +347,21 @@ function OrderList(props: OrderListProps) {
|
||||
// 再次购买 (已完成状态)
|
||||
const buyAgain = (order: ShopOrder) => {
|
||||
console.log('再次购买:', order);
|
||||
goTo(`/shop/orderConfirm/index?goodsId=${order.orderGoods[0].goodsId}`)
|
||||
const goodsId = order.orderGoods?.[0]?.goodsId
|
||||
if (!goodsId) {
|
||||
Taro.showToast({
|
||||
title: '订单商品信息缺失',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
goTo(`/shop/orderConfirm/index?goodsId=${goodsId}`)
|
||||
// Taro.showToast({
|
||||
// title: '再次购买功能开发中',
|
||||
// icon: 'none'
|
||||
// });
|
||||
};
|
||||
|
||||
// 评价商品 (已完成状态)
|
||||
const evaluateGoods = (order: ShopOrder) => {
|
||||
// 跳转到评价页面
|
||||
Taro.navigateTo({
|
||||
url: `/user/order/evaluate/index?orderId=${order.orderId}&orderNo=${order.orderNo}`
|
||||
});
|
||||
};
|
||||
|
||||
// 查看进度 (退款/售后状态)
|
||||
const viewProgress = (order: ShopOrder) => {
|
||||
// 根据订单状态确定售后类型
|
||||
let afterSaleType = 'refund' // 默认退款
|
||||
|
||||
if (order.orderStatus === 4) {
|
||||
afterSaleType = 'refund' // 退款申请中
|
||||
} else if (order.orderStatus === 7) {
|
||||
afterSaleType = 'return' // 退货申请中
|
||||
}
|
||||
|
||||
// 跳转到售后进度页面
|
||||
Taro.navigateTo({
|
||||
url: `/user/order/progress/index?orderId=${order.orderId}&orderNo=${order.orderNo}&type=${afterSaleType}`
|
||||
});
|
||||
};
|
||||
|
||||
// 撤销申请 (退款/售后状态)
|
||||
const cancelApplication = (order: ShopOrder) => {
|
||||
console.log('撤销申请:', order);
|
||||
Taro.showToast({
|
||||
title: '撤销申请功能开发中',
|
||||
icon: 'none'
|
||||
});
|
||||
};
|
||||
|
||||
// 取消订单
|
||||
const cancelOrder = (order: ShopOrder) => {
|
||||
setOrderToCancel(order);
|
||||
@@ -439,7 +407,7 @@ function OrderList(props: OrderListProps) {
|
||||
// 立即支付
|
||||
const payOrder = async (order: ShopOrder) => {
|
||||
try {
|
||||
if (!order.orderId || !order.orderNo) {
|
||||
if (!order.orderId) {
|
||||
Taro.showToast({
|
||||
title: '订单信息错误',
|
||||
icon: 'error'
|
||||
@@ -447,8 +415,40 @@ function OrderList(props: OrderListProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查订单是否已过期
|
||||
if (order.createTime && isPaymentExpired(order.createTime)) {
|
||||
if (payingOrderId === order.orderId) {
|
||||
return;
|
||||
}
|
||||
setPayingOrderId(order.orderId);
|
||||
|
||||
// 尽量以服务端最新状态为准,避免“已取消/已支付”但列表未刷新导致误发起支付
|
||||
let latestOrder: ShopOrder | null = null;
|
||||
try {
|
||||
latestOrder = await getShopOrder(order.orderId);
|
||||
} catch (_e) {
|
||||
// 忽略:网络波动时继续使用列表数据兜底
|
||||
}
|
||||
const effectiveOrder = latestOrder ? { ...order, ...latestOrder } : order;
|
||||
|
||||
if (effectiveOrder.payStatus) {
|
||||
Taro.showToast({
|
||||
title: '订单已支付',
|
||||
icon: 'none'
|
||||
});
|
||||
// 同步刷新一次,避免列表显示旧状态
|
||||
void reload(true);
|
||||
return;
|
||||
}
|
||||
if (effectiveOrder.orderStatus === 2) {
|
||||
Taro.showToast({
|
||||
title: '订单已取消,无法支付',
|
||||
icon: 'error'
|
||||
});
|
||||
void reload(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查订单是否已过期(以最新 createTime 为准)
|
||||
if (effectiveOrder.createTime && isPaymentExpired(effectiveOrder.createTime)) {
|
||||
Taro.showToast({
|
||||
title: '订单已过期,无法支付',
|
||||
icon: 'error'
|
||||
@@ -456,46 +456,69 @@ function OrderList(props: OrderListProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
if (order.payStatus) {
|
||||
Taro.showLoading({title: '发起支付...'});
|
||||
|
||||
// 构建商品数据:优先使用订单分页接口返回的 orderGoods;缺失时再补拉一次,避免goodsItems为空导致后端拒绝/再次支付失败
|
||||
let orderGoods = effectiveOrder.orderGoods || [];
|
||||
if (!orderGoods.length) {
|
||||
try {
|
||||
orderGoods = (await listShopOrderGoods({orderId: effectiveOrder.orderId})) || [];
|
||||
} catch (e) {
|
||||
// 继续走下面的校验提示
|
||||
console.error('补拉订单商品失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const goodsItems = orderGoods
|
||||
.filter(g => !!(g as any).goodsId || !!(g as any).itemId)
|
||||
.map(goods => ({
|
||||
goodsId: (goods.goodsId ?? (goods as any).itemId) as number,
|
||||
quantity: ((goods as any).quantity ?? goods.totalNum ?? 1) as number,
|
||||
// 若后端按SKU计算价格/库存,补齐SKU/规格信息更安全
|
||||
skuId: (goods as any).skuId ?? (goods as any).sku_id,
|
||||
specInfo: (goods as any).specInfo ?? (goods as any).spec
|
||||
}));
|
||||
|
||||
if (!goodsItems.length) {
|
||||
Taro.showToast({
|
||||
title: '订单已支付',
|
||||
title: '订单商品信息缺失,请稍后重试',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (order.orderStatus === 2) {
|
||||
Taro.showToast({
|
||||
title: '订单已取消,无法支付',
|
||||
icon: 'error'
|
||||
// 优先:对“已创建但未支付”的订单走“重新发起支付”接口(不应重复创建订单)
|
||||
// 若后端未提供该接口,则降级为重新创建订单(此时不传 orderNo,避免出现“相同订单号重复订单”)
|
||||
let result: any;
|
||||
let usedFallbackCreate = false;
|
||||
try {
|
||||
result = await prepayShopOrder({
|
||||
orderId: effectiveOrder.orderId!,
|
||||
payType: PaymentType.WECHAT
|
||||
});
|
||||
return;
|
||||
} catch (e) {
|
||||
// 订单状态等业务错误:直接提示,不要降级“重新创建订单”导致产生多笔订单
|
||||
if (e instanceof RequestError && e.type === ErrorType.BUSINESS_ERROR) {
|
||||
throw e;
|
||||
}
|
||||
usedFallbackCreate = true;
|
||||
const orderData: OrderCreateRequest = {
|
||||
goodsItems,
|
||||
addressId: effectiveOrder.addressId,
|
||||
payType: PaymentType.WECHAT,
|
||||
couponId: effectiveOrder.couponId,
|
||||
deliveryType: effectiveOrder.deliveryType,
|
||||
selfTakeMerchantId: effectiveOrder.selfTakeMerchantId,
|
||||
comments: effectiveOrder.comments,
|
||||
title: effectiveOrder.title,
|
||||
storeId: effectiveOrder.storeId,
|
||||
storeName: effectiveOrder.storeName,
|
||||
riderId: effectiveOrder.riderId,
|
||||
warehouseId: effectiveOrder.warehouseId
|
||||
};
|
||||
result = await createOrder(orderData);
|
||||
}
|
||||
|
||||
Taro.showLoading({title: '发起支付...'});
|
||||
|
||||
// 构建商品数据
|
||||
const goodsItems = order.orderGoods?.map(goods => ({
|
||||
goodsId: goods.goodsId,
|
||||
quantity: goods.totalNum || 1
|
||||
})) || [];
|
||||
|
||||
// 对于已存在的订单,我们需要重新发起支付
|
||||
// 构建支付请求数据,包含完整的商品信息
|
||||
const paymentData = {
|
||||
orderId: order.orderId,
|
||||
orderNo: order.orderNo,
|
||||
goodsItems: goodsItems,
|
||||
addressId: order.addressId,
|
||||
payType: PaymentType.WECHAT
|
||||
};
|
||||
|
||||
console.log('重新支付数据:', paymentData);
|
||||
|
||||
// 直接调用createOrder API进行重新支付
|
||||
const result = await createOrder(paymentData as any);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('支付发起失败');
|
||||
}
|
||||
@@ -506,13 +529,26 @@ function OrderList(props: OrderListProps) {
|
||||
}
|
||||
|
||||
// 调用微信支付
|
||||
await Taro.requestPayment({
|
||||
timeStamp: result.timeStamp,
|
||||
nonceStr: result.nonceStr,
|
||||
package: result.package,
|
||||
signType: (result.signType || 'MD5') as 'MD5' | 'HMAC-SHA256',
|
||||
paySign: result.paySign,
|
||||
});
|
||||
try {
|
||||
await Taro.requestPayment({
|
||||
timeStamp: result.timeStamp,
|
||||
nonceStr: result.nonceStr,
|
||||
package: result.package,
|
||||
signType: (result.signType || 'MD5') as 'MD5' | 'HMAC-SHA256',
|
||||
paySign: result.paySign,
|
||||
});
|
||||
} catch (payError: any) {
|
||||
const msg: string = payError?.errMsg || payError?.message || '';
|
||||
if (msg.includes('cancel')) {
|
||||
// 用户主动取消,不当作“失败”强提示
|
||||
Taro.showToast({
|
||||
title: '已取消支付',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw payError;
|
||||
}
|
||||
|
||||
// 支付成功
|
||||
Taro.showToast({
|
||||
@@ -520,6 +556,18 @@ function OrderList(props: OrderListProps) {
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 若因后端不支持“重新发起支付”而降级“重新创建订单”,则原订单会遗留为待支付,支付成功后自动将其标记为已取消,避免列表堆积
|
||||
if (usedFallbackCreate && effectiveOrder.orderId && !effectiveOrder.payStatus && effectiveOrder.orderStatus !== 2) {
|
||||
try {
|
||||
await updateShopOrder({
|
||||
orderId: effectiveOrder.orderId,
|
||||
orderStatus: 2
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('自动取消旧待支付订单失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载订单列表
|
||||
void reload(true);
|
||||
props.onReload?.();
|
||||
@@ -533,13 +581,14 @@ function OrderList(props: OrderListProps) {
|
||||
console.error('支付失败:', error);
|
||||
|
||||
let errorMessage = '支付失败,请重试';
|
||||
if (error.message) {
|
||||
if (error.message.includes('cancel')) {
|
||||
const rawMsg: string = error?.errMsg || error?.message || '';
|
||||
if (rawMsg) {
|
||||
if (rawMsg.includes('cancel')) {
|
||||
errorMessage = '用户取消支付';
|
||||
} else if (error.message.includes('余额不足')) {
|
||||
} else if (rawMsg.includes('余额不足')) {
|
||||
errorMessage = '账户余额不足';
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
errorMessage = rawMsg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,13 +598,13 @@ function OrderList(props: OrderListProps) {
|
||||
});
|
||||
} finally {
|
||||
Taro.hideLoading();
|
||||
setPayingOrderId(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
void reload(true); // 首次加载或tab切换时重置页码
|
||||
}, [tapIndex]); // 只监听tapIndex变化,避免reload依赖循环
|
||||
void reload(true); // 首次加载、tab切换或搜索条件变化时重置页码
|
||||
}, [reload]);
|
||||
|
||||
// 监听外部statusFilter变化,同步更新tab索引
|
||||
useEffect(() => {
|
||||
@@ -674,7 +723,7 @@ function OrderList(props: OrderListProps) {
|
||||
})
|
||||
?.map((item, index) => {
|
||||
return (
|
||||
<Cell key={index} style={{padding: '16px'}}
|
||||
<Cell key={item.orderId ?? item.orderNo ?? index} style={{padding: '16px'}}
|
||||
onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
|
||||
<Space direction={'vertical'} className={'w-full flex flex-col'}>
|
||||
<View className={'order-no flex justify-between'}>
|
||||
@@ -716,11 +765,16 @@ function OrderList(props: OrderListProps) {
|
||||
className={'rounded'}
|
||||
/>
|
||||
<View className={'ml-2 flex flex-col flex-1'}>
|
||||
<Text className={'text-sm font-bold'}>{goods.goodsName}</Text>
|
||||
{goods.spec && <Text className={'text-gray-500 text-xs'}>规格:{goods.spec}</Text>}
|
||||
<Text className={'text-gray-500 text-xs'}>数量:{goods.totalNum}</Text>
|
||||
<Text className={'text-sm font-bold'}>
|
||||
{goods.goodsName || (goods as any).goodsTitle || (goods as any).title || item.title || '订单商品'}
|
||||
</Text>
|
||||
{(goods.spec || (goods as any).specInfo) && (
|
||||
<Text className={'text-gray-500 text-xs'}>规格:{goods.spec || (goods as any).specInfo}</Text>
|
||||
)}
|
||||
<Text className={'text-gray-500 text-xs'}>数量:{(goods as any).quantity ?? goods.totalNum}</Text>
|
||||
</View>
|
||||
<Text className={'text-sm'}>¥{goods.price}</Text>
|
||||
<Text className={'text-gray-400 text-xs'}>x</Text>
|
||||
<Text className={'text-sm'}>¥{goods.price || (goods as any).payPrice}</Text>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
@@ -741,6 +795,7 @@ function OrderList(props: OrderListProps) {
|
||||
<Text className={'w-full text-right'}>实付金额:¥{item.payPrice}</Text>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{!isReadOnly && (
|
||||
<Space className={'btn flex justify-end'}>
|
||||
{/* 待付款状态:显示取消订单和立即支付 */}
|
||||
{(!item.payStatus) && item.orderStatus !== 2 && (
|
||||
@@ -765,7 +820,7 @@ function OrderList(props: OrderListProps) {
|
||||
)}
|
||||
|
||||
{/* 待收货状态:显示查看物流和确认收货 */}
|
||||
{item.deliveryStatus === 20 && item.orderStatus !== 2 && (
|
||||
{item.deliveryStatus === 20 && (!item.riderId || !!item.sendEndTime) && item.orderStatus !== 2 && item.orderStatus !== 6 && (
|
||||
<Space>
|
||||
<Button size={'small'} onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -789,6 +844,7 @@ function OrderList(props: OrderListProps) {
|
||||
{/* e.stopPropagation();*/}
|
||||
{/* evaluateGoods(item);*/}
|
||||
{/*}}>评价商品</Button>*/}
|
||||
|
||||
<Button size={'small'} onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
applyRefund(item);
|
||||
@@ -814,6 +870,7 @@ function OrderList(props: OrderListProps) {
|
||||
}}>再次购买</Button>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</Cell>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Space, NavBar, Button, Input} from '@nutui/nutui-react-taro'
|
||||
import {Search, Filter, ArrowLeft} from '@nutui/icons-react-taro'
|
||||
import {View} from '@tarojs/components';
|
||||
import OrderList from "./components/OrderList";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {useDidShow, useRouter} from '@tarojs/taro'
|
||||
import {ShopOrderParam} from "@/api/shop/shopOrder/model";
|
||||
import './order.scss'
|
||||
|
||||
@@ -72,6 +72,17 @@ function Order() {
|
||||
reload().then()
|
||||
}, []);
|
||||
|
||||
// 页面从其它页面返回/重新展示时,刷新一次列表数据
|
||||
// 典型场景:微信支付取消后返回到待支付列表,需要重新拉取订单/商品信息,避免使用旧数据再次支付失败
|
||||
useDidShow(() => {
|
||||
const statusFilter =
|
||||
params.statusFilter != undefined && params.statusFilter !== ''
|
||||
? parseInt(params.statusFilter)
|
||||
: -1;
|
||||
// 同步路由上的statusFilter,并触发子组件重新拉取列表
|
||||
setSearchParams(prev => ({ ...prev, statusFilter }));
|
||||
});
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<View style={{height: `${statusBarHeight || 0}px`, backgroundColor: '#ffffff'}}></View>
|
||||
|
||||
4
src/user/store/orders/index.config.ts
Normal file
4
src/user/store/orders/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '门店订单',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
73
src/user/store/orders/index.tsx
Normal file
73
src/user/store/orders/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {NavBar, Button} from '@nutui/nutui-react-taro'
|
||||
import {ArrowLeft} from '@nutui/icons-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import OrderList from '@/user/order/components/OrderList'
|
||||
import {getSelectedStoreFromStorage} from '@/utils/storeSelection'
|
||||
import {listShopStoreUser} from '@/api/shop/shopStoreUser'
|
||||
|
||||
export default function StoreOrders() {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0)
|
||||
|
||||
const [boundStoreId, setBoundStoreId] = useState<number | undefined>(undefined)
|
||||
const store = useMemo(() => getSelectedStoreFromStorage(), [])
|
||||
const storeId = boundStoreId || store?.id
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => setStatusBarHeight(res.statusBarHeight ?? 0)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// 优先按“店员绑定关系”确定门店归属:门店看到的是自己的订单
|
||||
const userId = Number(Taro.getStorageSync('UserId'))
|
||||
if (!Number.isFinite(userId) || userId <= 0) return
|
||||
listShopStoreUser({userId}).then(list => {
|
||||
const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId)
|
||||
if (first?.storeId) setBoundStoreId(first.storeId)
|
||||
}).catch(() => {
|
||||
// fallback to SelectedStore
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<View style={{height: `${statusBarHeight || 0}px`, backgroundColor: '#ffffff'}}></View>
|
||||
<NavBar
|
||||
fixed
|
||||
style={{marginTop: `${statusBarHeight || 0}px`, backgroundColor: '#ffffff'}}
|
||||
left={<ArrowLeft onClick={() => Taro.navigateBack()}/>}
|
||||
>
|
||||
<span>门店订单</span>
|
||||
</NavBar>
|
||||
|
||||
<View className="pt-14 px-3">
|
||||
<View className="bg-white rounded-lg p-3 mb-3">
|
||||
<Text className="text-sm text-gray-600">当前门店:</Text>
|
||||
<Text className="text-base font-medium">{store?.name || (boundStoreId ? `门店ID: ${boundStoreId}` : '未选择门店')}</Text>
|
||||
</View>
|
||||
|
||||
{!storeId ? (
|
||||
<View className="bg-white rounded-lg p-4">
|
||||
<Text className="text-sm text-gray-600">
|
||||
请先在首页左上角选择门店,再查看门店订单。
|
||||
</Text>
|
||||
<View className="mt-3">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => Taro.switchTab({url: '/pages/index/index'})}
|
||||
>
|
||||
去首页选择门店
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<OrderList mode="store" baseParams={{storeId}} readOnly />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -161,13 +161,13 @@ const StoreVerification: React.FC = () => {
|
||||
const getTypeText = (type: number) => {
|
||||
switch (type) {
|
||||
case 10:
|
||||
return '实物礼品卡'
|
||||
return '礼品劵'
|
||||
case 20:
|
||||
return '虚拟礼品卡'
|
||||
case 30:
|
||||
return '服务礼品卡'
|
||||
default:
|
||||
return '礼品卡'
|
||||
return '水票'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ const ThemeSelector: React.FC = () => {
|
||||
|
||||
// 获取当前主题
|
||||
useEffect(() => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'nature'
|
||||
setSelectedTheme(savedTheme)
|
||||
|
||||
if (savedTheme === 'auto') {
|
||||
// 自动主题:根据用户ID生成
|
||||
const userId = Taro.getStorageSync('userId') || '1'
|
||||
const theme = gradientUtils.getThemeByUserId(userId)
|
||||
const userId = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId') ?? '1'
|
||||
const theme = gradientUtils.getThemeByUserId(typeof userId === 'number' ? userId : parseInt(String(userId), 10))
|
||||
setCurrentTheme(theme)
|
||||
} else {
|
||||
// 手动选择的主题
|
||||
@@ -33,8 +33,8 @@ const ThemeSelector: React.FC = () => {
|
||||
setSelectedTheme(themeName)
|
||||
|
||||
if (themeName === 'auto') {
|
||||
const userId = Taro.getStorageSync('userId') || '1'
|
||||
const theme = gradientUtils.getThemeByUserId(userId)
|
||||
const userId = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId') ?? '1'
|
||||
const theme = gradientUtils.getThemeByUserId(typeof userId === 'number' ? userId : parseInt(String(userId), 10))
|
||||
setCurrentTheme(theme)
|
||||
} else {
|
||||
const theme = gradientThemes.find(t => t.name === themeName)
|
||||
@@ -61,8 +61,8 @@ const ThemeSelector: React.FC = () => {
|
||||
// 预览主题
|
||||
const previewTheme = (themeName: string) => {
|
||||
if (themeName === 'auto') {
|
||||
const userId = Taro.getStorageSync('userId') || '1'
|
||||
const theme = gradientUtils.getThemeByUserId(userId)
|
||||
const userId = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId') ?? '1'
|
||||
const theme = gradientUtils.getThemeByUserId(typeof userId === 'number' ? userId : parseInt(String(userId), 10))
|
||||
setCurrentTheme(theme)
|
||||
} else {
|
||||
const theme = gradientThemes.find(t => t.name === themeName)
|
||||
|
||||
4
src/user/ticket/add.config.ts
Normal file
4
src/user/ticket/add.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '新增收货地址',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
323
src/user/ticket/add.tsx
Normal file
323
src/user/ticket/add.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Button, Loading, CellGroup, Input, TextArea, Form, Switch, InputNumber, Radio, Image} from '@nutui/nutui-react-taro'
|
||||
import {Edit, Upload as UploadIcon} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopArticle} from "@/api/shop/shopArticle/model";
|
||||
import {getShopArticle, addShopArticle, updateShopArticle} from "@/api/shop/shopArticle";
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
|
||||
const AddShopArticle = () => {
|
||||
const {params} = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [formData, setFormData] = useState<ShopArticle>({
|
||||
type: 0, // 默认常规文章
|
||||
status: 0, // 默认已发布
|
||||
permission: 0, // 默认所有人可见
|
||||
recommend: 0, // 默认不推荐
|
||||
showType: 10, // 默认小图展示
|
||||
virtualViews: 0, // 默认虚拟阅读量
|
||||
actualViews: 0, // 默认实际阅读量
|
||||
sortNumber: 0 // 默认排序
|
||||
})
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
// 判断是编辑还是新增模式
|
||||
const isEditMode = !!params.id
|
||||
const articleId = params.id ? Number(params.id) : undefined
|
||||
|
||||
// 文章类型选项
|
||||
const typeOptions = [
|
||||
{ text: '常规文章', value: 0 },
|
||||
{ text: '视频文章', value: 1 }
|
||||
]
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ text: '已发布', value: 0 },
|
||||
{ text: '待审核', value: 1 },
|
||||
{ text: '已驳回', value: 2 },
|
||||
{ text: '违规内容', value: 3 }
|
||||
]
|
||||
|
||||
// 可见性选项
|
||||
const permissionOptions = [
|
||||
{ text: '所有人可见', value: 0 },
|
||||
{ text: '登录可见', value: 1 },
|
||||
{ text: '密码可见', value: 2 }
|
||||
]
|
||||
|
||||
// 显示方式选项
|
||||
const showTypeOptions = [
|
||||
{ text: '小图展示', value: 10 },
|
||||
{ text: '大图展示', value: 20 }
|
||||
]
|
||||
|
||||
const reload = async () => {
|
||||
// 如果是编辑模式,加载文章数据
|
||||
if (isEditMode && articleId) {
|
||||
try {
|
||||
const article = await getShopArticle(articleId)
|
||||
setFormData(article)
|
||||
// 更新表单值
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue(article)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载文章失败:', error)
|
||||
Taro.showToast({
|
||||
title: '加载文章失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图片上传处理
|
||||
const handleImageUpload = async () => {
|
||||
try {
|
||||
const res = await Taro.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera']
|
||||
});
|
||||
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
// 这里应该调用上传接口,暂时使用本地路径
|
||||
const imagePath = res.tempFilePaths[0];
|
||||
setFormData({
|
||||
...formData,
|
||||
image: imagePath
|
||||
});
|
||||
|
||||
Taro.showToast({
|
||||
title: '图片选择成功',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '图片选择失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
try {
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
...formData,
|
||||
...values,
|
||||
};
|
||||
|
||||
// 如果是编辑模式,添加id
|
||||
if (isEditMode && articleId) {
|
||||
submitData.articleId = articleId;
|
||||
}
|
||||
|
||||
// 执行新增或更新操作
|
||||
if (isEditMode) {
|
||||
await updateShopArticle(submitData);
|
||||
} else {
|
||||
await addShopArticle(submitData);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '保存'}成功`,
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '保存'}失败`,
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const submitFailed = (error: any) => {
|
||||
console.log(error, 'err...')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// 动态设置页面标题
|
||||
Taro.setNavigationBarTitle({
|
||||
title: isEditMode ? '编辑文章' : '新增文章'
|
||||
});
|
||||
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [isEditMode]);
|
||||
|
||||
if (loading) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
initialValues={formData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<CellGroup title="基本信息">
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="文章标题"
|
||||
required
|
||||
rules={[{ required: true, message: '请输入文章标题' }]}
|
||||
initialValue={formData.title}
|
||||
>
|
||||
<Input placeholder="请输入文章标题" maxLength={100}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="overview" label="文章概述" initialValue={formData.overview}>
|
||||
<TextArea placeholder="请输入文章概述,用于列表展示" maxLength={200} rows={3}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="detail"
|
||||
label="文章内容"
|
||||
required
|
||||
rules={[{ required: true, message: '请输入文章内容' }]}
|
||||
initialValue={formData.detail}
|
||||
>
|
||||
<TextArea placeholder="请输入文章内容" maxLength={10000} rows={8}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="author" label="作者" initialValue={formData.author}>
|
||||
<Input placeholder="请输入作者名称" maxLength={50}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="source" label="来源" initialValue={formData.source}>
|
||||
<Input placeholder="请输入文章来源" maxLength={100}/>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 文章设置 */}
|
||||
<CellGroup title="文章设置">
|
||||
<Form.Item name="type" label="文章类型" initialValue={formData.type}>
|
||||
<Radio.Group direction="horizontal" value={formData.type}>
|
||||
{typeOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="status" label="发布状态" initialValue={formData.status}>
|
||||
<Radio.Group direction="horizontal" value={formData.status}>
|
||||
{statusOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="permission" label="可见性" initialValue={formData.permission}>
|
||||
<Radio.Group direction="horizontal" value={formData.permission}>
|
||||
{permissionOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="showType" label="显示方式" initialValue={formData.showType}>
|
||||
<Radio.Group direction="horizontal" value={formData.showType}>
|
||||
{showTypeOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 高级设置 */}
|
||||
<CellGroup title="高级设置">
|
||||
<Form.Item name="recommend" label="推荐文章" initialValue={formData.recommend}>
|
||||
<Switch
|
||||
checked={formData.recommend === 1}
|
||||
onChange={(checked) =>
|
||||
setFormData({...formData, recommend: checked ? 1 : 0})
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="price" label="付费金额" initialValue={formData.price}>
|
||||
<Input placeholder="0.00" type="number"/>
|
||||
<View className="text-xs text-gray-500 mt-1">单位:元</View>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="virtualViews" label="虚拟阅读量" initialValue={formData.virtualViews}>
|
||||
<InputNumber min={0} defaultValue={formData.virtualViews || 0}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="actualViews" label="实际阅读量" initialValue={formData.actualViews}>
|
||||
<InputNumber min={0} defaultValue={formData.actualViews || 0}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="sortNumber" label="排序" initialValue={formData.sortNumber}>
|
||||
<InputNumber min={0} defaultValue={formData.sortNumber || 0}/>
|
||||
<View className="text-xs text-gray-500 mt-1">数字越小越靠前</View>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="tags" label="标签" initialValue={formData.tags}>
|
||||
<Input placeholder="请输入标签,多个标签用逗号分隔" maxLength={200}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="topic" label="话题" initialValue={formData.topic}>
|
||||
<Input placeholder="请输入话题" maxLength={100}/>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 图片上传 */}
|
||||
<CellGroup title="文章图片">
|
||||
<Form.Item name="image" label="封面图片" initialValue={formData.image}>
|
||||
<View className="flex items-center gap-3">
|
||||
{formData.image && (
|
||||
<Image
|
||||
src={formData.image}
|
||||
width="80"
|
||||
height="80"
|
||||
radius="8"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
fill="outline"
|
||||
icon={<UploadIcon />}
|
||||
onClick={handleImageUpload}
|
||||
>
|
||||
{formData.image ? '更换图片' : '上传图片'}
|
||||
</Button>
|
||||
</View>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<FixedButton text={isEditMode ? '更新文章' : '发布文章'} onClick={() => submitSucceed} icon={<Edit />} />
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddShopArticle;
|
||||
5
src/user/ticket/detail.config.ts
Normal file
5
src/user/ticket/detail.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '礼品卡详情',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
335
src/user/ticket/detail.tsx
Normal file
335
src/user/ticket/detail.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
import {useState, useEffect} from "react";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Button, ConfigProvider, Divider} from '@nutui/nutui-react-taro'
|
||||
import {Gift, Clock, Location, Phone, Copy, QrCode} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {ShopGift} from "@/api/shop/shopGift/model";
|
||||
import {getShopGift} from "@/api/shop/shopGift";
|
||||
import GiftCardShare from "@/components/GiftCardShare";
|
||||
import SimpleQRCodeModal from "@/components/SimpleQRCodeModal";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const GiftCardDetail = () => {
|
||||
const router = useRouter()
|
||||
const [gift, setGift] = useState<ShopGift | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showShare, setShowShare] = useState(false)
|
||||
const [showQRCode, setShowQRCode] = useState(false)
|
||||
const giftId = router.params.id
|
||||
|
||||
useEffect(() => {
|
||||
if (giftId) {
|
||||
loadGiftDetail()
|
||||
}
|
||||
}, [giftId])
|
||||
|
||||
const loadGiftDetail = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const data = await getShopGift(Number(giftId))
|
||||
setGift(data)
|
||||
} catch (error) {
|
||||
console.error('获取礼品卡详情失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取礼品卡详情失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取礼品卡类型文本
|
||||
const getGiftTypeText = (type?: number) => {
|
||||
switch (type) {
|
||||
case 10: return '礼品劵'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '水票'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取礼品卡面值显示
|
||||
const getGiftValueDisplay = () => {
|
||||
if (!gift || !gift.faceValue) return ''
|
||||
return `¥${gift.faceValue}`
|
||||
}
|
||||
|
||||
// 获取使用条件文本
|
||||
const getUsageText = () => {
|
||||
if (!gift) return ''
|
||||
|
||||
if (gift.instructions) {
|
||||
return gift.instructions
|
||||
}
|
||||
|
||||
switch (gift.type) {
|
||||
case 10: return '请到指定门店使用此礼品卡'
|
||||
case 20: return '可在线上平台直接使用'
|
||||
case 30: return '请联系客服预约服务时间'
|
||||
default: return '请按照使用说明进行操作'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取有效期文本
|
||||
const getValidityText = () => {
|
||||
if (!gift) return ''
|
||||
|
||||
if (gift.validDays) {
|
||||
return `有效期${gift.validDays}天`
|
||||
} else if (gift.expireTime) {
|
||||
return `有效期至 ${dayjs(gift.expireTime).format('YYYY年MM月DD日')}`
|
||||
} else {
|
||||
return '长期有效'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取礼品卡状态
|
||||
const getGiftStatus = () => {
|
||||
if (!gift) return { status: 0, text: '未知', color: 'default' }
|
||||
|
||||
switch (gift.status) {
|
||||
case 0:
|
||||
return { status: 0, text: '可使用', color: 'success' }
|
||||
case 1:
|
||||
return { status: 1, text: '已使用', color: 'warning' }
|
||||
case 2:
|
||||
return { status: 2, text: '已过期', color: 'danger' }
|
||||
default:
|
||||
return { status: 0, text: '未知', color: 'default' }
|
||||
}
|
||||
}
|
||||
|
||||
// 使用礼品卡 - 打开二维码弹窗
|
||||
const handleUseGift = () => {
|
||||
if (!gift) return
|
||||
setShowQRCode(true)
|
||||
}
|
||||
|
||||
// 点击二维码图标
|
||||
const handleQRCodeClick = () => {
|
||||
if (!gift) return
|
||||
setShowQRCode(true)
|
||||
}
|
||||
|
||||
// 复制兑换码
|
||||
const handleCopyCode = () => {
|
||||
if (!gift?.code) return
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: gift.code,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '兑换码已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const handleBack = () => {
|
||||
Taro.navigateBack()
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<View className="flex justify-center items-center h-screen">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
if (!gift) {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<View className="flex flex-col justify-center items-center h-screen">
|
||||
<Text className="text-gray-500 mb-4">礼品卡不存在</Text>
|
||||
<Button onClick={handleBack}>返回</Button>
|
||||
</View>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const statusInfo = getGiftStatus()
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
{/* 礼品卡卡片 */}
|
||||
<View className="m-4 p-6 rounded-2xl text-white" style={{background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)'}}>
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<View className="w-full">
|
||||
<Text className="text-4xl font-bold">{getGiftValueDisplay()}</Text>
|
||||
<Text className="opacity-90 mt-1 px-2">{getGiftTypeText(gift.type)}</Text>
|
||||
</View>
|
||||
<View
|
||||
className="p-2 bg-white bg-opacity-20 rounded-lg cursor-pointer"
|
||||
onClick={handleQRCodeClick}
|
||||
>
|
||||
<QrCode size="24" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text className="text-xl font-semibold mb-2">{gift.name}</Text>
|
||||
<Text className="text-base opacity-90 px-2">{gift.description || getUsageText()}</Text>
|
||||
|
||||
{/* 兑换码 */}
|
||||
{gift.code && (
|
||||
<View className="mt-4 p-3 bg-white bg-opacity-20 rounded-lg">
|
||||
<View className="flex items-center justify-between">
|
||||
<View>
|
||||
<Text className="text-sm opacity-80 px-2">兑换码</Text>
|
||||
<Text className="text-lg font-mono font-bold">{gift.code}</Text>
|
||||
</View>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Copy />}
|
||||
onClick={handleCopyCode}
|
||||
className="border-white text-white"
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 详细信息 */}
|
||||
<View className="bg-white mx-4 rounded-xl p-4">
|
||||
<Text className="text-lg font-semibold">使用说明</Text>
|
||||
|
||||
<View className={'mt-4'}>
|
||||
<View className="flex items-center mb-3">
|
||||
<Clock size="16" className="text-gray-400 mr-3" />
|
||||
<View>
|
||||
<Text className="text-gray-400 text-sm">有效期</Text>
|
||||
<Text className="text-blue-500 text-sm px-1">{getValidityText()}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View className="flex items-center mb-3">
|
||||
<Gift size="16" className="text-gray-400 mr-3" />
|
||||
<View>
|
||||
<Text className="text-gray-400 text-sm">类型</Text>
|
||||
<Text className="text-blue-500 text-sm px-1">{getGiftTypeText(gift.type)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{gift.useLocation && (
|
||||
<>
|
||||
<Divider />
|
||||
<View className="flex items-center">
|
||||
<Location size="16" className="text-gray-400 mr-3" />
|
||||
<View>
|
||||
<Text className="text-gray-400 text-sm">使用地址</Text>
|
||||
<Text className="text-blue-500 text-sm px-1">{gift.useLocation}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{gift.contactInfo && (
|
||||
<>
|
||||
<Divider />
|
||||
<View className="flex items-center">
|
||||
<Phone size="16" className="text-gray-400 mr-3" />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm">客服联系</Text>
|
||||
<Text className="text-gray-900">{gift.contactInfo}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{gift.instructions && (
|
||||
<>
|
||||
<Divider />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm mb-2">使用说明</Text>
|
||||
<Text className="text-gray-900 leading-relaxed">{gift.instructions}</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{gift.takeTime && (
|
||||
<>
|
||||
<Divider />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm mb-2">使用记录</Text>
|
||||
<Text className="text-gray-900">使用时间:{dayjs(gift.takeTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部操作按钮 */}
|
||||
{statusInfo.status === 0 && (
|
||||
<View className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
|
||||
<View className="flex gap-3">
|
||||
{gift.code && (
|
||||
<Button
|
||||
fill="outline"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
icon={<Copy />}
|
||||
onClick={handleCopyCode}
|
||||
>
|
||||
复制兑换码
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
onClick={handleUseGift}
|
||||
>
|
||||
立即使用
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 分享弹窗 */}
|
||||
{gift && (
|
||||
<GiftCardShare
|
||||
visible={showShare}
|
||||
giftCard={{
|
||||
id: gift.id || 0,
|
||||
name: gift.name || '',
|
||||
type: gift.type || 10,
|
||||
faceValue: gift.faceValue || '0',
|
||||
code: gift.code,
|
||||
description: gift.description
|
||||
}}
|
||||
onClose={() => setShowShare(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 二维码弹窗 */}
|
||||
{gift && (
|
||||
<SimpleQRCodeModal
|
||||
visible={showQRCode}
|
||||
onClose={() => setShowQRCode(false)}
|
||||
qrContent={gift.code + ''}
|
||||
giftName={gift.goodsName || gift.name}
|
||||
faceValue={gift.faceValue}
|
||||
/>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default GiftCardDetail;
|
||||
5
src/user/ticket/index.config.ts
Normal file
5
src/user/ticket/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的水票',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user