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:
2026-04-10 01:48:22 +08:00
parent 12917a4766
commit e3181c8ade
16 changed files with 1314 additions and 88 deletions

View File

@@ -11,7 +11,62 @@
"usedAt": 1775132175089,
"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
}

View 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
### 微信后台需要申请的模板
模板名称:订单配送通知
关键词:订单状态、订单编号、配送地址、商品数量、通知时间

View File

@@ -16,6 +16,21 @@ export async function pageGltTicketOrder(params: GltTicketOrderParam) {
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 };
}
/**
* 查询送水订单列表
*/

View File

@@ -38,6 +38,8 @@ export interface ShopDealerUser {
createTime?: string;
// 修改时间
updateTime?: string;
// 分销商等级0-普通用户 1-超级管理员 2-合伙人(总店) 3-合伙人(分店)
dealerLevel?: number;
}
/**

View File

@@ -128,6 +128,10 @@ export interface ShopGoods {
expiredDay?: number;
// 可购买数量
canBuyNumber?: number;
// 活动方式0全平台 1新用户专享
activityType?: number;
// 配送方式0送上门 1限自提
deliveryMode?: number;
}
export interface BathSet {

View File

@@ -201,6 +201,10 @@ export interface OrderCreateRequest {
selfTakeMerchantId?: number;
// 订单标题(可选,后端会自动生成)
title?: string;
// 配送方式elevator(电梯) / stairs(步梯) / groundFloor(一楼商铺/其他)
deliveryMethod?: string;
// 楼层(步梯+送上楼时有值从2开始
deliveryFloor?: number;
}
/**

View 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

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useState } from 'react'
import {View, Text} from '@tarojs/components'
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
import {
@@ -14,6 +14,7 @@ import {useUser} from '@/hooks/useUser'
import { useThemeStyles } from '@/hooks/useTheme'
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
import {updateShopDealerUser} from '@/api/shop/shopDealerUser'
import FreezeMoneyModal from './components/FreezeMoneyModal'
import Taro from '@tarojs/taro'
const DealerIndex: React.FC = () => {
@@ -23,6 +24,9 @@ const DealerIndex: React.FC = () => {
refresh,
} = useDealerUser()
// 待使用明细弹窗显示状态
const [freezeMoneyModalVisible, setFreezeMoneyModalVisible] = useState(false)
// 获取用户角色信息
const { hasRole } = useUser()
@@ -63,8 +67,21 @@ const DealerIndex: React.FC = () => {
// 判断是否是配送员
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) {
return
@@ -76,6 +93,9 @@ const DealerIndex: React.FC = () => {
return
}
// 关闭弹窗
setFreezeMoneyModalVisible(false)
// 弹出确认框
Taro.showModal({
title: '确认操作',
@@ -202,15 +222,15 @@ const DealerIndex: React.FC = () => {
className="text-center p-3 rounded-lg flex flex-col"
style={{
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">
{formatMoney(dealerUser.freezeMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>
{isRider && Number(dealerUser.freezeMoney ?? 0) > 0 ? '待使用' : '待使用'}
使
</Text>
</View>
<View className="text-center p-3 rounded-lg flex flex-col" style={{
@@ -351,6 +371,13 @@ const DealerIndex: React.FC = () => {
</View>
</View>
{/* 待使用明细弹窗 */}
<FreezeMoneyModal
visible={freezeMoneyModalVisible}
amount={dealerUser?.freezeMoney || '0'}
onClose={handleCloseFreezeMoneyModal}
/>
{/* 底部安全区域 */}
<View className="h-20"></View>
</View>

View 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

View File

@@ -14,6 +14,7 @@ import {useThemeStyles} from "@/hooks/useTheme";
import {getRootDomain} from "@/utils/domain";
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
import { saveStorageByLoginUser } from '@/utils/server'
import { getUserLevelName, getUserLevelConfig } from '@/utils/userLevel'
const UserCard = forwardRef<any, any>((_, ref) => {
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 ? '用户' : '点击登录')
}
// 角色名称:优先取用户 roles 数组的第一个角色名称
// 角色名称:优先使用 dealerLevel 显示四种分级,否则取用户 roles 数组的第一个角色名称
const getRoleName = () => {
const dealerLevel = (userInfo as any)?.dealerLevel
if (dealerLevel !== undefined && dealerLevel !== null) {
return getUserLevelName(dealerLevel)
}
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) => {
await refresh()
@@ -267,7 +283,22 @@ const UserCard = forwardRef<any, any>((_, ref) => {
<View className={'flex flex-col'}>
<Text style={{color: '#ffffff'}}>{getDisplayName() || '点击登录'}</Text>
{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 File

@@ -1,6 +1,6 @@
import React from 'react'
import React, { useEffect } from 'react'
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 {
User,
Shopping,
@@ -8,11 +8,13 @@ import {
ArrowRight,
Purse,
People,
Scan
Scan,
Setting
} from '@nutui/icons-react-taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {useUser} from '@/hooks/useUser'
import { useThemeStyles } from '@/hooks/useTheme'
import { useRiderNotification } from '@/hooks/useRiderNotification'
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
import {updateShopDealerUser} from '@/api/shop/shopDealerUser'
import Taro from '@tarojs/taro'
@@ -27,6 +29,15 @@ const DealerIndex: React.FC = () => {
// 获取用户角色信息
const { hasRole } = useUser()
// 配送员通知功能
const { pendingCount, startPolling, stopPolling, soundEnabled, toggleSound } = useRiderNotification()
// 页面生命周期管理
useEffect(() => {
startPolling()
return () => stopPolling()
}, [startPolling, stopPolling])
// 使用主题样式
const themeStyles = useThemeStyles()
@@ -64,6 +75,55 @@ const DealerIndex: React.FC = () => {
// 判断是否是配送员
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 () => {
// 检查是否是配送员
@@ -284,8 +344,15 @@ const DealerIndex: React.FC = () => {
>
<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">
<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"/>
{pendingCount > 0 && (
<Badge
value={pendingCount > 99 ? '99+' : pendingCount}
max={99}
style={{ position: 'absolute', top: '-4px', right: '-4px' }}
/>
)}
</View>
</View>
</Grid.Item>
@@ -323,46 +390,96 @@ const DealerIndex: React.FC = () => {
</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
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={() => {
const isSubscribed = Taro.getStorageSync('rider_subscribed') === '1'
Taro.showModal({
title: '通知设置',
content: `声音提醒:${soundEnabled ? '已开启' : '已关闭'}\n订阅消息${isSubscribed ? '已订阅' : '未订阅'}`,
confirmText: '更多设置',
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={''}>*/}
{/* <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.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>*/}
<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 File

@@ -377,6 +377,17 @@ const GoodsDetail = () => {
<View className={"car-no text-lg"}>
{goods.name}
</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"}>
<span className={"text-orange-500"}>
{goods.comments}

View File

@@ -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{
width: 100%;
border-radius: 12rpx 12rpx 0 0;

View File

@@ -11,7 +11,7 @@ import {
InputNumber,
ConfigProvider
} 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 {ShopGoods} from "@/api/shop/shopGoods/model";
import {getShopGoods} from "@/api/shop/shopGoods";
@@ -75,12 +75,21 @@ const OrderConfirm = () => {
const [availableCoupons, setAvailableCoupons] = useState<CouponCardProps[]>([])
const [couponLoading, setCouponLoading] = useState<boolean>(false)
// 门店选择:用于在下单页展示当前已选门店,并允许用户切换(写入 SelectedStore Storage
// 门店选择:用于在下单页展示当前"已选门店",并允许用户切换(写入 SelectedStore Storage
const [storePopupVisible, setStorePopupVisible] = useState(false)
const [stores, setStores] = useState<ShopStore[]>([])
const [storeLoading, setStoreLoading] = useState(false)
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 params = router?.params || ({} as Record<string, any>)
const goodsIdParam = params?.goodsId
@@ -213,11 +222,20 @@ const OrderConfirm = () => {
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 total = getGoodsTotal()
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;
}
// 配送方式校验(仅送货上门模式需要选择)
if (goods.deliveryMode !== 1 && !deliveryMethod) {
Taro.showToast({
title: '请选择配送方式',
icon: 'error'
})
return;
}
// 步梯场景:必须选择是否送上楼
if (deliveryMethod === 'stairs' && needCarryUpstairs === null) {
Taro.showToast({
title: '请选择是否需要送上楼',
icon: 'error'
})
return;
}
if (!payment) {
Taro.showToast({
title: '请选择支付方式',
@@ -551,11 +587,13 @@ const OrderConfirm = () => {
address.id,
{
comments: goods.name,
deliveryType: 0,
deliveryType: goods.deliveryMode === 1 ? 1 : 0,
buyerRemarks: orderRemark,
couponId: parseInt(String(bestCoupon.id), 10),
skuId: resolvedSkuId,
specInfo: orderDataParam?.specInfo
specInfo: orderDataParam?.specInfo,
deliveryMethod: deliveryMethod || undefined,
deliveryFloor: needCarryUpstairs ? deliveryFloor : undefined
}
);
@@ -603,12 +641,14 @@ const OrderConfirm = () => {
address.id,
{
comments: '桂乐淘',
deliveryType: 0,
deliveryType: goods.deliveryMode === 1 ? 1 : 0,
buyerRemarks: orderRemark,
// 🔧 确保 couponId 是正确的数字类型,且不传递 undefined
couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined,
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('围栏')
// 配送范围类错误给出更友好的解释,并提供快捷入口去更换收货地址
// "配送范围"类错误给出更友好的解释,并提供快捷入口去更换收货地址
if (isOutOfDeliveryRange) {
try {
const res = await Taro.showModal({
@@ -827,7 +867,7 @@ const OrderConfirm = () => {
setPayment(paymentRes[0])
}
// 加载优惠券:使用初始数量对应的总价做推荐,避免默认数量变化导致推荐不准
// 加载优惠券:使用"初始数量"对应的总价做推荐,避免默认数量变化导致推荐不准
if (goodsRes) {
const initQty = (() => {
const n = Number(goodsRes?.canBuyNumber)
@@ -893,36 +933,52 @@ const OrderConfirm = () => {
return (
<div className={'order-confirm-page'}>
<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'})}>
{goods.deliveryMode === 1 ? (
// 自提模式:显示到店自提提示
<CellGroup>
<Cell>
<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>
</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>*/}
{/* <Cell*/}
@@ -944,6 +1000,88 @@ const OrderConfirm = () => {
{/* />*/}
{/*</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>
<Cell key={goods.goodsId}>
<View className={'flex w-full justify-between gap-3'}>
@@ -1042,7 +1180,14 @@ const OrderConfirm = () => {
)}
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={(
<View className={'flex items-end gap-2'}>
<Text></Text>
@@ -1216,6 +1361,49 @@ const OrderConfirm = () => {
</View>
</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}/>
<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)}
</View>
)}
{getDeliveryFee() > 0 && (
<View className={'text-xs text-orange-500'}>
{getDeliveryFee().toFixed(2)}
</View>
)}
</div>
<div className={'buy-btn mx-4'}>
<Button

View File

@@ -476,6 +476,8 @@ export function buildSingleGoodsOrder(
specInfo?: string;
buyerRemarks?: string;
sendStartTime?: string;
deliveryMethod?: string;
deliveryFloor?: number;
}
): OrderCreateRequest {
return {
@@ -493,7 +495,9 @@ export function buildSingleGoodsOrder(
sendStartTime: options?.sendStartTime,
deliveryType: options?.deliveryType || 0,
couponId: options?.couponId,
selfTakeMerchantId: options?.selfTakeMerchantId
selfTakeMerchantId: options?.selfTakeMerchantId,
deliveryMethod: options?.deliveryMethod,
deliveryFloor: options?.deliveryFloor
};
}

115
src/utils/userLevel.ts Normal file
View 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;