refactor(config): 将环境配置文件从 TypeScript 转换为 JavaScript
- 移除 config/env.ts 文件并将环境配置转换为 config/env.js - 更新 config/index.ts 中的导入路径以匹配新的 JavaScript 文件扩展名 - 修改 src/utils/server.ts 中的开发服务器 URL 配置 - 更新 tsconfig.json 的 include 配置移除 config 目录 - 调整环境配置中的 API 地址设置统一使用生产环境地址 - 更新 .workbuddy/expert-history.json 中的时间戳记录
This commit is contained in:
@@ -11,7 +11,62 @@
|
|||||||
"usedAt": 1775132175089,
|
"usedAt": 1775132175089,
|
||||||
"industryId": "all"
|
"industryId": "all"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"b1db410ffede4e03be74a98ac57ea478": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "Will",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1775753422786,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"3ad590301d144e91ac2d9497ad0b1a22": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "Will",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1775754410884,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"a07270b8a79e42f88a866c9e908129ad": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "Will",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1775755906815,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"c8760ea899514a1aae49666ae8a1f5e1": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "Will",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1775755982035,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"92099fa2ec454bf7b29102b6ab4b41a6": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "Will",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1775756293122,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"lastUpdated": 1775141282813
|
"lastUpdated": 1775756419382
|
||||||
}
|
}
|
||||||
90
.workbuddy/memory/2026-04-10.md
Normal file
90
.workbuddy/memory/2026-04-10.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# 2026-04-10 工作日志
|
||||||
|
|
||||||
|
## 完成的功能:四种用户分级标签
|
||||||
|
|
||||||
|
### 需求
|
||||||
|
用户 ID 下方"管理员"标签改为四种分级:
|
||||||
|
- 0: 普通用户
|
||||||
|
- 1: 超级管理员
|
||||||
|
- 2: 合伙人(总店)
|
||||||
|
- 3: 合伙人(分店)
|
||||||
|
|
||||||
|
### 修改的文件
|
||||||
|
|
||||||
|
#### 后端 (JAVA)
|
||||||
|
1. `ShopDealerUser.java` - 添加 `dealerLevel` 字段
|
||||||
|
2. `ShopDealerUserMapper.xml` - 添加 dealerLevel 查询条件
|
||||||
|
|
||||||
|
#### 前端 (VUE)
|
||||||
|
1. `src/api/shop/shopDealerUser/model/index.ts` - 添加 dealerLevel 字段
|
||||||
|
2. `src/utils/userLevel.ts` - 新建用户等级配置工具文件
|
||||||
|
3. `src/pages/user/components/UserCard.tsx` - 修改角色标签显示逻辑
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
- 需要在数据库中添加 `dealer_level` 字段
|
||||||
|
- 后端需要重启生效
|
||||||
|
- 前端通过 dealerLevel 字段判断显示对应样式
|
||||||
|
|
||||||
|
## 配送员订单通知功能
|
||||||
|
|
||||||
|
### 需求
|
||||||
|
客户下单后,配送员手机声音提示和红点提示功能
|
||||||
|
|
||||||
|
### 实现方案
|
||||||
|
1. **红点提示**:在配送员首页「配送订单」图标上显示待配送订单数量
|
||||||
|
2. **声音提示**:收到新订单时播放微信官方提示音
|
||||||
|
3. **设置功能**:支持开启/关闭声音提醒
|
||||||
|
|
||||||
|
### 新增/修改的文件
|
||||||
|
1. `src/api/glt/gltTicketOrder/index.ts` - 添加 `getRiderPendingCount` 接口
|
||||||
|
2. `src/hooks/useRiderNotification.ts` - 新建配送员通知 Hook
|
||||||
|
3. `src/rider/index.tsx` - 添加 Badge 红点和设置入口
|
||||||
|
|
||||||
|
### 技术实现
|
||||||
|
- 使用 30 秒轮询获取待配送订单数量
|
||||||
|
- 使用 NutUI 的 Badge 组件显示红点
|
||||||
|
- 使用 Taro.createInnerAudioContext 播放提示音
|
||||||
|
- 声音设置保存在本地存储 `rider_sound_enabled`
|
||||||
|
|
||||||
|
### 待后端配合
|
||||||
|
- 需要后端提供 `/glt/glt-ticket-order/rider/count` 接口(可选,使用现有 page 接口也行)
|
||||||
|
- 需要配置微信订阅消息模板(可选)
|
||||||
|
|
||||||
|
## 下单页配送方式 + 配送费功能
|
||||||
|
|
||||||
|
### 需求
|
||||||
|
1. 下单页必选配送方式:电梯 / 步梯 / 一楼商铺·其他
|
||||||
|
2. 步梯需二级选择是否送上楼,送上楼需选楼层
|
||||||
|
3. 配送费计算:每桶每层1元,第1层不收费(即 `(楼层-1) × 数量`)
|
||||||
|
4. 自提模式隐藏配送方式选择器
|
||||||
|
|
||||||
|
### 修改的文件
|
||||||
|
1. `src/api/shop/shopOrder/model/index.ts` - OrderCreateRequest 新增 deliveryMethod、deliveryFloor 字段
|
||||||
|
2. `src/utils/payment.ts` - buildSingleGoodsOrder 透传配送方式字段
|
||||||
|
3. `src/shop/orderConfirm/index.tsx` - 配送方式选择UI、配送费计算、楼层选择弹窗、支付校验
|
||||||
|
4. `src/shop/orderConfirm/index.scss` - 配送方式选择器、楼层网格样式
|
||||||
|
|
||||||
|
### 后端需配合
|
||||||
|
- 订单表新增 `delivery_method` 和 `delivery_floor` 字段
|
||||||
|
- 订单创建接口接收并存储这两个字段
|
||||||
|
- 骑手端/后台展示配送方式和楼层信息
|
||||||
|
|
||||||
|
## 微信订阅消息配置(补充)
|
||||||
|
|
||||||
|
### 需要做的配置
|
||||||
|
|
||||||
|
#### 后端配置
|
||||||
|
1. `GltSubscribeMessageServiceImpl.java` - 订阅消息发送服务
|
||||||
|
- 需要配置 `SUBSCRIBE_TEMPLATE_ID` 为实际模板ID
|
||||||
|
|
||||||
|
2. `GltTicketOrderController.java` - 订单创建时通知配送员
|
||||||
|
- 注入 `GltSubscribeMessageService` 和 `UserMapper`
|
||||||
|
- 添加 `notifyRidersOfNewOrder` 方法
|
||||||
|
|
||||||
|
#### 前端配置
|
||||||
|
1. `src/rider/index.tsx`
|
||||||
|
- `handleRequestSubscribeMessage` 函数需要配置实际的模板ID
|
||||||
|
|
||||||
|
### 微信后台需要申请的模板
|
||||||
|
模板名称:订单配送通知
|
||||||
|
关键词:订单状态、订单编号、配送地址、商品数量、通知时间
|
||||||
@@ -16,6 +16,21 @@ export async function pageGltTicketOrder(params: GltTicketOrderParam) {
|
|||||||
return Promise.reject(new Error(res.message));
|
return Promise.reject(new Error(res.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配送员待处理订单数量
|
||||||
|
* @param riderId 配送员ID
|
||||||
|
*/
|
||||||
|
export async function getRiderPendingCount(riderId: number) {
|
||||||
|
const res = await request.get<ApiResult<{ pendingCount: number; totalCount: number }>>(
|
||||||
|
'/glt/glt-ticket-order/rider/count',
|
||||||
|
{ riderId }
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return { pendingCount: 0, totalCount: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询送水订单列表
|
* 查询送水订单列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export interface ShopDealerUser {
|
|||||||
createTime?: string;
|
createTime?: string;
|
||||||
// 修改时间
|
// 修改时间
|
||||||
updateTime?: string;
|
updateTime?: string;
|
||||||
|
// 分销商等级:0-普通用户 1-超级管理员 2-合伙人(总店) 3-合伙人(分店)
|
||||||
|
dealerLevel?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -128,6 +128,10 @@ export interface ShopGoods {
|
|||||||
expiredDay?: number;
|
expiredDay?: number;
|
||||||
// 可购买数量
|
// 可购买数量
|
||||||
canBuyNumber?: number;
|
canBuyNumber?: number;
|
||||||
|
// 活动方式:0全平台 1新用户专享
|
||||||
|
activityType?: number;
|
||||||
|
// 配送方式:0送上门 1限自提
|
||||||
|
deliveryMode?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BathSet {
|
export interface BathSet {
|
||||||
|
|||||||
@@ -201,6 +201,10 @@ export interface OrderCreateRequest {
|
|||||||
selfTakeMerchantId?: number;
|
selfTakeMerchantId?: number;
|
||||||
// 订单标题(可选,后端会自动生成)
|
// 订单标题(可选,后端会自动生成)
|
||||||
title?: string;
|
title?: string;
|
||||||
|
// 配送方式:elevator(电梯) / stairs(步梯) / groundFloor(一楼商铺/其他)
|
||||||
|
deliveryMethod?: string;
|
||||||
|
// 楼层(步梯+送上楼时有值,从2开始)
|
||||||
|
deliveryFloor?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
98
src/dealer/components/FreezeMoneyModal.tsx
Normal file
98
src/dealer/components/FreezeMoneyModal.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { Popup, Button } from '@nutui/nutui-react-taro'
|
||||||
|
import { Close } from '@nutui/icons-react-taro'
|
||||||
|
|
||||||
|
interface FreezeMoneyModalProps {
|
||||||
|
visible: boolean
|
||||||
|
amount: string
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待使用明细弹窗组件
|
||||||
|
* 展示待使用(冻结中)金额和解冻规则说明
|
||||||
|
*/
|
||||||
|
const FreezeMoneyModal: React.FC<FreezeMoneyModalProps> = ({
|
||||||
|
visible,
|
||||||
|
amount,
|
||||||
|
onClose
|
||||||
|
}) => {
|
||||||
|
// 格式化金额,保留2位小数
|
||||||
|
const formatAmount = (value: string | number): string => {
|
||||||
|
const num = typeof value === 'string' ? parseFloat(value) : value
|
||||||
|
if (isNaN(num)) return '0.00'
|
||||||
|
return num.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popup
|
||||||
|
visible={visible}
|
||||||
|
style={{ padding: '0', borderRadius: '16px', overflow: 'hidden' }}
|
||||||
|
onClose={onClose}
|
||||||
|
closeOnOverlayClick={true}
|
||||||
|
position="center"
|
||||||
|
>
|
||||||
|
<View className="w-72 bg-white">
|
||||||
|
{/* 头部标题 */}
|
||||||
|
<View className="relative px-4 py-4 border-b border-gray-100">
|
||||||
|
<Text className="text-lg font-semibold text-gray-800 text-center block">
|
||||||
|
待使用明细
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 p-1"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<Close size={20} className="text-gray-400" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 金额展示区域 */}
|
||||||
|
<View className="px-6 py-8 text-center">
|
||||||
|
<Text className="text-sm text-gray-500 mb-2 block">
|
||||||
|
待使用(冻结中)
|
||||||
|
</Text>
|
||||||
|
<View className="flex items-baseline justify-center">
|
||||||
|
<Text className="text-2xl font-bold text-gray-800">¥</Text>
|
||||||
|
<Text className="text-4xl font-bold text-gray-800 ml-1">
|
||||||
|
{formatAmount(amount)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 分隔线 */}
|
||||||
|
<View className="mx-6 h-px bg-gray-100" />
|
||||||
|
|
||||||
|
{/* 温馨提示区域 */}
|
||||||
|
<View className="px-6 py-6">
|
||||||
|
<Text className="text-sm font-medium text-gray-700 mb-3 block">
|
||||||
|
温馨提示:
|
||||||
|
</Text>
|
||||||
|
<Text className="text-sm text-gray-500 leading-relaxed block">
|
||||||
|
需要直推用户进行下单配送后方可解冻到待提现资金组
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部按钮 */}
|
||||||
|
<View className="px-6 pb-6">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
height: '44px',
|
||||||
|
lineHeight: '44px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Popup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FreezeMoneyModal
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
|
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +14,7 @@ import {useUser} from '@/hooks/useUser'
|
|||||||
import { useThemeStyles } from '@/hooks/useTheme'
|
import { useThemeStyles } from '@/hooks/useTheme'
|
||||||
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
|
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
|
||||||
import {updateShopDealerUser} from '@/api/shop/shopDealerUser'
|
import {updateShopDealerUser} from '@/api/shop/shopDealerUser'
|
||||||
|
import FreezeMoneyModal from './components/FreezeMoneyModal'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
|
|
||||||
const DealerIndex: React.FC = () => {
|
const DealerIndex: React.FC = () => {
|
||||||
@@ -23,6 +24,9 @@ const DealerIndex: React.FC = () => {
|
|||||||
refresh,
|
refresh,
|
||||||
} = useDealerUser()
|
} = useDealerUser()
|
||||||
|
|
||||||
|
// 待使用明细弹窗显示状态
|
||||||
|
const [freezeMoneyModalVisible, setFreezeMoneyModalVisible] = useState(false)
|
||||||
|
|
||||||
// 获取用户角色信息
|
// 获取用户角色信息
|
||||||
const { hasRole } = useUser()
|
const { hasRole } = useUser()
|
||||||
|
|
||||||
@@ -63,8 +67,21 @@ const DealerIndex: React.FC = () => {
|
|||||||
// 判断是否是配送员
|
// 判断是否是配送员
|
||||||
const isRider = hasRole('rider')
|
const isRider = hasRole('rider')
|
||||||
|
|
||||||
// 点击待使用金额 - 配送员专用:将冻结金额转入可提现
|
// 点击待使用金额 - 显示待使用明细弹窗
|
||||||
const handleFreezeMoneyClick = async () => {
|
const handleFreezeMoneyClick = () => {
|
||||||
|
const freezeMoney = Number(dealerUser?.freezeMoney ?? 0)
|
||||||
|
if (freezeMoney > 0) {
|
||||||
|
setFreezeMoneyModalVisible(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭待使用明细弹窗
|
||||||
|
const handleCloseFreezeMoneyModal = () => {
|
||||||
|
setFreezeMoneyModalVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配送员专用:将冻结金额转入可提现
|
||||||
|
const handleTransferFreezeMoney = async () => {
|
||||||
// 检查是否是配送员
|
// 检查是否是配送员
|
||||||
if (!isRider) {
|
if (!isRider) {
|
||||||
return
|
return
|
||||||
@@ -76,6 +93,9 @@ const DealerIndex: React.FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
setFreezeMoneyModalVisible(false)
|
||||||
|
|
||||||
// 弹出确认框
|
// 弹出确认框
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
title: '确认操作',
|
title: '确认操作',
|
||||||
@@ -202,15 +222,15 @@ const DealerIndex: React.FC = () => {
|
|||||||
className="text-center p-3 rounded-lg flex flex-col"
|
className="text-center p-3 rounded-lg flex flex-col"
|
||||||
style={{
|
style={{
|
||||||
background: businessGradients.money.frozen,
|
background: businessGradients.money.frozen,
|
||||||
opacity: isRider && Number(dealerUser.freezeMoney ?? 0) > 0 ? 1 : 0.8
|
opacity: Number(dealerUser.freezeMoney ?? 0) > 0 ? 1 : 0.8
|
||||||
}}
|
}}
|
||||||
onClick={isRider && Number(dealerUser.freezeMoney ?? 0) > 0 ? handleFreezeMoneyClick : undefined}
|
onClick={Number(dealerUser.freezeMoney ?? 0) > 0 ? handleFreezeMoneyClick : undefined}
|
||||||
>
|
>
|
||||||
<Text className="text-lg font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
{formatMoney(dealerUser.freezeMoney)}
|
{formatMoney(dealerUser.freezeMoney)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>
|
||||||
{isRider && Number(dealerUser.freezeMoney ?? 0) > 0 ? '待使用' : '待使用'}
|
待使用
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||||
@@ -351,6 +371,13 @@ const DealerIndex: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 待使用明细弹窗 */}
|
||||||
|
<FreezeMoneyModal
|
||||||
|
visible={freezeMoneyModalVisible}
|
||||||
|
amount={dealerUser?.freezeMoney || '0'}
|
||||||
|
onClose={handleCloseFreezeMoneyModal}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 底部安全区域 */}
|
{/* 底部安全区域 */}
|
||||||
<View className="h-20"></View>
|
<View className="h-20"></View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
277
src/hooks/useRiderNotification.ts
Normal file
277
src/hooks/useRiderNotification.ts
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
/**
|
||||||
|
* 配送员订单通知 Hook
|
||||||
|
* 功能:
|
||||||
|
* 1. 轮询获取待配送订单数量
|
||||||
|
* 2. 新订单时播放提示音
|
||||||
|
* 3. 支持声音开关设置
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { pageGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||||
|
|
||||||
|
// 轮询间隔(毫秒)- 30秒
|
||||||
|
const POLL_INTERVAL = 30000
|
||||||
|
|
||||||
|
// 提示音 URL(使用微信官方提示音)
|
||||||
|
const NOTIFICATION_SOUND_URL = 'https://res.wx.qq.com/wechatApp/pkg/notice/notice.mp3'
|
||||||
|
|
||||||
|
interface RiderNotificationState {
|
||||||
|
/** 待配送订单数量 */
|
||||||
|
pendingCount: number
|
||||||
|
/** 所有配送中订单数量 */
|
||||||
|
totalCount: number
|
||||||
|
/** 是否正在轮询 */
|
||||||
|
isPolling: boolean
|
||||||
|
/** 最后更新的订单ID */
|
||||||
|
lastOrderId: number | null
|
||||||
|
/** 声音是否开启 */
|
||||||
|
soundEnabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseRiderNotificationReturn extends RiderNotificationState {
|
||||||
|
/** 开始轮询 */
|
||||||
|
startPolling: () => void
|
||||||
|
/** 停止轮询 */
|
||||||
|
stopPolling: () => void
|
||||||
|
/** 刷新数据 */
|
||||||
|
refresh: () => Promise<void>
|
||||||
|
/** 切换声音开关 */
|
||||||
|
toggleSound: () => void
|
||||||
|
/** 手动播放提示音 */
|
||||||
|
playSound: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配送员通知 Hook
|
||||||
|
* @param autoStart 是否自动开始轮询,默认 true
|
||||||
|
*/
|
||||||
|
export const useRiderNotification = (autoStart = true): UseRiderNotificationReturn => {
|
||||||
|
const [state, setState] = useState<RiderNotificationState>({
|
||||||
|
pendingCount: 0,
|
||||||
|
totalCount: 0,
|
||||||
|
isPolling: false,
|
||||||
|
lastOrderId: null,
|
||||||
|
soundEnabled: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const pollTimerRef = useRef<number | null>(null)
|
||||||
|
const audioContextRef = useRef<any>(null)
|
||||||
|
const lastCountRef = useRef(0)
|
||||||
|
const isMountedRef = useRef(true)
|
||||||
|
|
||||||
|
// 获取配送员ID
|
||||||
|
const getRiderId = useCallback(() => {
|
||||||
|
const riderId = Taro.getStorageSync('UserId')
|
||||||
|
return Number(riderId) || undefined
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 从本地存储加载声音设置
|
||||||
|
const loadSoundSetting = useCallback(() => {
|
||||||
|
try {
|
||||||
|
const enabled = Taro.getStorageSync('rider_sound_enabled')
|
||||||
|
return enabled !== '0' // 默认开启
|
||||||
|
} catch {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 保存声音设置到本地存储
|
||||||
|
const saveSoundSetting = useCallback((enabled: boolean) => {
|
||||||
|
try {
|
||||||
|
Taro.setStorageSync('rider_sound_enabled', enabled ? '1' : '0')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存声音设置失败', e)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 播放提示音
|
||||||
|
const playSound = useCallback(() => {
|
||||||
|
if (!state.soundEnabled) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 停止之前的音频
|
||||||
|
if (audioContextRef.current) {
|
||||||
|
audioContextRef.current.stop()
|
||||||
|
audioContextRef.current.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的音频实例
|
||||||
|
const audioContext = Taro.createInnerAudioContext()
|
||||||
|
audioContextRef.current = audioContext
|
||||||
|
|
||||||
|
audioContext.src = NOTIFICATION_SOUND_URL
|
||||||
|
audioContext.volume = 0.8
|
||||||
|
|
||||||
|
audioContext.play()
|
||||||
|
audioContext.onPlay(() => {
|
||||||
|
console.log('📢 新订单提示音播放中')
|
||||||
|
})
|
||||||
|
|
||||||
|
audioContext.onError((err: any) => {
|
||||||
|
console.error('提示音播放失败', err)
|
||||||
|
// 播放失败时尝试使用本地提示音
|
||||||
|
tryFallbackSound()
|
||||||
|
})
|
||||||
|
|
||||||
|
audioContext.onEnded(() => {
|
||||||
|
audioContext.destroy()
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('播放提示音异常', e)
|
||||||
|
}
|
||||||
|
}, [state.soundEnabled])
|
||||||
|
|
||||||
|
// 备用提示音方案(使用微信震动)
|
||||||
|
const tryFallbackSound = useCallback(() => {
|
||||||
|
try {
|
||||||
|
// 震动提示
|
||||||
|
Taro.vibrateShort({ type: 'heavy' })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('震动提示失败', e)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 获取未读订单数量
|
||||||
|
const fetchUnreadCount = useCallback(async () => {
|
||||||
|
const riderId = getRiderId()
|
||||||
|
if (!riderId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await pageGltTicketOrder({
|
||||||
|
riderId,
|
||||||
|
deliveryStatus: 10, // 待配送状态
|
||||||
|
page: 1,
|
||||||
|
limit: 1
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const pendingCount = res?.count || 0
|
||||||
|
const currentLastId = res?.list?.[0]?.id
|
||||||
|
|
||||||
|
// 如果数量增加,说明有新订单,播放提示音
|
||||||
|
if (pendingCount > lastCountRef.current && lastCountRef.current > 0) {
|
||||||
|
playSound()
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCountRef.current = pendingCount
|
||||||
|
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
pendingCount,
|
||||||
|
lastOrderId: currentLastId || prev.lastOrderId
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取未读订单失败', e)
|
||||||
|
}
|
||||||
|
}, [getRiderId, playSound])
|
||||||
|
|
||||||
|
// 开始轮询
|
||||||
|
const startPolling = useCallback(() => {
|
||||||
|
if (pollTimerRef.current) return
|
||||||
|
|
||||||
|
// 立即执行一次
|
||||||
|
fetchUnreadCount()
|
||||||
|
|
||||||
|
// 设置定时器
|
||||||
|
pollTimerRef.current = setInterval(() => {
|
||||||
|
fetchUnreadCount()
|
||||||
|
}, POLL_INTERVAL)
|
||||||
|
|
||||||
|
setState(prev => ({ ...prev, isPolling: true }))
|
||||||
|
}, [fetchUnreadCount])
|
||||||
|
|
||||||
|
// 停止轮询
|
||||||
|
const stopPolling = useCallback(() => {
|
||||||
|
if (pollTimerRef.current) {
|
||||||
|
clearInterval(pollTimerRef.current)
|
||||||
|
pollTimerRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止音频
|
||||||
|
if (audioContextRef.current) {
|
||||||
|
audioContextRef.current.stop()
|
||||||
|
audioContextRef.current.destroy()
|
||||||
|
audioContextRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(prev => ({ ...prev, isPolling: false }))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
lastCountRef.current = 0 // 重置计数,强制刷新
|
||||||
|
await fetchUnreadCount()
|
||||||
|
}, [fetchUnreadCount])
|
||||||
|
|
||||||
|
// 切换声音开关
|
||||||
|
const toggleSound = useCallback(() => {
|
||||||
|
setState(prev => {
|
||||||
|
const newEnabled = !prev.soundEnabled
|
||||||
|
saveSoundSetting(newEnabled)
|
||||||
|
return { ...prev, soundEnabled: newEnabled }
|
||||||
|
})
|
||||||
|
}, [saveSoundSetting])
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true
|
||||||
|
|
||||||
|
// 加载声音设置
|
||||||
|
const soundEnabled = loadSoundSetting()
|
||||||
|
setState(prev => ({ ...prev, soundEnabled }))
|
||||||
|
|
||||||
|
// 自动开始轮询
|
||||||
|
if (autoStart) {
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false
|
||||||
|
stopPolling()
|
||||||
|
}
|
||||||
|
}, [autoStart, loadSoundSetting, startPolling, stopPolling])
|
||||||
|
|
||||||
|
// 监听页面显示/隐藏
|
||||||
|
useEffect(() => {
|
||||||
|
const onShow = () => {
|
||||||
|
if (!state.isPolling) {
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onHide = () => {
|
||||||
|
// 页面隐藏时停止轮询节省电量
|
||||||
|
// stopPolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信小程序监听
|
||||||
|
if (typeof Taro.onAppShow === 'function') {
|
||||||
|
Taro.onAppShow(onShow)
|
||||||
|
}
|
||||||
|
if (typeof Taro.onAppHide === 'function') {
|
||||||
|
Taro.onAppHide(onHide)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (typeof Taro.offAppShow === 'function') {
|
||||||
|
Taro.offAppShow(onShow)
|
||||||
|
}
|
||||||
|
if (typeof Taro.offAppHide === 'function') {
|
||||||
|
Taro.offAppHide(onHide)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [state.isPolling, startPolling])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
startPolling,
|
||||||
|
stopPolling,
|
||||||
|
refresh,
|
||||||
|
toggleSound,
|
||||||
|
playSound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useRiderNotification
|
||||||
@@ -14,6 +14,7 @@ import {useThemeStyles} from "@/hooks/useTheme";
|
|||||||
import {getRootDomain} from "@/utils/domain";
|
import {getRootDomain} from "@/utils/domain";
|
||||||
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
|
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
|
||||||
import { saveStorageByLoginUser } from '@/utils/server'
|
import { saveStorageByLoginUser } from '@/utils/server'
|
||||||
|
import { getUserLevelName, getUserLevelConfig } from '@/utils/userLevel'
|
||||||
|
|
||||||
const UserCard = forwardRef<any, any>((_, ref) => {
|
const UserCard = forwardRef<any, any>((_, ref) => {
|
||||||
const {data, refresh} = useUserData()
|
const {data, refresh} = useUserData()
|
||||||
@@ -33,11 +34,26 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
return userInfo.nickname || (userInfo as any).realName || (userInfo as any).username || (IsLogin ? '用户' : '点击登录')
|
return userInfo.nickname || (userInfo as any).realName || (userInfo as any).username || (IsLogin ? '用户' : '点击登录')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 角色名称:优先取用户 roles 数组的第一个角色名称
|
// 角色名称:优先使用 dealerLevel 显示四种分级,否则取用户 roles 数组的第一个角色名称
|
||||||
const getRoleName = () => {
|
const getRoleName = () => {
|
||||||
|
const dealerLevel = (userInfo as any)?.dealerLevel
|
||||||
|
if (dealerLevel !== undefined && dealerLevel !== null) {
|
||||||
|
return getUserLevelName(dealerLevel)
|
||||||
|
}
|
||||||
return userInfo?.roles?.[0]?.roleName || userInfo?.roleName || '注册用户'
|
return userInfo?.roles?.[0]?.roleName || userInfo?.roleName || '注册用户'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取用户等级配置(用于自定义样式)
|
||||||
|
const getRoleLevelConfig = () => {
|
||||||
|
const dealerLevel = (userInfo as any)?.dealerLevel
|
||||||
|
if (dealerLevel !== undefined && dealerLevel !== null) {
|
||||||
|
return getUserLevelConfig(dealerLevel)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleLevelConfig = getRoleLevelConfig()
|
||||||
|
|
||||||
// 下拉刷新
|
// 下拉刷新
|
||||||
const reloadStats = async (showToast = false) => {
|
const reloadStats = async (showToast = false) => {
|
||||||
await refresh()
|
await refresh()
|
||||||
@@ -267,7 +283,22 @@ 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>
|
||||||
{getRootDomain() && (
|
{getRootDomain() && (
|
||||||
<View><Tag type="success">{getRoleName()}</Tag></View>
|
<View>
|
||||||
|
{roleLevelConfig ? (
|
||||||
|
<Tag
|
||||||
|
type={roleLevelConfig.tagType as any}
|
||||||
|
style={{
|
||||||
|
backgroundColor: roleLevelConfig.bgColor,
|
||||||
|
color: roleLevelConfig.textColor,
|
||||||
|
borderColor: roleLevelConfig.borderColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getRoleName()}
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag type="success">{getRoleName()}</Tag>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
|
import {ConfigProvider, Button, Grid, Avatar, Badge} from '@nutui/nutui-react-taro'
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
Shopping,
|
Shopping,
|
||||||
@@ -8,11 +8,13 @@ import {
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
Purse,
|
Purse,
|
||||||
People,
|
People,
|
||||||
Scan
|
Scan,
|
||||||
|
Setting
|
||||||
} from '@nutui/icons-react-taro'
|
} from '@nutui/icons-react-taro'
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import {useUser} from '@/hooks/useUser'
|
import {useUser} from '@/hooks/useUser'
|
||||||
import { useThemeStyles } from '@/hooks/useTheme'
|
import { useThemeStyles } from '@/hooks/useTheme'
|
||||||
|
import { useRiderNotification } from '@/hooks/useRiderNotification'
|
||||||
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
|
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
|
||||||
import {updateShopDealerUser} from '@/api/shop/shopDealerUser'
|
import {updateShopDealerUser} from '@/api/shop/shopDealerUser'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
@@ -27,6 +29,15 @@ const DealerIndex: React.FC = () => {
|
|||||||
// 获取用户角色信息
|
// 获取用户角色信息
|
||||||
const { hasRole } = useUser()
|
const { hasRole } = useUser()
|
||||||
|
|
||||||
|
// 配送员通知功能
|
||||||
|
const { pendingCount, startPolling, stopPolling, soundEnabled, toggleSound } = useRiderNotification()
|
||||||
|
|
||||||
|
// 页面生命周期管理
|
||||||
|
useEffect(() => {
|
||||||
|
startPolling()
|
||||||
|
return () => stopPolling()
|
||||||
|
}, [startPolling, stopPolling])
|
||||||
|
|
||||||
// 使用主题样式
|
// 使用主题样式
|
||||||
const themeStyles = useThemeStyles()
|
const themeStyles = useThemeStyles()
|
||||||
|
|
||||||
@@ -64,6 +75,55 @@ const DealerIndex: React.FC = () => {
|
|||||||
// 判断是否是配送员
|
// 判断是否是配送员
|
||||||
const isRider = hasRole('rider')
|
const isRider = hasRole('rider')
|
||||||
|
|
||||||
|
// 请求订阅消息授权
|
||||||
|
const handleRequestSubscribeMessage = () => {
|
||||||
|
// 微信订阅消息模板ID(需在微信公众平台配置后替换)
|
||||||
|
const templateIds = [
|
||||||
|
'YOUR_TEMPLATE_ID', // TODO: 替换为实际的订阅消息模板ID
|
||||||
|
]
|
||||||
|
|
||||||
|
// 过滤出有效的模板ID
|
||||||
|
const validTemplateIds = templateIds.filter(id => id && !id.includes('YOUR_'))
|
||||||
|
|
||||||
|
if (validTemplateIds.length === 0) {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '订阅消息功能尚未配置,请联系管理员',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求订阅
|
||||||
|
Taro.requestSubscribeMessage({
|
||||||
|
tmplIds: validTemplateIds,
|
||||||
|
success: (res) => {
|
||||||
|
console.log('订阅消息授权结果:', res)
|
||||||
|
const accepted = Object.values(res).some(v => v === 'accept')
|
||||||
|
if (accepted) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '订阅成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
// 保存授权状态到本地
|
||||||
|
Taro.setStorageSync('rider_subscribed', '1')
|
||||||
|
} else {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '您已拒绝订阅',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('订阅消息授权失败:', err)
|
||||||
|
Taro.showToast({
|
||||||
|
title: '授权失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 点击待使用金额 - 配送员专用:将冻结金额转入可提现
|
// 点击待使用金额 - 配送员专用:将冻结金额转入可提现
|
||||||
const handleFreezeMoneyClick = async () => {
|
const handleFreezeMoneyClick = async () => {
|
||||||
// 检查是否是配送员
|
// 检查是否是配送员
|
||||||
@@ -284,8 +344,15 @@ const DealerIndex: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Grid.Item text="配送订单" onClick={() => navigateToPage('/rider/orders/index')}>
|
<Grid.Item text="配送订单" onClick={() => navigateToPage('/rider/orders/index')}>
|
||||||
<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-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2 relative">
|
||||||
<Shopping color="#3b82f6" size="20"/>
|
<Shopping color="#3b82f6" size="20"/>
|
||||||
|
{pendingCount > 0 && (
|
||||||
|
<Badge
|
||||||
|
value={pendingCount > 99 ? '99+' : pendingCount}
|
||||||
|
max={99}
|
||||||
|
style={{ position: 'absolute', top: '-4px', right: '-4px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Grid.Item>
|
</Grid.Item>
|
||||||
@@ -323,46 +390,96 @@ const DealerIndex: React.FC = () => {
|
|||||||
</Grid.Item>
|
</Grid.Item>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* 第二行功能 */}
|
{/* 第二行功能 - 通知设置 */}
|
||||||
{/*<Grid*/}
|
<Grid
|
||||||
{/* columns={4}*/}
|
columns={4}
|
||||||
{/* className="no-border-grid mt-4"*/}
|
className="no-border-grid mt-4"
|
||||||
{/* style={{*/}
|
style={{
|
||||||
{/* '--nutui-grid-border-color': 'transparent',*/}
|
'--nutui-grid-border-color': 'transparent',
|
||||||
{/* '--nutui-grid-item-border-width': '0px',*/}
|
'--nutui-grid-item-border-width': '0px',
|
||||||
{/* border: 'none'*/}
|
border: 'none'
|
||||||
{/* } as React.CSSProperties}*/}
|
} as React.CSSProperties}
|
||||||
{/*>*/}
|
>
|
||||||
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
|
<Grid.Item text={'通知设置'} onClick={() => {
|
||||||
{/* <View className="text-center">*/}
|
const isSubscribed = Taro.getStorageSync('rider_subscribed') === '1'
|
||||||
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
Taro.showModal({
|
||||||
{/* <Presentation color="#6366f1" size="20"/>*/}
|
title: '通知设置',
|
||||||
{/* </View>*/}
|
content: `声音提醒:${soundEnabled ? '已开启' : '已关闭'}\n订阅消息:${isSubscribed ? '已订阅' : '未订阅'}`,
|
||||||
{/* </View>*/}
|
confirmText: '更多设置',
|
||||||
{/* </Grid.Item>*/}
|
cancelText: '关闭',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
// 显示更多设置选项
|
||||||
|
Taro.showActionSheet({
|
||||||
|
itemList: [
|
||||||
|
soundEnabled ? '关闭声音提醒' : '开启声音提醒',
|
||||||
|
isSubscribed ? '订阅状态正常' : '订阅消息通知',
|
||||||
|
'检查更新'
|
||||||
|
],
|
||||||
|
success: (sheetRes) => {
|
||||||
|
if (sheetRes.tapIndex === 0) {
|
||||||
|
// 切换声音
|
||||||
|
toggleSound()
|
||||||
|
Taro.showToast({
|
||||||
|
title: soundEnabled ? '已关闭声音' : '已开启声音',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} else if (sheetRes.tapIndex === 1) {
|
||||||
|
// 订阅消息
|
||||||
|
if (!isSubscribed) {
|
||||||
|
handleRequestSubscribeMessage()
|
||||||
|
} else {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '已订阅消息通知',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (sheetRes.tapIndex === 2) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '已是最新版本',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<View className="text-center">
|
||||||
|
<View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2 relative">
|
||||||
|
<Setting color={soundEnabled ? '#6366f1' : '#9ca3af'} size="20"/>
|
||||||
|
{soundEnabled ? (
|
||||||
|
<View className="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-white"></View>
|
||||||
|
) : (
|
||||||
|
<View className="absolute -bottom-1 -right-1 w-3 h-3 bg-gray-400 rounded-full border-2 border-white"></View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Grid.Item>
|
||||||
|
|
||||||
{/* /!* 预留其他功能位置 *!/*/}
|
{/* 预留功能位置 */}
|
||||||
{/* <Grid.Item text={''}>*/}
|
<Grid.Item text={''}>
|
||||||
{/* <View className="text-center">*/}
|
<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 className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
{/* </View>*/}
|
</View>
|
||||||
{/* </View>*/}
|
</View>
|
||||||
{/* </Grid.Item>*/}
|
</Grid.Item>
|
||||||
|
|
||||||
{/* <Grid.Item text={''}>*/}
|
<Grid.Item text={''}>
|
||||||
{/* <View className="text-center">*/}
|
<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 className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
{/* </View>*/}
|
</View>
|
||||||
{/* </View>*/}
|
</View>
|
||||||
{/* </Grid.Item>*/}
|
</Grid.Item>
|
||||||
|
|
||||||
{/* <Grid.Item text={''}>*/}
|
<Grid.Item text={''}>
|
||||||
{/* <View className="text-center">*/}
|
<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 className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
{/* </View>*/}
|
</View>
|
||||||
{/* </View>*/}
|
</View>
|
||||||
{/* </Grid.Item>*/}
|
</Grid.Item>
|
||||||
{/*</Grid>*/}
|
</Grid>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -377,6 +377,17 @@ const GoodsDetail = () => {
|
|||||||
<View className={"car-no text-lg"}>
|
<View className={"car-no text-lg"}>
|
||||||
{goods.name}
|
{goods.name}
|
||||||
</View>
|
</View>
|
||||||
|
{/* 活动/配送标签 */}
|
||||||
|
{(goods.activityType === 1 || goods.deliveryMode === 1) && (
|
||||||
|
<View className={"flex gap-1 py-1"}>
|
||||||
|
{goods.activityType === 1 && (
|
||||||
|
<Text className={"text-xs bg-red-500 text-white px-2 py-0.5 rounded-full"}>新用户专享</Text>
|
||||||
|
)}
|
||||||
|
{goods.deliveryMode === 1 && (
|
||||||
|
<Text className={"text-xs bg-orange-500 text-white px-2 py-0.5 rounded-full"}>仅限自提</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
<View className={"flex justify-between text-xs py-1"}>
|
<View className={"flex justify-between text-xs py-1"}>
|
||||||
<span className={"text-orange-500"}>
|
<span className={"text-orange-500"}>
|
||||||
{goods.comments}
|
{goods.comments}
|
||||||
|
|||||||
@@ -39,6 +39,189 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 配送方式选择
|
||||||
|
.delivery-method-group {
|
||||||
|
.delivery-method-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-method-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-method-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-method-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 16px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #f0f0f0;
|
||||||
|
background: #fafafa;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #07c160;
|
||||||
|
background: rgba(7, 193, 96, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-method-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-method-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-method-item.active .delivery-method-text {
|
||||||
|
color: #07c160;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否送上楼
|
||||||
|
.carry-upstairs-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px 0 0;
|
||||||
|
border-top: 1px dashed #eee;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carry-upstairs-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carry-upstairs-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid #f0f0f0;
|
||||||
|
background: #fafafa;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #07c160;
|
||||||
|
background: rgba(7, 193, 96, 0.05);
|
||||||
|
color: #07c160;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 楼层选择
|
||||||
|
.floor-select-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0 0;
|
||||||
|
border-top: 1px dashed #eee;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-select-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-fee-tip {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 楼层选择弹窗
|
||||||
|
.floor-picker-popup {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
text-align: center;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-grid-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14px 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid #f0f0f0;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #07c160;
|
||||||
|
background: rgba(7, 193, 96, 0.08);
|
||||||
|
color: #07c160;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
.address-bottom-line{
|
.address-bottom-line{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 12rpx 12rpx 0 0;
|
border-radius: 12rpx 12rpx 0 0;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
InputNumber,
|
InputNumber,
|
||||||
ConfigProvider
|
ConfigProvider
|
||||||
} from '@nutui/nutui-react-taro'
|
} from '@nutui/nutui-react-taro'
|
||||||
import {Location, ArrowRight} from '@nutui/icons-react-taro'
|
import {Location, ArrowRight, Shop} from '@nutui/icons-react-taro'
|
||||||
import Taro, {useDidShow} from '@tarojs/taro'
|
import Taro, {useDidShow} from '@tarojs/taro'
|
||||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||||
import {getShopGoods} from "@/api/shop/shopGoods";
|
import {getShopGoods} from "@/api/shop/shopGoods";
|
||||||
@@ -75,12 +75,21 @@ const OrderConfirm = () => {
|
|||||||
const [availableCoupons, setAvailableCoupons] = useState<CouponCardProps[]>([])
|
const [availableCoupons, setAvailableCoupons] = useState<CouponCardProps[]>([])
|
||||||
const [couponLoading, setCouponLoading] = useState<boolean>(false)
|
const [couponLoading, setCouponLoading] = useState<boolean>(false)
|
||||||
|
|
||||||
// 门店选择:用于在下单页展示当前“已选门店”,并允许用户切换(写入 SelectedStore Storage)
|
// 门店选择:用于在下单页展示当前"已选门店",并允许用户切换(写入 SelectedStore Storage)
|
||||||
const [storePopupVisible, setStorePopupVisible] = useState(false)
|
const [storePopupVisible, setStorePopupVisible] = useState(false)
|
||||||
const [stores, setStores] = useState<ShopStore[]>([])
|
const [stores, setStores] = useState<ShopStore[]>([])
|
||||||
const [storeLoading, setStoreLoading] = useState(false)
|
const [storeLoading, setStoreLoading] = useState(false)
|
||||||
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
|
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
|
||||||
|
|
||||||
|
// 配送方式:elevator(电梯) / stairs(步梯) / groundFloor(一楼商铺/其他)
|
||||||
|
const [deliveryMethod, setDeliveryMethod] = useState<string>('')
|
||||||
|
// 步梯是否需要送上楼(null=未选择)
|
||||||
|
const [needCarryUpstairs, setNeedCarryUpstairs] = useState<boolean | null>(null)
|
||||||
|
// 楼层(从2开始,需要送上楼时选择)
|
||||||
|
const [deliveryFloor, setDeliveryFloor] = useState<number>(2)
|
||||||
|
// 楼层选择弹窗
|
||||||
|
const [floorPickerVisible, setFloorPickerVisible] = useState(false)
|
||||||
|
|
||||||
const router = Taro.getCurrentInstance().router;
|
const router = Taro.getCurrentInstance().router;
|
||||||
const params = router?.params || ({} as Record<string, any>)
|
const params = router?.params || ({} as Record<string, any>)
|
||||||
const goodsIdParam = params?.goodsId
|
const goodsIdParam = params?.goodsId
|
||||||
@@ -213,11 +222,20 @@ const OrderConfirm = () => {
|
|||||||
return calculateCouponDiscount(selectedCoupon, total)
|
return calculateCouponDiscount(selectedCoupon, total)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算配送费:每桶每层1元,第1层不收费
|
||||||
|
const getDeliveryFee = () => {
|
||||||
|
// 仅步梯 + 需要送上楼 时才有配送费
|
||||||
|
if (deliveryMethod !== 'stairs' || !needCarryUpstairs) return 0
|
||||||
|
if (deliveryFloor <= 1) return 0
|
||||||
|
return quantity * (deliveryFloor - 1)
|
||||||
|
}
|
||||||
|
|
||||||
// 计算实付金额
|
// 计算实付金额
|
||||||
const getFinalPrice = () => {
|
const getFinalPrice = () => {
|
||||||
const total = getGoodsTotal()
|
const total = getGoodsTotal()
|
||||||
const discount = getCouponDiscount()
|
const discount = getCouponDiscount()
|
||||||
return Math.max(0, total - discount)
|
const deliveryFee = getDeliveryFee()
|
||||||
|
return Math.max(0, total - discount + deliveryFee)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -481,6 +499,24 @@ const OrderConfirm = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 配送方式校验(仅送货上门模式需要选择)
|
||||||
|
if (goods.deliveryMode !== 1 && !deliveryMethod) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '请选择配送方式',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步梯场景:必须选择是否送上楼
|
||||||
|
if (deliveryMethod === 'stairs' && needCarryUpstairs === null) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '请选择是否需要送上楼',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!payment) {
|
if (!payment) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '请选择支付方式',
|
title: '请选择支付方式',
|
||||||
@@ -551,11 +587,13 @@ const OrderConfirm = () => {
|
|||||||
address.id,
|
address.id,
|
||||||
{
|
{
|
||||||
comments: goods.name,
|
comments: goods.name,
|
||||||
deliveryType: 0,
|
deliveryType: goods.deliveryMode === 1 ? 1 : 0,
|
||||||
buyerRemarks: orderRemark,
|
buyerRemarks: orderRemark,
|
||||||
couponId: parseInt(String(bestCoupon.id), 10),
|
couponId: parseInt(String(bestCoupon.id), 10),
|
||||||
skuId: resolvedSkuId,
|
skuId: resolvedSkuId,
|
||||||
specInfo: orderDataParam?.specInfo
|
specInfo: orderDataParam?.specInfo,
|
||||||
|
deliveryMethod: deliveryMethod || undefined,
|
||||||
|
deliveryFloor: needCarryUpstairs ? deliveryFloor : undefined
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -603,12 +641,14 @@ const OrderConfirm = () => {
|
|||||||
address.id,
|
address.id,
|
||||||
{
|
{
|
||||||
comments: '桂乐淘',
|
comments: '桂乐淘',
|
||||||
deliveryType: 0,
|
deliveryType: goods.deliveryMode === 1 ? 1 : 0,
|
||||||
buyerRemarks: orderRemark,
|
buyerRemarks: orderRemark,
|
||||||
// 🔧 确保 couponId 是正确的数字类型,且不传递 undefined
|
// 🔧 确保 couponId 是正确的数字类型,且不传递 undefined
|
||||||
couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined,
|
couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined,
|
||||||
skuId: resolvedSkuId,
|
skuId: resolvedSkuId,
|
||||||
specInfo: orderDataParam?.specInfo
|
specInfo: orderDataParam?.specInfo,
|
||||||
|
deliveryMethod: deliveryMethod || undefined,
|
||||||
|
deliveryFloor: needCarryUpstairs ? deliveryFloor : undefined
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -704,7 +744,7 @@ const OrderConfirm = () => {
|
|||||||
message.includes('电子围栏') ||
|
message.includes('电子围栏') ||
|
||||||
message.includes('围栏')
|
message.includes('围栏')
|
||||||
|
|
||||||
// “配送范围”类错误给出更友好的解释,并提供快捷入口去更换收货地址
|
// "配送范围"类错误给出更友好的解释,并提供快捷入口去更换收货地址
|
||||||
if (isOutOfDeliveryRange) {
|
if (isOutOfDeliveryRange) {
|
||||||
try {
|
try {
|
||||||
const res = await Taro.showModal({
|
const res = await Taro.showModal({
|
||||||
@@ -827,7 +867,7 @@ const OrderConfirm = () => {
|
|||||||
setPayment(paymentRes[0])
|
setPayment(paymentRes[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载优惠券:使用“初始数量”对应的总价做推荐,避免默认数量变化导致推荐不准
|
// 加载优惠券:使用"初始数量"对应的总价做推荐,避免默认数量变化导致推荐不准
|
||||||
if (goodsRes) {
|
if (goodsRes) {
|
||||||
const initQty = (() => {
|
const initQty = (() => {
|
||||||
const n = Number(goodsRes?.canBuyNumber)
|
const n = Number(goodsRes?.canBuyNumber)
|
||||||
@@ -893,36 +933,52 @@ const OrderConfirm = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'order-confirm-page'}>
|
<div className={'order-confirm-page'}>
|
||||||
<CellGroup>
|
{goods.deliveryMode === 1 ? (
|
||||||
{
|
// 自提模式:显示到店自提提示
|
||||||
address && (
|
<CellGroup>
|
||||||
<Cell className={'address-bottom-line'} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
|
<Cell>
|
||||||
<Space>
|
|
||||||
<Location className={'text-gray-500'}/>
|
|
||||||
<View className={'flex flex-col w-full justify-between items-start'}>
|
|
||||||
<Space className={'flex flex-row w-full'}>
|
|
||||||
<View className={'flex-wrap text-nowrap whitespace-nowrap text-gray-500'}>送至</View>
|
|
||||||
<View className={'font-medium text-sm flex items-center w-full'}>
|
|
||||||
<View
|
|
||||||
style={{width: '64%'}}>{address.province} {address.city} {address.region} {address.address}</View>
|
|
||||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
|
||||||
</View>
|
|
||||||
</Space>
|
|
||||||
<View className={'pt-1 pb-3 text-gray-500'}>{address.name} {address.phone}</View>
|
|
||||||
</View>
|
|
||||||
</Space>
|
|
||||||
</Cell>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{!address && (
|
|
||||||
<Cell className={''} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
|
|
||||||
<Space>
|
<Space>
|
||||||
<Location/>
|
<Shop className={'text-orange-500'}/>
|
||||||
添加收货地址
|
<View className={'flex flex-col w-full'}>
|
||||||
|
<Text className={'font-medium text-orange-600'}>到店自提</Text>
|
||||||
|
<Text className={'text-gray-500 text-sm mt-1'}>请到店取货或出示核销码</Text>
|
||||||
|
</View>
|
||||||
</Space>
|
</Space>
|
||||||
</Cell>
|
</Cell>
|
||||||
)}
|
</CellGroup>
|
||||||
</CellGroup>
|
) : (
|
||||||
|
// 送货上门模式:显示地址选择
|
||||||
|
<CellGroup>
|
||||||
|
{
|
||||||
|
address && (
|
||||||
|
<Cell className={'address-bottom-line'} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
|
||||||
|
<Space>
|
||||||
|
<Location className={'text-gray-500'}/>
|
||||||
|
<View className={'flex flex-col w-full justify-between items-start'}>
|
||||||
|
<Space className={'flex flex-row w-full'}>
|
||||||
|
<View className={'flex-wrap text-nowrap whitespace-nowrap text-gray-500'}>送至</View>
|
||||||
|
<View className={'font-medium text-sm flex items-center w-full'}>
|
||||||
|
<View
|
||||||
|
style={{width: '64%'}}>{address.province} {address.city} {address.region} {address.address}</View>
|
||||||
|
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||||
|
</View>
|
||||||
|
</Space>
|
||||||
|
<View className={'pt-1 pb-3 text-gray-500'}>{address.name} {address.phone}</View>
|
||||||
|
</View>
|
||||||
|
</Space>
|
||||||
|
</Cell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{!address && (
|
||||||
|
<Cell className={''} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
|
||||||
|
<Space>
|
||||||
|
<Location/>
|
||||||
|
添加收货地址
|
||||||
|
</Space>
|
||||||
|
</Cell>
|
||||||
|
)}
|
||||||
|
</CellGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
{/*<CellGroup>*/}
|
{/*<CellGroup>*/}
|
||||||
{/* <Cell*/}
|
{/* <Cell*/}
|
||||||
@@ -944,6 +1000,88 @@ const OrderConfirm = () => {
|
|||||||
{/* />*/}
|
{/* />*/}
|
||||||
{/*</CellGroup>*/}
|
{/*</CellGroup>*/}
|
||||||
|
|
||||||
|
{/* 配送方式选择(仅送货上门模式显示) */}
|
||||||
|
{goods.deliveryMode !== 1 && (
|
||||||
|
<CellGroup className={'delivery-method-group'}>
|
||||||
|
<Cell>
|
||||||
|
<View className={'delivery-method-section'}>
|
||||||
|
<View className={'delivery-method-label'}>
|
||||||
|
<Text className={'font-medium text-sm'}>配送方式</Text>
|
||||||
|
<Text className={'text-red-500 text-xs ml-1'}>*</Text>
|
||||||
|
</View>
|
||||||
|
<View className={'delivery-method-options'}>
|
||||||
|
{[
|
||||||
|
{ key: 'elevator', label: '电梯', icon: '🏛️' },
|
||||||
|
{ key: 'stairs', label: '步梯', icon: '🚶' },
|
||||||
|
{ key: 'groundFloor', label: '一楼商铺/其他', icon: '🏪' },
|
||||||
|
].map(item => (
|
||||||
|
<View
|
||||||
|
key={item.key}
|
||||||
|
className={`delivery-method-item ${deliveryMethod === item.key ? 'active' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setDeliveryMethod(item.key)
|
||||||
|
// 切换配送方式时重置送上楼选项
|
||||||
|
setNeedCarryUpstairs(null)
|
||||||
|
setDeliveryFloor(2)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className={'delivery-method-icon'}>{item.icon}</Text>
|
||||||
|
<Text className={'delivery-method-text'}>{item.label}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 步梯:是否需要送上楼 */}
|
||||||
|
{deliveryMethod === 'stairs' && (
|
||||||
|
<View className={'carry-upstairs-section'}>
|
||||||
|
<Text className={'text-sm text-gray-600 mb-2'}>是否需要送上楼?</Text>
|
||||||
|
<View className={'carry-upstairs-options'}>
|
||||||
|
<View
|
||||||
|
className={`carry-upstairs-item ${needCarryUpstairs === true ? 'active' : ''}`}
|
||||||
|
onClick={() => setNeedCarryUpstairs(true)}
|
||||||
|
>
|
||||||
|
<Text>需要送上楼</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className={`carry-upstairs-item ${needCarryUpstairs === false ? 'active' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setNeedCarryUpstairs(false)
|
||||||
|
setDeliveryFloor(2)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>不需要</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 步梯+送上楼:选择楼层 */}
|
||||||
|
{deliveryMethod === 'stairs' && needCarryUpstairs === true && (
|
||||||
|
<View className={'floor-select-section'}>
|
||||||
|
<Text className={'text-sm text-gray-600'}>送至楼层</Text>
|
||||||
|
<View
|
||||||
|
className={'floor-select-btn'}
|
||||||
|
onClick={() => setFloorPickerVisible(true)}
|
||||||
|
>
|
||||||
|
<Text className={deliveryFloor > 1 ? 'text-gray-900' : 'text-gray-400'}>
|
||||||
|
{deliveryFloor > 1 ? `${deliveryFloor}楼` : '请选择楼层'}
|
||||||
|
</Text>
|
||||||
|
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||||
|
</View>
|
||||||
|
{deliveryFloor > 1 && (
|
||||||
|
<View className={'floor-fee-tip'}>
|
||||||
|
<Text className={'text-xs text-orange-500'}>
|
||||||
|
配送费:{quantity}桶 × {deliveryFloor - 1}层 = ¥{getDeliveryFee().toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Cell>
|
||||||
|
</CellGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
<CellGroup>
|
<CellGroup>
|
||||||
<Cell key={goods.goodsId}>
|
<Cell key={goods.goodsId}>
|
||||||
<View className={'flex w-full justify-between gap-3'}>
|
<View className={'flex w-full justify-between gap-3'}>
|
||||||
@@ -1042,7 +1180,14 @@ const OrderConfirm = () => {
|
|||||||
)}
|
)}
|
||||||
onClick={() => setCouponVisible(true)}
|
onClick={() => setCouponVisible(true)}
|
||||||
/>
|
/>
|
||||||
<Cell title={'配送费'} extra={'¥0.00'}/>
|
<Cell title={'配送费'} extra={
|
||||||
|
<View className={'flex items-center gap-1'}>
|
||||||
|
<Text>¥{getDeliveryFee().toFixed(2)}</Text>
|
||||||
|
{deliveryMethod === 'stairs' && needCarryUpstairs && deliveryFloor > 1 && (
|
||||||
|
<Text className={'text-xs text-gray-400'}>({quantity}桶×{deliveryFloor - 1}层)</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
}/>
|
||||||
<Cell extra={(
|
<Cell extra={(
|
||||||
<View className={'flex items-end gap-2'}>
|
<View className={'flex items-end gap-2'}>
|
||||||
<Text>已优惠</Text>
|
<Text>已优惠</Text>
|
||||||
@@ -1216,6 +1361,49 @@ const OrderConfirm = () => {
|
|||||||
</View>
|
</View>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|
||||||
|
{/* 楼层选择弹窗 */}
|
||||||
|
<Popup
|
||||||
|
visible={floorPickerVisible}
|
||||||
|
position="bottom"
|
||||||
|
onClose={() => setFloorPickerVisible(false)}
|
||||||
|
style={{height: '40vh'}}
|
||||||
|
>
|
||||||
|
<View className="floor-picker-popup">
|
||||||
|
<View className="floor-picker-popup__header">
|
||||||
|
<Text className="text-base font-medium">选择楼层</Text>
|
||||||
|
<Text
|
||||||
|
className="text-sm text-gray-500"
|
||||||
|
onClick={() => setFloorPickerVisible(false)}
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View className="floor-picker-popup__content">
|
||||||
|
<View className="floor-grid">
|
||||||
|
{Array.from({length: 32}, (_, i) => i + 2).map(f => (
|
||||||
|
<View
|
||||||
|
key={f}
|
||||||
|
className={`floor-grid-item ${deliveryFloor === f ? 'active' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setDeliveryFloor(f)
|
||||||
|
setFloorPickerVisible(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>{f}楼</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{deliveryFloor > 1 && (
|
||||||
|
<View className="floor-picker-popup__footer">
|
||||||
|
<Text className={'text-sm text-gray-600'}>
|
||||||
|
配送费:{quantity}桶 × {deliveryFloor - 1}层 = <Text className={'text-red-500 font-bold'}>¥{(quantity * (deliveryFloor - 1)).toFixed(2)}</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Popup>
|
||||||
|
|
||||||
<Gap height={50}/>
|
<Gap height={50}/>
|
||||||
|
|
||||||
<div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
|
<div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
|
||||||
@@ -1230,6 +1418,11 @@ const OrderConfirm = () => {
|
|||||||
已优惠 ¥{getCouponDiscount().toFixed(2)}
|
已优惠 ¥{getCouponDiscount().toFixed(2)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
{getDeliveryFee() > 0 && (
|
||||||
|
<View className={'text-xs text-orange-500'}>
|
||||||
|
含配送费 ¥{getDeliveryFee().toFixed(2)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={'buy-btn mx-4'}>
|
<div className={'buy-btn mx-4'}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -476,6 +476,8 @@ export function buildSingleGoodsOrder(
|
|||||||
specInfo?: string;
|
specInfo?: string;
|
||||||
buyerRemarks?: string;
|
buyerRemarks?: string;
|
||||||
sendStartTime?: string;
|
sendStartTime?: string;
|
||||||
|
deliveryMethod?: string;
|
||||||
|
deliveryFloor?: number;
|
||||||
}
|
}
|
||||||
): OrderCreateRequest {
|
): OrderCreateRequest {
|
||||||
return {
|
return {
|
||||||
@@ -493,7 +495,9 @@ export function buildSingleGoodsOrder(
|
|||||||
sendStartTime: options?.sendStartTime,
|
sendStartTime: options?.sendStartTime,
|
||||||
deliveryType: options?.deliveryType || 0,
|
deliveryType: options?.deliveryType || 0,
|
||||||
couponId: options?.couponId,
|
couponId: options?.couponId,
|
||||||
selfTakeMerchantId: options?.selfTakeMerchantId
|
selfTakeMerchantId: options?.selfTakeMerchantId,
|
||||||
|
deliveryMethod: options?.deliveryMethod,
|
||||||
|
deliveryFloor: options?.deliveryFloor
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
115
src/utils/userLevel.ts
Normal file
115
src/utils/userLevel.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* 用户等级配置
|
||||||
|
* 用于分销商角色的显示和样式管理
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 用户等级枚举 */
|
||||||
|
export enum UserLevel {
|
||||||
|
/** 普通用户 */
|
||||||
|
NORMAL = 0,
|
||||||
|
/** 超级管理员 */
|
||||||
|
SUPER_ADMIN = 1,
|
||||||
|
/** 合伙人(总店) */
|
||||||
|
PARTNER_HEAD = 2,
|
||||||
|
/** 合伙人(分店) */
|
||||||
|
PARTNER_BRANCH = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户等级配置接口 */
|
||||||
|
export interface UserLevelConfig {
|
||||||
|
level: UserLevel;
|
||||||
|
name: string;
|
||||||
|
/** Tag 组件的 type 属性 */
|
||||||
|
tagType: 'default' | 'success' | 'warning' | 'danger';
|
||||||
|
/** 背景色 */
|
||||||
|
bgColor: string;
|
||||||
|
/** 文字颜色 */
|
||||||
|
textColor: string;
|
||||||
|
/** 边框颜色 */
|
||||||
|
borderColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户等级配置表 */
|
||||||
|
export const USER_LEVEL_CONFIG: Record<UserLevel, UserLevelConfig> = {
|
||||||
|
[UserLevel.NORMAL]: {
|
||||||
|
level: UserLevel.NORMAL,
|
||||||
|
name: '普通用户',
|
||||||
|
tagType: 'default',
|
||||||
|
bgColor: '#f5f5f5',
|
||||||
|
textColor: '#666666',
|
||||||
|
borderColor: '#e5e5e5',
|
||||||
|
},
|
||||||
|
[UserLevel.SUPER_ADMIN]: {
|
||||||
|
level: UserLevel.SUPER_ADMIN,
|
||||||
|
name: '超级管理员',
|
||||||
|
tagType: 'danger',
|
||||||
|
bgColor: '#fff2f0',
|
||||||
|
textColor: '#cf1322',
|
||||||
|
borderColor: '#ffccc7',
|
||||||
|
},
|
||||||
|
[UserLevel.PARTNER_HEAD]: {
|
||||||
|
level: UserLevel.PARTNER_HEAD,
|
||||||
|
name: '合伙人',
|
||||||
|
tagType: 'warning',
|
||||||
|
bgColor: '#fff7e6',
|
||||||
|
textColor: '#d46b08',
|
||||||
|
borderColor: '#ffd8bf',
|
||||||
|
},
|
||||||
|
[UserLevel.PARTNER_BRANCH]: {
|
||||||
|
level: UserLevel.PARTNER_BRANCH,
|
||||||
|
name: '合伙人',
|
||||||
|
tagType: 'success',
|
||||||
|
bgColor: '#f6ffed',
|
||||||
|
textColor: '#389e0d',
|
||||||
|
borderColor: '#b7eb8f',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 显示名称(带后缀区分总店和分店) */
|
||||||
|
export const USER_LEVEL_DISPLAY_NAMES: Record<UserLevel, string> = {
|
||||||
|
[UserLevel.NORMAL]: '普通用户',
|
||||||
|
[UserLevel.SUPER_ADMIN]: '超级管理员',
|
||||||
|
[UserLevel.PARTNER_HEAD]: '合伙人(总店)',
|
||||||
|
[UserLevel.PARTNER_BRANCH]: '合伙人(分店)',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据等级值获取配置
|
||||||
|
* @param level 等级值
|
||||||
|
* @returns 用户等级配置
|
||||||
|
*/
|
||||||
|
export function getUserLevelConfig(level?: number): UserLevelConfig {
|
||||||
|
const validLevel = level ?? 0;
|
||||||
|
return USER_LEVEL_CONFIG[validLevel as UserLevel] || USER_LEVEL_CONFIG[UserLevel.NORMAL];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据等级值获取显示名称
|
||||||
|
* @param level 等级值
|
||||||
|
* @returns 显示名称
|
||||||
|
*/
|
||||||
|
export function getUserLevelName(level?: number): string {
|
||||||
|
const validLevel = level ?? 0;
|
||||||
|
return USER_LEVEL_DISPLAY_NAMES[validLevel as UserLevel] || USER_LEVEL_DISPLAY_NAMES[UserLevel.NORMAL];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否是合伙人(包括总店和分店)
|
||||||
|
* @param level 等级值
|
||||||
|
* @returns 是否是合伙人
|
||||||
|
*/
|
||||||
|
export function isPartner(level?: number): boolean {
|
||||||
|
return level === UserLevel.PARTNER_HEAD || level === UserLevel.PARTNER_BRANCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否是管理员(包括超级管理员)
|
||||||
|
* @param level 等级值
|
||||||
|
* @returns 是否是管理员
|
||||||
|
*/
|
||||||
|
export function isAdmin(level?: number): boolean {
|
||||||
|
return level === UserLevel.SUPER_ADMIN || level === UserLevel.ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出默认配置
|
||||||
|
export default USER_LEVEL_CONFIG;
|
||||||
Reference in New Issue
Block a user