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