Compare commits

29 Commits

Author SHA1 Message Date
fcbaa970d0 feat(home): 添加商品上下架状态管理功能
- 在商品模型中新增status字段用于标识商品上下架状态
- 首页请求商品列表时默认传入status为0参数
- 商品列表数据过滤仅显示上架状态的商品
- 添加商品状态注释说明0为上架1为下架
2026-02-04 15:40:27 +08:00
5e36f243ef feat(order): 添加订单重新发起支付功能并优化支付流程
- 新增 prepayShopOrder 接口用于对未支付订单生成新预支付参数
- 实现多路径兼容探测机制,支持不同后端版本的支付接口
- 优化订单支付逻辑,优先使用服务端最新状态避免重复支付
- 添加 fallback 机制,在重新支付失败时降级为重新创建订单
- 实现支付成功后自动取消旧待支付订单,避免列表堆积
- 修复订单列表中key值重复的问题
- 在商品列表中添加数量标识符x提升UI显示效果
2026-02-04 15:32:27 +08:00
afe8f93c32 fix(config): 恢复开发和测试环境API配置
- 将开发环境API_BASE_URL从线上地址改回本地地址
- 在开发环境注释掉线上API地址配置
- 将测试环境API_BASE_URL从线上地址改回本地地址
- 在测试环境注释掉线上API地址配置

fix(order): 修复待收货状态下订单操作权限控制

- 在待收货状态判断条件中增加orderStatus !== 6的限制
- 防止已完成订单在待收货状态下显示查看物流和确认收货按钮
2026-02-04 15:10:31 +08:00
174f9458e2 fix(order): 修复订单列表中申请退款按钮的事件冒泡问题
- 在申请退款按钮点击事件中添加了 stopPropagation 防止事件冒泡
- 确保退款申请操作不会触发父级元素的点击事件
- 保持了原有的订单项数据传递逻辑不变
2026-02-04 14:45:34 +08:00
f96918bf86 feat(ticket): 添加水票功能支持
- 在订单模型中增加formId字段用于标识商品ID
- 更新统一扫码组件以支持水票和礼品卡核销
- 实现水票列表页面,包含我的水票和核销记录两个标签页
- 添加水票核销二维码生成功能
- 支持水票的分页加载和搜索功能
- 实现水票核销记录的展示
- 添加水票状态变更历史追踪
- 更新订单状态判断逻辑以支持特定商品完成状态
- 扩展扫码验证功能以处理水票业务类型
2026-02-04 11:00:54 +08:00
a3c952d092 feat(ticket): 添加水票功能模块
- 新增水票相关API接口,包括水票模板、用户水票、消费日志和水票释放功能
- 添加水票管理页面,实现水票的增删改查和详情展示功能
- 实现水票的分页查询和列表展示界面
- 替换原有的礼品卡功能为水票功能,在首页导航中更新路由链接
- 添加水票详情页面,支持二维码展示和兑换码复制功能
- 实现水票的状态管理和使用流程控制
2026-02-04 10:02:26 +08:00
cb17e48b03 feat(home): 优化首页商品展示功能
- 添加 recommend 字段到商品模型定义
- 重构首页标签页逻辑,支持分类参数传递
- 实现动态商品列表加载,按标签分类获取数据
- 更新订水跳转链接指向新商品ID
- 优化标签页切换逻辑,使用键值对映射
- 添加错误处理机制,防止商品列表加载失败
2026-02-03 20:06:50 +08:00
945bf9af8d refactor(order): 优化订单商品数据显示逻辑
- 将订单模型中的 orderGoods 类型从 OrderGoods 改为 ShopOrderGoods
- 移除 OrderWithGoods 接口定义和 normalizeOrderGoodsList 函数
- 直接使用订单分页接口返回的 orderGoods 字段渲染商品信息
- 添加 utils/orderGoods.ts 工具函数处理订单商品数据标准化
- 在骑手端订单页面实现商品名称汇总显示功能
- 优化再次购买和支付功能中的商品数据获取逻辑
2026-02-01 12:21:55 +08:00
dea40268fe refactor(order): 优化订单列表性能并移除冗余推荐人信息
- 移除经销商页面中的推荐人显示信息
- 将订单商品详情从单独接口请求改为直接从分页接口获取,避免N+1查询问题
- 添加normalizeOrderGoodsList函数实现订单商品数据结构标准化
- 统一门店名称文字颜色样式为灰色
- 简化支付工具类中的重复API端点调用
2026-02-01 11:51:28 +08:00
a2e34466d5 feat(store): 添加门店管理功能和订单配送功能
- 在app.config.ts中添加门店相关路由配置
- 在config/app.ts中添加租户名称常量
- 在Header.tsx中实现门店选择功能,包括定位、距离计算和门店切换
- 更新ShopOrder模型,添加门店ID、门店名称、配送员ID和仓库ID字段
- 新增ShopStore相关API和服务,支持门店的增删改查
- 新增ShopStoreRider相关API和服务,支持配送员管理
- 新增ShopStoreUser相关API和服务,支持店员管理
- 新增ShopWarehouse相关API和服务,支持仓库管理
- 添加配送订单页面,支持订单状态管理和送达确认功能
- 优化经销商页面的样式布局
2026-02-01 02:42:20 +08:00
3d82a0f194 feat(store): 添加门店管理功能和订单配送功能
- 在app.config.ts中添加门店相关路由配置
- 在config/app.ts中添加租户名称常量
- 在Header.tsx中实现门店选择功能,包括定位、距离计算和门店切换
- 更新ShopOrder模型,添加门店ID、门店名称、配送员ID和仓库ID字段
- 新增ShopStore相关API和服务,支持门店的增删改查
- 新增ShopStoreRider相关API和服务,支持配送员管理
- 新增ShopStoreUser相关API和服务,支持店员管理
- 新增ShopWarehouse相关API和服务,支持仓库管理
- 添加配送订单页面,支持订单状态管理和送达确认功能
- 优化经销商页面的样式布局
2026-02-01 01:39:49 +08:00
f8e689e250 feat(header): 替换网站名称为租户名称显示
- 引入User模型类型定义
- 添加userInfo状态管理
- 实现getTenantName方法获取租户名称
- 将Header组件中的getWebsiteName替换为getTenantName
- 在用户卡片组件中根据域名条件渲染角色标签
2026-01-31 22:47:02 +08:00
e07fd4091e refactor(withdraw): 移除快速提现金额中的无效选项
- 从 quickAmounts 数组中移除 '0.2' 选项
- 防止用户选择低于最低限额的快速金额

refactor(user): 优化用户角色名称获取逻辑

- 移除对 useUser hook 中 getRoleName 的依赖
- 在组件内部实现角色名称获取逻辑
- 优先取用户 roles 数组的第一个角色名称
- 添加默认角色名称为'注册用户'的回退机制
2026-01-31 22:30:51 +08:00
47d2eee486 feat(withdraw): 添加分销商提现领取功能
- 新增 receiveShopDealerWithdraw 接口用于用户领取提现
- 新增 receiveSuccessShopDealerWithdraw 接口用于领取成功回调
- 添加 ShopDealerWithdrawReceiveResult 类型定义
- 实现提取 package_info 的 extractPackageInfo 函数
- 更新提现列表页面的领取按钮样式
- 完善领取流程的状态处理和错误提示机制
2026-01-31 22:24:51 +08:00
3b98dfa150 feat(dealer): 更新提现流程为审核后领取模式
- 添加新的API接口getShopDealerWithdraw和updateShopDealerWithdraw
- 新增package_info相关字段用于微信确认收款流程
- 添加claimingId状态管理用于控制领取按钮
- 修改状态显示逻辑,将"审核通过"改为"待领取",颜色从success改为info
- 移除直接调用微信收款确认的逻辑,改为先提交审核再领取
- 新增handleClaim函数处理提现领取流程
- 在提现记录中添加"立即领取"按钮,仅在待领取状态下显示
- 更新提现说明文案,明确审核后领取流程
- 调整记录列表界面布局,优化时间显示和按钮位置
2026-01-31 22:04:59 +08:00
3a68955f1c refactor(dealer): 简化提现功能只支持微信钱包
- 移除支付宝和银行卡提现方式的选择
- 删除相关账户信息输入字段验证逻辑
- 简化提现表单只保留微信钱包选项
- 更新快速金额按钮配置
- 移除多余的状态管理变量
- 删除不再使用的 Radio 和 Cell 组件导入
- 移除提现
2026-01-31 21:14:10 +08:00
b9c03be394 feat(withdraw): 实现微信商家转账收款确认功能
- 配置文件中更新测试环境API基础URL
- 添加ShopDealerWithdrawCreateResult类型定义以支持微信转账返回的package_info
- 修改addShopDealerWithdraw函数以处理微信转账流程的特殊返回值
- 实现extractPackageInfo、canRequestMerchantTransferConfirm和requestMerchantTransferConfirm辅助函数
- 在微信钱包提现流程中集成商户转账确认页面调用
- 添加对微信小程序环境的检测和错误处理
- 更新快速金额选项,增加1元选项
- 修改微信提现提示文字,说明需要确认收款的流程
2026-01-31 18:57:32 +08:00
3a42eaf853 feat(gift): 将礼品卡功能重命名为水票并添加新增页面路由
- 将所有"礼品卡"文本替换为"水票",包括页面标题、组件文案、注释等
- 修改首页导航,将充值水票按钮指向我的水票页面
- 调整订水按钮链接直接跳转到商品详情页
- 移除帮助按钮相关代码
- 更新数据转换函数中的面值规格文案
- 修改核销成功提示中的商品类型文案
- 调整空状态提示文案为水票相关内容
- 在应用配置中添加新的水票添加页面路由
- 更新类型定义中的注释说明
2026-01-31 13:39:10 +08:00
f5c6d52b78 feat(rider): 添加配送员模块和订单图片保存功能
- 新增配送员首页界面,包含订单管理、工资明细、配送小区、仓库地址等功能入口
- 实现小程序码保存到相册功能,支持权限检查和错误处理
- 添加相册写入权限配置和图片下载临时路径处理
- 修复订单列表商品信息显示问题,优化支付流程
- 更新首页轮播图广告代码,调整用户中心网格布局
- 增加订单页面返回时的数据刷新机制,提升用户体验
2026-01-31 02:52:28 +08:00
7227ec6d84 fix(dealer): 修复经销商提现功能中的金额处理问题
- 添加 normalizeMoneyString 函数统一处理后端返回的金额数据类型
- 使用 normalizeMoneyString 替代直接访问 dealerUser.money 确保金额始终为字符串
- 修改金额验证逻辑确保数值转换的准确性
- 更新格式化金额函数支持未知类型输入并添加数值有效性检查
- 修复 Radio.Group 控件值更新时表单字段同步问题
2026-01-28 14:30:07 +08:00
ed5ef3fb19 feat(register): 移除注册页面并调整经销商申请流程
- 删除 passport/register.tsx 和 passport/register.config.ts 注册相关文件
- 从 app.config.ts 中移除 register 页面配置
- 将经销商申请页面标题从"邀请注册"改为"注册会员"
- 注释掉经销商申请表单中的邀请人ID字段
- 更新经销商申请页面导航栏标题文本
2026-01-28 10:22:14 +08:00
ed02db5a8d fix(dealer): 解决经销商申请注册流程中的角色分配问题
- 添加了对用户ID存在性的检查,避免注册时用户信息缺失导致的错误
- 实现了更健壮的角色查询逻辑,当查询不到角色时使用默认角色ID进行兜底
- 新增了addUserRole API方法用于在用户无角色时创建默认角色
- 优化了角色分配逻辑,支持upsert操作以处理不同后端实现方式
- 将页面跳转从navigateBack改为switchTab,确保注册后正确返回到"我的"页面
- 更新了API基础URL配置,统一指向新的mp-api域名
- 修复了二维码组件中的API地址引用,保持与新域名的一致性
2026-01-27 17:50:16 +08:00
a4938fbe31 fix(config): 更新API基础URL配置
- 将开发环境API_BASE_URL从mp-api.websoft.top更改为cms-api.websoft.top
- 将生产环境API_BASE_URL从mp-api.websoft.top更改为cms-api.websoft.top
- 将测试环境API_BASE_URL从mp-api.s209.websoft.top更改为cms-api.s209.websoft.top
- 更新SimpleQRCodeModal组件中的二维码生成API地址为cms-api.websoft.top
2026-01-27 16:53:13 +08:00
aff888794f feat(dealer): 添加分销商收益明细页面并优化订单管理功能
- 新增收益明细页面,支持下拉刷新和上拉加载更多
- 在app.config.ts中注册收益明细页面路由
- 更新API基础URL配置,统一使用mp-api域名
- 优化提交表单逻辑,确保refereeId参数为数字类型
- 修改订单页面,添加resourceId参数以正确过滤分销订单
- 修复订单号显示逻辑,优先使用接口返回的订单号
- 优化订单列表项点击事件,跳转到收益明细页面
- 更新客户名称显示格式,包含昵称和用户ID
- 调整订单详情展示布局和信息内容
2026-01-25 13:32:49 +08:00
0d6eb331c8 feat(shop): 添加商品分享邀请功能
- 切换API基础URL到生产环境地址
- 在商品详情页添加邀请参数解析和存储逻辑
- 实现分享链接携带邀请者ID和来源信息
- 新增商品分享来源类型标识
- 在短信登录成功后处理待绑定的邀请关系
- 添加邀请关系跟踪和统计功能
2026-01-20 15:18:48 +08:00
415e05cc4e feat(user): 添加用户卡片统计数据接口和优化性能
- 新增 UserCardStats 接口定义余额/积分/优惠券/礼品卡数据结构
- 实现 getUserCardStats 函数聚合返回用户卡片统计数据
- 替换原有多个独立请求为单一聚合接口提升性能
- 修改 useUserData Hook 使用新聚合接口并调整数据类型
- 移除废弃的 pageShopOrder 和相关 API 导入
- 优化用户登录后自动刷新卡片统计数据逻辑
2026-01-20 12:47:22 +08:00
0542b93dc7 feat(home): 重构首页轮播图组件并优化广告数据处理
- 修改首页轮播图组件,替换为新的 Banner 组件实现
- 新增广告图片数据标准化处理函数,支持多种字段格式兼容
- 优化首页广告数据加载逻辑,改用 Promise.allSettled 并行请求
- 修复轮播图高度计算,添加数字转换安全处理
- 调整经销商申请页面文本,将"入驻申请"改为"门店入驻"
- 修复商品卡片图片显示,添加空值处理防止报错
- 临时隐藏搜索栏组件,设置为隐藏状态
- 恢复开发环境 API 地址配置,便于本地调试
- 移除经销商申请表单中邀请人 ID 的禁用状态
2026-01-20 11:12:31 +08:00
0770eb1699 feat(home): 重构首页界面并更新API配置
- 移除底部导航栏中的"基地生活"选项卡
- 切换开发环境API地址为线上测试接口
- 添加完整的首页样式定义,包括英雄区域、商品卡片、快捷入口等
- 重构首页组件结构,集成商品列表、分类标签页和交互功能
- 更新主题管理逻辑,支持多种主题模式和用户ID兼容处理
- 添加商品数据获取和展示功能,实现首页内容动态加载
2026-01-15 10:12:49 +08:00
039af32fc3 config(app): 更新应用配置以适配新项目名称
- 将租户ID从10550更新为10584
- 将应用名称从"时里院子市集"更新为"桂乐淘"
- 更新package.json中的项目名称
- 更新project.config.json中的项目描述和APPID
- 更新Vercel项目配置名称
- 更新头条小程序项目描述
- 更新服务器模板ID配置
- 更新各页面分享标题中的应用名称
- 更新订单确认页面的评论字段值
2026-01-14 17:20:45 +08:00
112 changed files with 6948 additions and 958 deletions

View File

@@ -1 +1 @@
{"projectName":"trae_template-10550_mhk8"} {"projectName":"trae_template-10584_mhk8"}

View File

@@ -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;
// 当前版本 // 当前版本

View File

@@ -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',
} }

BIN
dist.zip Normal file

Binary file not shown.

View File

@@ -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.",

View File

@@ -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,

View File

@@ -1,7 +1,7 @@
{ {
"miniprogramRoot": "./", "miniprogramRoot": "./",
"projectname": "mp-react", "projectname": "mp-react",
"description": "时里院子市集", "description": "桂乐淘",
"appid": "touristappid", "appid": "touristappid",
"setting": { "setting": {
"urlCheck": true, "urlCheck": true,

View File

@@ -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'}>*/}

View 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));
}

View 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;
}

View 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));
}

View 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;
}

View 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));
}

View 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;
}

View 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));
}

View 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;
}

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -1,7 +1,7 @@
import type { PageParam } from '@/api'; import type { PageParam } from '@/api';
/** /**
* 礼品卡 * 水票
*/ */
export interface ShopGift { export interface ShopGift {
// 礼品卡ID // 礼品卡ID

View File

@@ -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;
} }

View File

@@ -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('发起支付失败'));
}
/** /**
* 创建订单 * 创建订单
*/ */

View File

@@ -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;
// 归属门店IDshop_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微信Native2会员卡支付3支付宝4现金5POS机6VIP月卡7VIP年卡8VIP次卡9IC月卡10IC年卡11IC次卡12免费13VIP充值卡14IC充值卡15积分支付16VIP季卡17IC季卡18代付 // 0余额支付, 1微信支付102微信Native2会员卡支付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[];
// 归属门店IDshop_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[];
// 归属门店IDshop_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;
// 归属门店IDshop_store.id
storeId?: number;
// 配送员用户ID
riderId?: number;
// 发货仓库ID
warehouseId?: number;
keywords?: string; keywords?: string;
deliveryStatus?: number; deliveryStatus?: number;
statusFilter?: number; statusFilter?: number;

View 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));
}

View 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;
// 默认仓库IDshop_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;
}

View 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));
}

View File

@@ -0,0 +1,67 @@
import type { PageParam } from '@/api';
/**
* 配送员
*/
export interface ShopStoreRider {
// 主键ID
id?: string;
// 配送点IDshop_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;
}

View 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));
}

View File

@@ -0,0 +1,36 @@
import type { PageParam } from '@/api';
/**
* 店员
*/
export interface ShopStoreUser {
// 主键ID
id?: number;
// 配送点IDshop_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;
}

View 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));
}

View 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;
}

View File

@@ -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));
}

View File

@@ -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))
}
/** /**
* 获取用户完整仪表板数据(一次性获取所有数据) * 获取用户完整仪表板数据(一次性获取所有数据)
*/ */

View File

@@ -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": "用于保存小程序码到相册,方便分享给好友"
} }
} }
} }

View File

@@ -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('分享成功');

View File

@@ -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 '水票'
} }
} }

View File

@@ -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: [
'🎁 实物礼品:需到指定地址领取商品', '🎁 礼品:需到指定地址领取商品',
'💻 虚拟礼品卡:自动发放到账户余额', '💻 虚拟礼品卡:自动发放到账户余额',
'🛎️ 服务礼品卡:联系客服预约服务', '🛎️ 服务礼品卡:联系客服预约服务',
'⏰ 注意查看有效期,过期无法使用' '⏰ 注意查看有效期,过期无法使用'

View File

@@ -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 '水票'
} }
} }

View File

@@ -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(); // 递归调用继续扫码

View File

@@ -1,4 +1,4 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '邀请注册', navigationBarTitleText: '注册会员',
navigationBarTextStyle: 'black' navigationBarTextStyle: 'black'
}) })

View File

@@ -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) 取不到就回退到旧的默认ID1848
}) 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

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '收益明细'
})

View File

@@ -0,0 +1,2 @@
/* Intentionally empty: styling is done via utility classes. */

View 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

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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')
})
})

View File

@@ -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>
)) ))
) : ( ) : (

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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]);

View File

@@ -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)
}, []) }, [])

View File

@@ -41,7 +41,7 @@ function Cart() {
useShareAppMessage(() => { useShareAppMessage(() => {
return { return {
title: '购物车 - 时里院子市集', title: '购物车 - 桂乐淘',
success: function () { success: function () {
console.log('分享成功'); console.log('分享成功');
}, },

View File

@@ -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('分享成功');

View File

@@ -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>
) )
} }

View File

@@ -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>
</> </>
) )

View File

@@ -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={{

View File

@@ -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平台启用硬件加速滚动 */
} }

View File

@@ -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>
</> </>
) )
} }

View File

@@ -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}/>}

View File

@@ -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>

View File

@@ -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

View File

@@ -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()
} }

View File

@@ -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

View File

@@ -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'

View File

@@ -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">

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '配送中心'
})

0
src/rider/index.scss Normal file
View File

295
src/rider/index.tsx Normal file
View 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

View File

@@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '配送订单',
navigationBarTextStyle: 'black'
}

390
src/rider/orders/index.tsx Normal file
View 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>
)
}

View File

@@ -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('分享成功');

View File

@@ -1,4 +1,4 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '注册账号', navigationBarTitleText: '立即订水',
navigationBarTextStyle: 'black' navigationBarTextStyle: 'black'
}) })

0
src/shop/gift/index.tsx Normal file
View File

View 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);

View File

@@ -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

View File

@@ -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>
); );
}; };

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '门店中心'
})

0
src/store/index.scss Normal file
View File

281
src/store/index.tsx Normal file
View 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

View File

@@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '门店订单',
navigationBarTextStyle: 'black'
}

View 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>
)
}

View File

@@ -4,7 +4,7 @@
/** 礼品卡类型枚举 */ /** 礼品卡类型枚举 */
export enum GiftCardType { export enum GiftCardType {
/** 实物礼品 */ /** 礼品 */
PHYSICAL = 10, PHYSICAL = 10,
/** 虚拟礼品卡 */ /** 虚拟礼品卡 */
VIRTUAL = 20, VIRTUAL = 20,

View File

@@ -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 '水票'
} }
} }

View File

@@ -1,5 +1,5 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '我的礼品卡', navigationBarTitleText: '我的水票',
navigationBarTextStyle: 'black', navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff' navigationBarBackgroundColor: '#ffffff'
}) })

View File

@@ -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"

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>
) )

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '门店订单',
navigationBarTextStyle: 'black'
}

View 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>
)
}

View File

@@ -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 '水票'
} }
} }

View File

@@ -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)

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '新增收货地址',
navigationBarTextStyle: 'black'
})

323
src/user/ticket/add.tsx Normal file
View 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;

View File

@@ -0,0 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '礼品卡详情',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff'
})

335
src/user/ticket/detail.tsx Normal file
View 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;

View 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