feat(registration): 优化经销商注册流程并增加地址定位功能

- 修改导航栏标题从“邀请注册”为“注册成为会员”
- 修复重复提交问题并移除不必要的submitting状态
- 增加昵称和头像的必填验证提示
- 添加用户角色缺失时的默认角色写入机制
- 集成地图选点功能,支持经纬度获取和地址解析
- 实现微信地址导入功能,自动填充基本信息
- 增加定位权限检查和错误处理机制
- 添加.gitignore规则忽略备份文件夹src__bak
- 移除已废弃的银行卡和客户管理页面代码
- 优化表单验证规则和错误提示信息
- 实现经销商注册成功后自动跳转到“我的”页面
- 添加用户信息缓存刷新机制确保角色信息同步
```
This commit is contained in:
2026-03-01 12:35:41 +08:00
parent 945351be91
commit eee4644d06
296 changed files with 28845 additions and 6664 deletions

View File

@@ -0,0 +1,255 @@
import {Button} from '@nutui/nutui-react-taro'
import {Avatar, Tag} from '@nutui/nutui-react-taro'
import {getUserInfo, getWxOpenId} from '@/api/layout';
import Taro from '@tarojs/taro';
import {useEffect, useState} from "react";
import {User} from "@/api/system/user/model";
import navTo from "@/utils/common";
import {TenantId} from "@/config/app";
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
import {useUser} from "@/hooks/useUser";
import {getStoredInviteParams} from "@/utils/invite";
function UserCard() {
const {getDisplayName, getRoleName} = useUser();
const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>()
const [couponCount, setCouponCount] = useState(0)
// const [pointsCount, setPointsCount] = useState(0)
const [giftCount, setGiftCount] = useState(0)
useEffect(() => {
// Taro.getSetting获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
success: (res) => {
if (res.authSetting['scope.userInfo']) {
// 用户已经授权过,可以直接获取用户信息
console.log('用户已经授权过,可以直接获取用户信息')
reload();
} else {
// 用户未授权,需要弹出授权窗口
console.log('用户未授权,需要弹出授权窗口')
showAuthModal();
}
}
});
}, []);
const loadUserStats = (userId: number) => {
// 加载优惠券数量
getMyAvailableCoupons()
.then((coupons: any) => {
setCouponCount(coupons?.length || 0)
})
.catch((error: any) => {
console.error('Coupon count error:', error)
})
// 加载积分数量
console.log(userId)
// getUserPointsStats(userId)
// .then((res: any) => {
// setPointsCount(res.currentPoints || 0)
// })
// .catch((error: any) => {
// console.error('Points stats error:', error)
// })
// 加载礼品劵数量
setGiftCount(0)
// pageUserGiftLog({userId, page: 1, limit: 1}).then(res => {
// setGiftCount(res.count || 0)
// })
}
const reload = () => {
Taro.getUserInfo({
success: (res) => {
const avatar = res.userInfo.avatarUrl;
setUserInfo({
avatar,
nickname: res.userInfo.nickName,
sexName: res.userInfo.gender == 1 ? '男' : '女'
})
getUserInfo().then((data) => {
if (data) {
setUserInfo(data)
setIsLogin(true);
Taro.setStorageSync('UserId', data.userId)
// 加载用户统计数据
if (data.userId) {
loadUserStats(data.userId)
}
// 获取openId
if (!data.openid) {
Taro.login({
success: (res) => {
getWxOpenId({code: res.code}).then(() => {
})
}
})
}
}
}).catch(() => {
console.log('未登录')
});
}
});
};
const showAuthModal = () => {
Taro.showModal({
title: '授权提示',
content: '需要获取您的用户信息',
confirmText: '去授权',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 用户点击确认,打开授权设置页面
openSetting();
}
}
});
};
const openSetting = () => {
// Taro.openSetting调起客户端小程序设置界面返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
Taro.openSetting({
success: (res) => {
if (res.authSetting['scope.userInfo']) {
// 用户授权成功,可以获取用户信息
reload();
} else {
// 用户拒绝授权,提示授权失败
Taro.showToast({
title: '授权失败',
icon: 'none'
});
}
}
});
};
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
const {code, encryptedData, iv} = detail
// 获取存储的邀请参数
const inviteParams = getStoredInviteParams()
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
Taro.login({
success: function () {
if (code) {
Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
code,
encryptedData,
iv,
notVerifyPhone: true,
refereeId: refereeId, // 使用解析出的推荐人ID
sceneType: 'save_referee',
tenantId: TenantId
},
header: {
'content-type': 'application/json',
TenantId
},
success: function (res) {
if (res.data.code == 1) {
Taro.showToast({
title: res.data.message,
icon: 'error',
duration: 2000
})
return false;
}
// 登录成功
Taro.setStorageSync('access_token', res.data.data.access_token)
Taro.setStorageSync('UserId', res.data.data.user.userId)
setUserInfo(res.data.data.user)
setIsLogin(true)
}
})
} else {
console.log('登录失败!')
}
}
})
}
return (
<div className={'header-bg pt-20'}>
<div className={'p-4'}>
<div
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
style={{
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
// width: '720rpx',
// margin: '10px auto 0px auto',
height: '170px',
// borderRadius: '22px 22px 0 0',
}}
>
<div className={'user-card-header flex w-full justify-between items-center pt-4'}>
<div className={'flex items-center mx-4'}>
{
IsLogin ? (
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
) : (
<Button className={'text-black'} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
</Button>
)
}
<div className={'user-info flex flex-col px-2'}>
<div className={'py-1 text-black font-bold'}>{getDisplayName()}</div>
{IsLogin ? (
<div className={'grade text-xs py-1'}>
<Tag type="success" round>
<div className={'p-1'}>
{getRoleName()}
</div>
</Tag>
</div>
) : ''}
</div>
</div>
<div className={'mx-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
onClick={() => navTo('/user/profile/profile', true)}>
{'个人资料'}
</div>
</div>
<div className={'flex justify-around mt-1'}>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/wallet/wallet', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>¥ {userInfo?.balance || '0.00'}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/coupon/index', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{couponCount}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/gift/index', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{giftCount}</span>
</div>
{/*<div className={'item flex justify-center flex-col items-center'}>*/}
{/* <span className={'text-sm text-gray-500'}>积分</span>*/}
{/* <span className={'text-xl'}>{pointsCount}</span>*/}
{/*</div>*/}
</div>
</div>
</div>
</div>
)
}
export default UserCard;

View File

@@ -0,0 +1,186 @@
import {Cell} from '@nutui/nutui-react-taro'
import navTo from "@/utils/common";
import Taro from '@tarojs/taro'
import {View, Text} from '@tarojs/components'
import {ArrowRight, ShieldCheck, LogisticsError, Location, Reward, Tips, Ask, Setting, Scan} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
const UserCell = () => {
const {logoutUser, isCertified, hasRole, isAdmin} = useUser();
const onLogout = () => {
Taro.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: function (res) {
if (res.confirm) {
// 使用 useUser hook 的 logoutUser 方法
logoutUser();
Taro.reLaunch({
url: '/pages/index/index'
})
}
}
})
}
return (
<>
<View className={'px-4'}>
{/*是否分销商*/}
{!hasRole('dealer') && !isAdmin() && (
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/dealer/index', true)}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
<Text className={'text-white opacity-80 pl-3'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
)}
{/*是否管理员*/}
{isAdmin() && (
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #ff8e0c, #ed680d)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/admin/article/index', true)}>
<Setting className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
)}
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}></Text>
</View>
}>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Scan size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/wallet/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
style={{
display: 'none'
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<LogisticsError size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/wallet/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Location size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/address/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<ShieldCheck size={16} color={isCertified() ? '#52c41a' : '#666'}/>
<Text className={'pl-3 text-sm'}></Text>
{isCertified() && (
<Text className={'pl-2 text-xs text-green-500'}></Text>
)}
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/userVerify/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Ask size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/help/index')
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Tips size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/about/index')
}}
/>
</Cell.Group>
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}></Text>
</View>
}>
<Cell
className="nutui-cell-clickable"
title="账号安全"
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/user/profile/profile', true)}
/>
<Cell
className="nutui-cell-clickable"
title="退出登录"
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={onLogout}
/>
</Cell.Group>
</View>
</>
)
}
export default UserCell

View File

@@ -0,0 +1,102 @@
import {loginBySms} from "@/api/passport/login";
import {useState} from "react";
import Taro from '@tarojs/taro'
import {Popup} from '@nutui/nutui-react-taro'
import {UserParam} from "@/api/system/user/model";
import {Button} from '@nutui/nutui-react-taro'
import {Form, Input} from '@nutui/nutui-react-taro'
import {Copyright, Version} from "@/config/app";
const UserFooter = () => {
const [openLoginByPhone, setOpenLoginByPhone] = useState(false)
const [clickNum, setClickNum] = useState<number>(0)
const [FormData, setFormData] = useState<UserParam>(
{
phone: undefined,
password: undefined
}
)
const onLoginByPhone = () => {
setFormData({})
setClickNum(clickNum + 1);
if (clickNum > 10) {
setOpenLoginByPhone(true);
}
}
const closeLoginByPhone = () => {
setClickNum(0)
setOpenLoginByPhone(false)
}
// 提交表单
const submitByPhone = (values: any) => {
loginBySms({
phone: values.phone,
code: values.code
}).then(() => {
setOpenLoginByPhone(false);
setTimeout(() => {
Taro.reLaunch({
url: '/pages/index/index'
})
},1000)
})
}
return (
<>
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
<div className={'text-xs text-gray-400 py-1'}>{Version}</div>
<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>
</div>
<Popup
style={{width: '350px', padding: '10px'}}
visible={openLoginByPhone}
closeOnOverlayClick={false}
closeable={true}
onClose={closeLoginByPhone}
>
<Form
style={{width: '350px',padding: '10px'}}
divider
initialValues={FormData}
labelPosition="left"
onFinish={(values) => submitByPhone(values)}
footer={
<div
style={{
display: 'flex',
justifyContent: 'center',
width: '100%'
}}
>
<Button nativeType="submit" block style={{backgroundColor: '#000000',color: '#ffffff'}}>
</Button>
</div>
}
>
<Form.Item
label={'手机号码'}
name="phone"
required
rules={[{message: '手机号码'}]}
>
<Input placeholder="请输入手机号码" maxLength={11} type="text"/>
</Form.Item>
<Form.Item
label={'短信验证码'}
name="code"
required
rules={[{message: '短信验证码'}]}
>
<Input placeholder="请输入短信验证码" maxLength={6} type="text"/>
</Form.Item>
</Form>
</Popup>
</>
)
}
export default UserFooter

View File

@@ -0,0 +1,69 @@
import {useEffect} from "react";
import navTo from "@/utils/common";
import {View, Text} from '@tarojs/components';
import {ArrowRight, Wallet, Comment, Transit, Refund, Package} from '@nutui/icons-react-taro';
function UserOrder() {
const reload = () => {
};
useEffect(() => {
reload()
}, []);
return (
<>
<View className={'px-4 pb-2'}>
<View
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
style={{
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
// margin: '10px auto 0px auto',
height: '120px',
// borderRadius: '22px 22px 0 0',
}}
>
<View className={'title-bar flex justify-between pt-2'}>
<Text className={'title font-medium px-4'}></Text>
<View className={'more flex items-center px-2'} onClick={() => navTo('/user/order/order', true)}>
<Text className={'text-xs text-gray-500'}></Text>
<ArrowRight color="#cccccc" size={12}/>
</View>
</View>
<View className={'flex justify-around pb-1'}>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=0', true)}>
<Wallet size={26} className={'font-normal text-gray-500'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=1', true)}>
<Package size={26} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=3', true)}>
<Transit size={24} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=5', true)}>
<Comment size={24} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=6', true)}>
<Refund size={26} className={'font-normal text-gray-500'}/>
<Text className={'text-sm text-gray-600 py-1'}>退/</Text>
</View>
</View>
</View>
</View>
</>
)
}
export default UserOrder;

View File

@@ -1,295 +1,35 @@
import React from 'react'
import {View, Text} from '@tarojs/components'
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
import {
User,
Shopping,
Dongdong,
ArrowRight,
Purse,
People
} from '@nutui/icons-react-taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import { useThemeStyles } from '@/hooks/useTheme'
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
import Taro from '@tarojs/taro'
import {useEffect} from 'react'
import {useUser} from "@/hooks/useUser";
import {Empty} from '@nutui/nutui-react-taro';
import {Text} from '@tarojs/components';
const DealerIndex: React.FC = () => {
function Admin() {
const {
dealerUser,
error,
refresh,
} = useDealerUser()
isAdmin
} = useUser();
// 使用主题样式
const themeStyles = useThemeStyles()
useEffect(() => {
}, []);
// 导航到各个功能页面
const navigateToPage = (url: string) => {
Taro.navigateTo({url})
}
// 格式化金额
const formatMoney = (money?: string) => {
if (!money) return '0.00'
return parseFloat(money).toFixed(2)
}
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-'
return new Date(time).toLocaleDateString()
}
// 获取用户主题
const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId)
// 获取渐变背景
const getGradientBackground = (themeColor?: string) => {
if (themeColor) {
const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30)
return gradientUtils.createGradient(themeColor, darkerColor)
}
return userTheme.background
}
console.log(getGradientBackground(),'getGradientBackground()')
if (error) {
if (!isAdmin()) {
return (
<View className="p-4">
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
<Text className="text-red-600">{error}</Text>
</View>
<Button type="primary" onClick={refresh}>
</Button>
</View>
)
<Empty
description="您不是管理员"
imageSize={80}
style={{
backgroundColor: 'transparent',
height: 'calc(100vh - 200px)'
}}
>
</Empty>
);
}
return (
<View className="bg-gray-100 min-h-screen">
<View>
{/*头部信息*/}
{dealerUser && (
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
{/* 装饰性背景元素 - 小程序兼容版本 */}
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="absolute w-24 h-24 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.08)',
bottom: '-12px',
left: '-12px'
}}></View>
<View className="absolute w-16 h-16 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
top: '60px',
left: '120px'
}}></View>
<View className="flex items-center justify-between relative z-10 mb-4">
<Avatar
size="50"
src={dealerUser?.qrcode}
icon={<User/>}
className="mr-4"
style={{
border: '2px solid rgba(255, 255, 255, 0.3)'
}}
/>
<View className="flex-1 flex-col">
<View className="text-white text-lg font-bold mb-1" style={{
}}>
{dealerUser?.realName || '分销商'}
</View>
<View className="text-sm" style={{
color: 'rgba(255, 255, 255, 0.8)'
}}>
ID: {dealerUser.userId} | : {dealerUser.refereeId || '无'}
</View>
</View>
<View className="text-right hidden">
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.9)'
}}></Text>
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.7)'
}}>
{formatTime(dealerUser.createTime)}
</Text>
</View>
</View>
</View>
)}
{/* 佣金统计卡片 */}
{dealerUser && (
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
<View className="mb-4">
<Text className="font-semibold text-gray-800"></Text>
</View>
<View className="grid grid-cols-3 gap-3">
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.available
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.money)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.frozen
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.freezeMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.total
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.totalMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
</View>
</View>
)}
{/* 团队统计 */}
{dealerUser && (
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
<View className="flex items-center justify-between mb-4">
<Text className="font-semibold text-gray-800"></Text>
<View
className="text-gray-400 text-sm flex items-center"
onClick={() => navigateToPage('/dealer/team/index')}
>
<Text></Text>
<ArrowRight size="12"/>
</View>
</View>
<View className="grid grid-cols-3 gap-4">
<View className="text-center grid">
<Text className="text-xl font-bold text-purple-500 mb-1">
{dealerUser.firstNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-indigo-500 mb-1">
{dealerUser.secondNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-pink-500 mb-1">
{dealerUser.thirdNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
</View>
</View>
)}
{/* 功能导航 */}
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<View className="font-semibold mb-4 text-gray-800"></View>
<ConfigProvider>
<Grid
columns={4}
className="no-border-grid"
style={{
'--nutui-grid-border-color': 'transparent',
'--nutui-grid-item-border-width': '0px',
border: 'none'
} as React.CSSProperties}
>
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shopping color="#3b82f6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Purse color="#10b981" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<People color="#8b5cf6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Dongdong color="#f59e0b" size="20"/>
</View>
</View>
</Grid.Item>
</Grid>
{/* 第二行功能 */}
{/*<Grid*/}
{/* columns={4}*/}
{/* className="no-border-grid mt-4"*/}
{/* style={{*/}
{/* '--nutui-grid-border-color': 'transparent',*/}
{/* '--nutui-grid-item-border-width': '0px',*/}
{/* border: 'none'*/}
{/* } as React.CSSProperties}*/}
{/*>*/}
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Presentation color="#6366f1" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* /!* 预留其他功能位置 *!/*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/*</Grid>*/}
</ConfigProvider>
</View>
</View>
{/* 底部安全区域 */}
<View className="h-20"></View>
</View>
<>
<Text>...</Text>
</>
)
}
export default DealerIndex
export default Admin

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '实名审核'
})

View File

@@ -1,319 +0,0 @@
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {
Space,
Tabs,
Tag,
Empty,
Loading,
PullToRefresh,
Button,
Dialog,
Image,
ImagePreview,
TextArea
} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {pageUserVerify, updateUserVerify} from '@/api/system/userVerify'
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
import {UserVerify} from "@/api/system/userVerify/model";
const UserVeirfyAdmin: React.FC = () => {
const [activeTab, setActiveTab] = useState<string | number>(0)
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [list, setList] = useState<UserVerify[]>([])
const [rejectDialogVisible, setRejectDialogVisible] = useState<boolean>(false)
const [rejectReason, setRejectReason] = useState<string>('')
const [currentRecord, setCurrentRecord] = useState<ShopDealerWithdraw | null>(null)
const [showPreview, setShowPreview] = useState(false)
const [showPreview2, setShowPreview2] = useState(false)
const {dealerUser} = useDealerUser()
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换到:', value)
setActiveTab(value)
// activeTab变化会自动触发useEffect重新获取数据无需手动调用
}
// 获取审核记录
const fetchWithdrawRecords = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
const currentStatus = Number(activeTab)
const result = await pageUserVerify({
page: 1,
limit: 100,
status: currentStatus // 后端筛选,提高性能
})
if (result?.list) {
const processedRecords = result.list.map(record => ({
...record
}))
setList(processedRecords)
}
} catch (error) {
console.error('获取审核记录失败:', error)
Taro.showToast({
title: '获取审核记录失败',
icon: 'none'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId, activeTab])
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
await Promise.all([fetchWithdrawRecords()])
setRefreshing(false)
}
// 审核通过
const handleApprove = async (record: ShopDealerWithdraw) => {
try {
await updateUserVerify({
...record,
status: 1, // 审核通过
})
Taro.showToast({
title: '审核通过',
icon: 'success'
})
await fetchWithdrawRecords()
} catch (error: any) {
if (error !== 'cancel') {
console.error('审核通过失败:', error)
Taro.showToast({
title: error.message || '操作失败',
icon: 'none'
})
}
}
}
// 驳回申请
const handleReject = (record: ShopDealerWithdraw) => {
setCurrentRecord(record)
setRejectReason('')
setRejectDialogVisible(true)
}
// 确认驳回
const confirmReject = async () => {
if (!rejectReason.trim()) {
Taro.showToast({
title: '请输入驳回原因',
icon: 'none'
})
return
}
try {
await updateUserVerify({
...currentRecord!,
status: 2, // 驳回
comments: rejectReason.trim()
})
Taro.showToast({
title: '已驳回',
icon: 'success'
})
setRejectDialogVisible(false)
setCurrentRecord(null)
setRejectReason('')
await fetchWithdrawRecords()
} catch (error: any) {
console.error('驳回失败:', error)
Taro.showToast({
title: error.message || '操作失败',
icon: 'none'
})
}
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchWithdrawRecords().then()
}
}, [fetchWithdrawRecords])
const getStatusText = (status?: number) => {
switch (status) {
case 0:
return '待审核'
case 1:
return '审核通过'
case 2:
return '已驳回'
default:
return '未知'
}
}
const getStatusColor = (status?: number) => {
switch (status) {
case 0:
return 'warning'
case 1:
return 'success'
case 2:
return 'danger'
default:
return 'default'
}
}
const renderWithdrawRecords = () => {
console.log('渲染审核记录:', {loading, recordsCount: list.length, dealerUserId: dealerUser?.userId})
return (
<PullToRefresh
disabled={refreshing}
onRefresh={handleRefresh}
>
<View>
{loading ? (
<View className="text-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : list.length > 0 ? (
list.map(record => (
<View key={record.id} className="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<Space direction={'vertical'}>
<Text className="font-semibold text-gray-800">
{record.realName}
</Text>
<Text className="font-normal text-sm text-gray-500">
{record.phone}
</Text>
<Text className="font-normal text-sm text-gray-500">
{record.idCard}
</Text>
</Space>
<Tag type={getStatusColor(record.status)}>
{getStatusText(record.status)}
</Tag>
</View>
<View className="flex gap-2 mb-2">
<Image src={record.sfz1} height={100} onClick={() => setShowPreview(true)}/>
<Image src={record.sfz2} height={100} onClick={() => setShowPreview2(true)}/>
</View>
<ImagePreview
autoPlay
images={[{src: `${record.sfz1}`}]}
visible={showPreview}
onClose={() => setShowPreview(false)}
/>
<ImagePreview
autoPlay
images={[{src: `${record.sfz1}`}]}
visible={showPreview2}
onClose={() => setShowPreview2(false)}
/>
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.status == 1 && (
<Text className="block mt-1">
{record.updateTime}
</Text>
)}
{record.status == 2 && (
<Text className="block mt-1 text-red-500">
{record.comments}
</Text>
)}
</View>
{/* 操作按钮 */}
{record.status === 0 && (
<View className="flex gap-2 mt-3">
<Button
type="success"
size="small"
className="flex-1"
onClick={() => handleApprove(record)}
>
</Button>
<Button
type="danger"
size="small"
className="flex-1"
onClick={() => handleReject(record)}
>
</Button>
</View>
)}
</View>
))
) : (
<Empty description="暂无申请记录"/>
)}
</View>
</PullToRefresh>
)
}
return (
<View className="bg-gray-50 min-h-screen">
<Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.TabPane title="待审核" value="0">
{renderWithdrawRecords()}
</Tabs.TabPane>
<Tabs.TabPane title="已通过" value="1">
{renderWithdrawRecords()}
</Tabs.TabPane>
<Tabs.TabPane title="已驳回" value="2">
{renderWithdrawRecords()}
</Tabs.TabPane>
</Tabs>
{/* 驳回原因对话框 */}
<Dialog
visible={rejectDialogVisible}
title="驳回原因"
onCancel={() => {
setRejectDialogVisible(false)
setCurrentRecord(null)
setRejectReason('')
}}
onConfirm={confirmReject}
>
<View className="p-4">
<TextArea
placeholder="请输入驳回原因"
value={rejectReason}
onChange={(value) => setRejectReason(value)}
maxLength={200}
rows={4}
/>
</View>
</Dialog>
</View>
)
}
export default UserVeirfyAdmin

320
src/api/afterSale.ts Normal file
View File

@@ -0,0 +1,320 @@
import { request } from '../utils/request'
// 售后类型
export type AfterSaleType = 'refund' | 'return' | 'exchange' | 'repair'
// 售后状态
export type AfterSaleStatus =
| 'pending' // 待审核
| 'approved' // 已同意
| 'rejected' // 已拒绝
| 'processing' // 处理中
| 'completed' // 已完成
| 'cancelled' // 已取消
// 售后进度记录
export interface ProgressRecord {
id: string
time: string
status: string
description: string
operator?: string
remark?: string
images?: string[]
}
// 售后详情
export interface AfterSaleDetail {
id: string
orderId: string
orderNo: string
type: AfterSaleType
status: AfterSaleStatus
reason: string
description: string
amount: number
applyTime: string
processTime?: string
completeTime?: string
rejectReason?: string
contactPhone?: string
evidenceImages: string[]
progressRecords: ProgressRecord[]
}
// 售后申请参数
export interface AfterSaleApplyParams {
orderId: string
type: AfterSaleType
reason: string
description: string
amount: number
contactPhone?: string
evidenceImages?: string[]
goodsItems?: Array<{
goodsId: string
quantity: number
}>
}
// 售后列表查询参数
export interface AfterSaleListParams {
page?: number
pageSize?: number
status?: AfterSaleStatus
type?: AfterSaleType
startTime?: string
endTime?: string
}
// 售后列表响应
export interface AfterSaleListResponse {
success: boolean
data: {
list: AfterSaleDetail[]
total: number
page: number
pageSize: number
}
message?: string
}
// 售后详情响应
export interface AfterSaleDetailResponse {
success: boolean
data: AfterSaleDetail
message?: string
}
// 售后类型映射
export const AFTER_SALE_TYPE_MAP = {
'refund': '退款',
'return': '退货',
'exchange': '换货',
'repair': '维修'
}
// 售后状态映射
export const AFTER_SALE_STATUS_MAP = {
'pending': '待审核',
'approved': '已同意',
'rejected': '已拒绝',
'processing': '处理中',
'completed': '已完成',
'cancelled': '已取消'
}
// 状态颜色映射
export const STATUS_COLOR_MAP = {
'pending': '#faad14',
'approved': '#52c41a',
'rejected': '#ff4d4f',
'processing': '#1890ff',
'completed': '#52c41a',
'cancelled': '#999'
}
// 申请售后
export const applyAfterSale = async (params: AfterSaleApplyParams): Promise<AfterSaleDetailResponse> => {
try {
const response = await request<AfterSaleDetailResponse>({
url: '/api/after-sale/apply',
method: 'POST',
data: params
})
return response
} catch (error) {
console.error('申请售后失败:', error)
throw error
}
}
// 查询售后详情
export const getAfterSaleDetail = async (params: {
orderId?: string
afterSaleId?: string
}): Promise<AfterSaleDetailResponse> => {
try {
const response = await request<AfterSaleDetailResponse>({
url: '/api/after-sale/detail',
method: 'GET',
data: params
})
return response
} catch (error) {
console.error('查询售后详情失败:', error)
// 返回模拟数据作为降级方案
return getMockAfterSaleDetail(params)
}
}
// 查询售后列表
export const getAfterSaleList = async (params: AfterSaleListParams): Promise<AfterSaleListResponse> => {
try {
const response = await request<AfterSaleListResponse>({
url: '/api/after-sale/list',
method: 'GET',
data: params
})
return response
} catch (error) {
console.error('查询售后列表失败:', error)
throw error
}
}
// 撤销售后申请
export const cancelAfterSale = async (afterSaleId: string): Promise<{ success: boolean; message?: string }> => {
try {
const response = await request<{ success: boolean; message?: string }>({
url: '/api/after-sale/cancel',
method: 'POST',
data: { afterSaleId }
})
return response
} catch (error) {
console.error('撤销售后申请失败:', error)
throw error
}
}
// 获取模拟售后详情数据
const getMockAfterSaleDetail = (params: {
orderId?: string
afterSaleId?: string
}): AfterSaleDetailResponse => {
const now = new Date()
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)
const mockData: AfterSaleDetailResponse = {
success: true,
data: {
id: 'AS' + Date.now(),
orderId: params.orderId || '',
orderNo: 'ORD' + Date.now(),
type: 'refund',
status: 'processing',
reason: '商品质量问题',
description: '收到的商品有明显瑕疵,包装破损,希望申请退款处理',
amount: 9999,
applyTime: twoDaysAgo.toISOString(),
processTime: yesterday.toISOString(),
contactPhone: '138****5678',
evidenceImages: [
'https://via.placeholder.com/200x200?text=Evidence1',
'https://via.placeholder.com/200x200?text=Evidence2'
],
progressRecords: [
{
id: '1',
time: now.toISOString(),
status: '处理中',
description: '客服正在处理您的申请,请耐心等待',
operator: '客服小王',
remark: '预计1-2个工作日内完成处理'
},
{
id: '2',
time: new Date(now.getTime() - 4 * 60 * 60 * 1000).toISOString(),
status: '已审核',
description: '您的申请已通过审核,正在安排退款处理',
operator: '审核员张三'
},
{
id: '3',
time: yesterday.toISOString(),
status: '已受理',
description: '我们已收到您的申请,正在进行审核',
operator: '系统'
},
{
id: '4',
time: twoDaysAgo.toISOString(),
status: '已提交',
description: '您已成功提交售后申请',
operator: '用户'
}
]
}
}
return mockData
}
// 格式化售后状态
export const formatAfterSaleStatus = (status: AfterSaleStatus): {
text: string
color: string
icon: string
} => {
const statusMap = {
'pending': { text: '待审核', color: '#faad14', icon: '⏳' },
'approved': { text: '已同意', color: '#52c41a', icon: '✅' },
'rejected': { text: '已拒绝', color: '#ff4d4f', icon: '❌' },
'processing': { text: '处理中', color: '#1890ff', icon: '🔄' },
'completed': { text: '已完成', color: '#52c41a', icon: '✅' },
'cancelled': { text: '已取消', color: '#999', icon: '⭕' }
}
return statusMap[status] || { text: status, color: '#666', icon: '📋' }
}
// 计算预计处理时间
export const calculateEstimatedTime = (
applyTime: string,
type: AfterSaleType,
status: AfterSaleStatus
): string => {
const applyDate = new Date(applyTime)
let estimatedDays = 3 // 默认3个工作日
// 根据售后类型调整预计时间
switch (type) {
case 'refund':
estimatedDays = 3 // 退款3个工作日
break
case 'return':
estimatedDays = 7 // 退货7个工作日
break
case 'exchange':
estimatedDays = 10 // 换货10个工作日
break
case 'repair':
estimatedDays = 15 // 维修15个工作日
break
}
// 根据当前状态调整
if (status === 'completed') {
return '已完成'
} else if (status === 'rejected' || status === 'cancelled') {
return '已结束'
}
const estimatedDate = new Date(applyDate.getTime() + estimatedDays * 24 * 60 * 60 * 1000)
return `预计${estimatedDate.getMonth() + 1}${estimatedDate.getDate()}日前完成`
}
// 获取售后进度步骤
export const getAfterSaleSteps = (type: AfterSaleType, status: AfterSaleStatus) => {
const baseSteps = [
{ title: '提交申请', description: '用户提交售后申请' },
{ title: '审核中', description: '客服审核申请材料' },
{ title: '处理中', description: '正在处理您的申请' },
{ title: '完成', description: '售后处理完成' }
]
// 根据类型调整步骤
if (type === 'return' || type === 'exchange') {
baseSteps.splice(2, 0, { title: '等待收货', description: '等待用户寄回商品' })
baseSteps.splice(3, 0, { title: '确认收货', description: '商家确认收到退回商品' })
}
return baseSteps
}

View File

@@ -102,7 +102,7 @@ export async function getCmsAd(id: number) {
}
/**
* 根据id查询广告位
* 根据code查询广告位
*/
export async function getCmsAdByCode(code: string) {
const res = await request.get<ApiResult<CmsAd>>(

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api/index';
import type { PageParam } from '@/api';
/**
* 广告位
@@ -52,7 +52,6 @@ export interface CmsAd {
*/
export interface CmsAdParam extends PageParam {
adId?: number;
type?: number;
adType?: string;
pageId?: number;
pageName?: string;

View File

@@ -1,5 +1,5 @@
import request from '@/utils/request';
import type {ApiResult, PageResult} from '@/api/index';
import type {ApiResult, PageResult} from '@/api';
import type {CmsArticle, CmsArticleParam} from './model';
/**
@@ -204,3 +204,15 @@ export async function getByIds(params?: CmsArticleParam) {
return Promise.reject(new Error(res.message));
}
/**
* 根据code查询文章
*/
export async function getByCode(code: string) {
const res = await request.get<ApiResult<CmsArticle>>(
'/cms/cms-article/getByCode/' + code
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -123,3 +123,16 @@ export async function getNavigationByPath(params: CmsNavigationParam) {
}
/**
* 根据code查询导航
*/
export async function getByCode(code: string) {
const res = await request.get<ApiResult<CmsNavigation>>(
'/cms/cms-navigation/getByCode/' + code
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -55,8 +55,8 @@ export interface CmsNavigation {
parentName?: string;
// 模型名称
modelName?: string;
// 类型(已废弃)
type?: number;
// 类型(模型)
type?: string;
// 绑定的页面(已废弃)
pageId?: number;
// 项目ID
@@ -113,6 +113,5 @@ export interface CmsNavigationParam extends PageParam {
parentId?: number;
hide?: number;
model?: string;
home?: number;
keywords?: string;
}

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api';
import type { PageParam } from '@/api/index';
/**
* 应用参数
@@ -55,6 +55,9 @@ export interface Config {
email?: string;
loginTitle?: string;
sysLogo?: string;
NoticeBar?: string;
apiUrl?: string;
vipText?: string;
vipComments?: string;
deliveryText?: string;
guaranteeText?: string;
openComments?: string;
}

View File

@@ -0,0 +1,101 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
import type { GltTicketOrder, GltTicketOrderParam } from './model';
/**
* 分页查询送水订单
*/
export async function pageGltTicketOrder(params: GltTicketOrderParam) {
const res = await request.get<ApiResult<PageResult<GltTicketOrder>>>(
'/glt/glt-ticket-order/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询送水订单列表
*/
export async function listGltTicketOrder(params?: GltTicketOrderParam) {
const res = await request.get<ApiResult<GltTicketOrder[]>>(
'/glt/glt-ticket-order',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加送水订单
*/
export async function addGltTicketOrder(data: GltTicketOrder) {
const res = await request.post<ApiResult<unknown>>(
'/glt/glt-ticket-order',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改送水订单
*/
export async function updateGltTicketOrder(data: GltTicketOrder) {
const res = await request.put<ApiResult<unknown>>(
'/glt/glt-ticket-order',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除送水订单
*/
export async function removeGltTicketOrder(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-ticket-order/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除送水订单
*/
export async function removeBatchGltTicketOrder(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-ticket-order/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询送水订单
*/
export async function getGltTicketOrder(id: number) {
const res = await request.get<ApiResult<GltTicketOrder>>(
'/glt/glt-ticket-order/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,94 @@
import type { PageParam } from '@/api';
/**
* 送水订单
*/
export interface GltTicketOrder {
//
id?: number;
// 用户水票ID
userTicketId?: number;
// 订单编号
orderNo?: string;
// 门店ID
storeId?: number;
// 门店名称
storeName?: string;
// 门店地址
storeAddress?: string;
// 门店电话
storePhone?: string;
// 配送员
riderId?: number;
// 配送员名称
riderName?: string;
// 配送员电话
riderPhone?: string;
// 仓库ID
warehouseId?: number;
// 仓库名称
warehouseName?: string;
// 仓库地址
warehouseAddress?: string;
// 关联收货地址
addressId?: number;
// 收货地址
address?: string;
// 配送时间
sendTime?: string;
// 配送开始时间(配送员点击“开始配送”)
sendStartTime?: string;
// 配送结束时间(配送员确认送达)
sendEndTime?: string;
// 配送员送达拍照(选填/必填由后端策略决定)
sendEndImg?: string;
// 发货/配送状态建议10待配送 20配送中 30待客户确认 40已完成
deliveryStatus?: number;
// 客户确认收货时间(客户点击确认收货)
receiveConfirmTime?: string;
// 客户确认方式建议10客户手动确认 20配送照片自动确认 30后台超时自动确认
receiveConfirmType?: number;
// 买家留言
buyerRemarks?: string;
// 用于统计
price?: string;
// 购买数量
totalNum?: number;
// 用户ID
userId?: number;
// 昵称
nickname?: string;
// 头像
avatar?: string;
// 手机号码
phone?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 送水订单搜索条件
*/
export interface GltTicketOrderParam extends PageParam {
id?: number;
keywords?: string;
userId?: number;
// 配送员用户ID用于配送员端查询
riderId?: number;
// 发货/配送状态(建议与 GltTicketOrder.deliveryStatus 对齐)
deliveryStatus?: number;
// 兼容 ShopOrderParam 的筛选字段(如后端已实现可直接复用)
statusFilter?: number;
}

View File

@@ -0,0 +1,118 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { GltTicketTemplate, GltTicketTemplateParam } from './model';
/**
* 分页查询水票
*/
export async function pageGltTicketTemplate(params: GltTicketTemplateParam) {
const res = await request.get<ApiResult<PageResult<GltTicketTemplate>>>(
'/glt/glt-ticket-template/page',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询水票列表
*/
export async function listGltTicketTemplate(params?: GltTicketTemplateParam) {
const res = await request.get<ApiResult<GltTicketTemplate[]>>(
'/glt/glt-ticket-template',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加水票
*/
export async function addGltTicketTemplate(data: GltTicketTemplate) {
const res = await request.post<ApiResult<unknown>>(
'/glt/glt-ticket-template',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改水票
*/
export async function updateGltTicketTemplate(data: GltTicketTemplate) {
const res = await request.put<ApiResult<unknown>>(
'/glt/glt-ticket-template',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除水票
*/
export async function removeGltTicketTemplate(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-ticket-template/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除水票
*/
export async function removeBatchGltTicketTemplate(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-ticket-template/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询水票
*/
export async function getGltTicketTemplate(id: number) {
const res = await request.get<ApiResult<GltTicketTemplate>>(
'/glt/glt-ticket-template/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据商品ID查询水票模板
*/
export async function getGltTicketTemplateByGoodsId(id: number) {
const res = await request.get<ApiResult<GltTicketTemplate>>(
'/glt/glt-ticket-template/getByGoodsId/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,55 @@
import type { PageParam } from '@/api';
/**
* 水票
*/
export interface GltTicketTemplate {
//
id?: number;
// 关联商品ID
goodsId?: number;
// 名称
name?: string;
// 启用
enabled?: boolean;
// 单位名称
unitName?: string;
// 最小购买数量
minBuyQty?: number;
// 起始发送数量
startSendQty?: number;
// 买赠买1送4 => gift_multiplier=4
giftMultiplier?: number;
// 是否把购买量也计入套票总量(默认仅计入赠送量)
includeBuyQty?: boolean;
// 每期释放数量默认每月释放10
monthlyReleaseQty?: number;
// 总共释放多少期(若配置>0则按期数平均分摊
releasePeriods?: number;
// 首期释放时机0=支付成功当刻1=下个月同日
firstReleaseMode?: number;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 水票搜索条件
*/
export interface GltTicketTemplateParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,170 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { GltUserTicket, GltUserTicketParam } from './model';
function normalizeTotal(input: unknown): number {
if (typeof input === 'number' && Number.isFinite(input)) return input;
if (typeof input === 'string') {
const n = Number(input);
if (Number.isFinite(n)) return n;
}
if (input && typeof input === 'object') {
const obj: any = input;
// Common shapes from different backends.
for (const key of ['total', 'count', 'value', 'num', 'ticketTotal', 'totalQty']) {
const v = obj?.[key];
const n = normalizeTotal(v);
if (n) return n;
}
// Sometimes nested: { data: { total: ... } } / { data: 12 }
if ('data' in obj) {
const n = normalizeTotal(obj.data);
if (n) return n;
}
}
return 0;
}
/**
* 分页查询我的水票
*/
export async function pageGltUserTicket(params: GltUserTicketParam) {
const res = await request.get<ApiResult<PageResult<GltUserTicket>>>(
'/glt/glt-user-ticket/page',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询我的水票列表
*/
export async function listGltUserTicket(params?: GltUserTicketParam) {
const res = await request.get<ApiResult<GltUserTicket[]>>(
'/glt/glt-user-ticket',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加我的水票
*/
export async function addGltUserTicket(data: GltUserTicket) {
const res = await request.post<ApiResult<unknown>>(
'/glt/glt-user-ticket',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改我的水票
*/
export async function updateGltUserTicket(data: GltUserTicket) {
const res = await request.put<ApiResult<unknown>>(
'/glt/glt-user-ticket',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除我的水票
*/
export async function removeGltUserTicket(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除我的水票
*/
export async function removeBatchGltUserTicket(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询我的水票
*/
export async function getGltUserTicket(id: number) {
const res = await request.get<ApiResult<GltUserTicket>>(
'/glt/glt-user-ticket/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取我的水票总数
*/
export async function getMyGltUserTicketTotal(userId?: number) {
const params = userId ? { userId } : undefined
const extract = (res: any) => {
// Some backends may return a raw number instead of ApiResult.
if (typeof res === 'number' || typeof res === 'string') return normalizeTotal(res)
if (res && typeof res === 'object' && 'code' in res) {
const apiRes = res as ApiResult<unknown>
if (apiRes.code === 0) return normalizeTotal(apiRes.data)
throw new Error(apiRes.message)
}
return normalizeTotal(res)
}
// Try both the configured BaseUrl host and the auth-server host.
// If the first one returns 0, keep trying; some tenants deploy GLT on a different host.
const urls = [
'/glt/glt-user-ticket/my-total'
]
let lastError: unknown
let firstTotal: number | undefined
for (const url of urls) {
try {
const res = await request.get<any>(url, params)
if (process.env.NODE_ENV === 'development') {
console.log('[getMyGltUserTicketTotal] response:', { url, res })
}
const total = extract(res)
if (firstTotal === undefined) firstTotal = total
if (total) return total
} catch (e) {
if (process.env.NODE_ENV === 'development') {
console.warn('[getMyGltUserTicketTotal] failed:', { url, error: e })
}
lastError = e
}
}
if (firstTotal !== undefined) return firstTotal
return Promise.reject(lastError instanceof Error ? lastError : new Error('获取水票总数失败'))
}

View File

@@ -0,0 +1,66 @@
import type { PageParam } from '@/api';
/**
* 我的水票
*/
export interface GltUserTicket {
//
id?: number;
// 模板ID
templateId?: number;
// 模板名称
templateName?: string;
// 商品ID
goodsId?: number;
// 订单ID
orderId?: number;
// 订单编号
orderNo?: string;
// 订单商品ID
orderGoodsId?: number;
// 总数量
totalQty?: number;
// 可用数量
availableQty?: number;
// 冻结数量
frozenQty?: number;
// 已使用数量
usedQty?: number;
// 已释放数量
releasedQty?: number;
// 用户ID
userId?: number;
// 用户昵称
nickname?: string;
// 用户头像
avatar?: string;
// 用户手机号
phone?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 我的水票搜索条件
*/
export interface GltUserTicketParam extends PageParam {
id?: number;
templateId?: number;
userId?: number;
phone?: string;
keywords?: string;
// 状态过滤0正常1冻结
status?: number;
}

View File

@@ -0,0 +1,101 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { GltUserTicketLog, GltUserTicketLogParam } from './model';
/**
* 分页查询消费日志
*/
export async function pageGltUserTicketLog(params: GltUserTicketLogParam) {
const res = await request.get<ApiResult<PageResult<GltUserTicketLog>>>(
'/glt/glt-user-ticket-log/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询消费日志列表
*/
export async function listGltUserTicketLog(params?: GltUserTicketLogParam) {
const res = await request.get<ApiResult<GltUserTicketLog[]>>(
'/glt/glt-user-ticket-log',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加消费日志
*/
export async function addGltUserTicketLog(data: GltUserTicketLog) {
const res = await request.post<ApiResult<unknown>>(
'/glt/glt-user-ticket-log',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改消费日志
*/
export async function updateGltUserTicketLog(data: GltUserTicketLog) {
const res = await request.put<ApiResult<unknown>>(
'/glt/glt-user-ticket-log',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除消费日志
*/
export async function removeGltUserTicketLog(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket-log/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除消费日志
*/
export async function removeBatchGltUserTicketLog(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket-log/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询消费日志
*/
export async function getGltUserTicketLog(id: number) {
const res = await request.get<ApiResult<GltUserTicketLog>>(
'/glt/glt-user-ticket-log/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,54 @@
import type { PageParam } from '@/api';
/**
* 消费日志
*/
export interface GltUserTicketLog {
//
id?: number;
// 用户水票ID
userTicketId?: number;
// 变更类型
changeType?: number;
// 可更改
changeAvailable?: number;
// 更改冻结状态
changeFrozen?: number;
// 已使用更改
changeUsed?: number;
// 可用后
availableAfter?: number;
// 冻结后
frozenAfter?: number;
// 使用后
usedAfter?: number;
// 订单ID
orderId?: number;
// 订单编号
orderNo?: string;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 消费日志搜索条件
*/
export interface GltUserTicketLogParam extends PageParam {
id?: number;
keywords?: string;
userId?: number;
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { GltUserTicketRelease, GltUserTicketReleaseParam } from './model';
/**
* 分页查询水票释放
*/
export async function pageGltUserTicketRelease(params: GltUserTicketReleaseParam) {
const res = await request.get<ApiResult<PageResult<GltUserTicketRelease>>>(
'/glt/glt-user-ticket-release/page',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询水票释放列表
*/
export async function listGltUserTicketRelease(params?: GltUserTicketReleaseParam) {
const res = await request.get<ApiResult<GltUserTicketRelease[]>>(
'/glt/glt-user-ticket-release',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加水票释放
*/
export async function addGltUserTicketRelease(data: GltUserTicketRelease) {
const res = await request.post<ApiResult<unknown>>(
'/glt/glt-user-ticket-release',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改水票释放
*/
export async function updateGltUserTicketRelease(data: GltUserTicketRelease) {
const res = await request.put<ApiResult<unknown>>(
'/glt/glt-user-ticket-release',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除水票释放
*/
export async function removeGltUserTicketRelease(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket-release/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除水票释放
*/
export async function removeBatchGltUserTicketRelease(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket-release/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询水票释放
*/
export async function getGltUserTicketRelease(id: number) {
const res = await request.get<ApiResult<GltUserTicketRelease>>(
'/glt/glt-user-ticket-release/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,38 @@
import type { PageParam } from '@/api';
/**
* 水票释放
*/
export interface GltUserTicketRelease {
//
id?: string;
// 水票ID
userTicketId?: string;
// 用户ID
userId?: number;
// 周期编号
periodNo?: number;
// 释放数量
releaseQty?: number;
// 释放时间
releaseTime?: string;
// 状态
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 水票释放搜索条件
*/
export interface GltUserTicketReleaseParam extends PageParam {
id?: number;
userId?: number;
keywords?: string;
}

View File

@@ -58,5 +58,4 @@ export interface PageParam {
lang?: string;
model?: string;
BaseUrl?: string;
sceneType?: string;
}

259
src/api/logistics.ts Normal file
View File

@@ -0,0 +1,259 @@
import { request } from '../utils/request'
// 物流信息接口
export interface LogisticsInfo {
expressCompany: string // 快递公司代码
expressCompanyName: string // 快递公司名称
expressNo: string // 快递单号
status: string // 物流状态
updateTime: string // 更新时间
estimatedTime?: string // 预计送达时间
currentLocation?: string // 当前位置
senderInfo?: {
name: string
phone: string
address: string
}
receiverInfo?: {
name: string
phone: string
address: string
}
}
// 物流跟踪记录
export interface LogisticsTrack {
time: string
location: string
status: string
description: string
isCompleted: boolean
}
// 物流查询响应
export interface LogisticsResponse {
success: boolean
data: {
logisticsInfo: LogisticsInfo
trackList: LogisticsTrack[]
}
message?: string
}
// 支持的快递公司
export const EXPRESS_COMPANIES = {
'SF': '顺丰速运',
'YTO': '圆通速递',
'ZTO': '中通快递',
'STO': '申通快递',
'YD': '韵达速递',
'HTKY': '百世快递',
'JD': '京东物流',
'EMS': '中国邮政',
'YUNDA': '韵达快递',
'JTSD': '极兔速递',
'DBKD': '德邦快递',
'UC': '优速快递'
}
// 查询物流信息
export const queryLogistics = async (params: {
orderId?: string
expressNo: string
expressCompany: string
}): Promise<LogisticsResponse> => {
try {
// 实际项目中这里应该调用真实的物流查询API
// 例如快递100、快递鸟、菜鸟裹裹等第三方物流查询服务
// 模拟API调用
const response = await request({
url: '/api/logistics/query',
method: 'POST',
data: params
})
return response
} catch (error) {
console.error('查询物流信息失败:', error)
// 返回模拟数据作为降级方案
return getMockLogisticsData(params)
}
}
// 获取模拟物流数据
const getMockLogisticsData = (params: {
orderId?: string
expressNo: string
expressCompany: string
}): LogisticsResponse => {
const now = new Date()
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)
const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000)
const mockData: LogisticsResponse = {
success: true,
data: {
logisticsInfo: {
expressCompany: params.expressCompany,
expressCompanyName: EXPRESS_COMPANIES[params.expressCompany] || params.expressCompany,
expressNo: params.expressNo,
status: '运输中',
updateTime: now.toISOString(),
estimatedTime: new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),
currentLocation: '北京市朝阳区',
senderInfo: {
name: '商家仓库',
phone: '400-123-4567',
address: '上海市浦东新区张江高科技园区'
},
receiverInfo: {
name: '张三',
phone: '138****5678',
address: '北京市朝阳区三里屯街道'
}
},
trackList: [
{
time: now.toISOString(),
location: '北京市朝阳区',
status: '运输中',
description: '快件正在运输途中,预计今日送达,请保持手机畅通',
isCompleted: false
},
{
time: new Date(now.getTime() - 2 * 60 * 60 * 1000).toISOString(),
location: '北京转运中心',
status: '已发出',
description: '快件已从北京转运中心发出,正在派送途中',
isCompleted: true
},
{
time: new Date(now.getTime() - 6 * 60 * 60 * 1000).toISOString(),
location: '北京转运中心',
status: '已到达',
description: '快件已到达北京转运中心,正在进行分拣',
isCompleted: true
},
{
time: yesterday.toISOString(),
location: '天津转运中心',
status: '已发出',
description: '快件已从天津转运中心发出',
isCompleted: true
},
{
time: new Date(yesterday.getTime() - 4 * 60 * 60 * 1000).toISOString(),
location: '天津转运中心',
status: '已到达',
description: '快件已到达天津转运中心',
isCompleted: true
},
{
time: twoDaysAgo.toISOString(),
location: '上海转运中心',
status: '已发出',
description: '快件已从上海转运中心发出',
isCompleted: true
},
{
time: new Date(twoDaysAgo.getTime() - 2 * 60 * 60 * 1000).toISOString(),
location: '上海转运中心',
status: '已到达',
description: '快件已到达上海转运中心,正在进行分拣',
isCompleted: true
},
{
time: threeDaysAgo.toISOString(),
location: '上海市浦东新区',
status: '已发货',
description: '商家已发货,快件已交给快递公司',
isCompleted: true
}
]
}
}
return mockData
}
// 获取快递公司列表
export const getExpressCompanies = () => {
return Object.entries(EXPRESS_COMPANIES).map(([code, name]) => ({
code,
name
}))
}
// 根据快递单号自动识别快递公司
export const detectExpressCompany = (expressNo: string): string => {
// 这里可以根据快递单号的规则来自动识别快递公司
// 实际项目中可以使用第三方服务的自动识别API
if (expressNo.startsWith('SF')) return 'SF'
if (expressNo.startsWith('YT')) return 'YTO'
if (expressNo.startsWith('ZT')) return 'ZTO'
if (expressNo.startsWith('ST')) return 'STO'
if (expressNo.startsWith('YD')) return 'YD'
if (expressNo.startsWith('JD')) return 'JD'
if (expressNo.startsWith('EMS')) return 'EMS'
// 默认返回顺丰
return 'SF'
}
// 格式化物流状态
export const formatLogisticsStatus = (status: string): {
text: string
color: string
icon: string
} => {
const statusMap = {
'已发货': { text: '已发货', color: '#1890ff', icon: '📦' },
'运输中': { text: '运输中', color: '#52c41a', icon: '🚚' },
'派送中': { text: '派送中', color: '#faad14', icon: '🏃' },
'已签收': { text: '已签收', color: '#52c41a', icon: '✅' },
'异常': { text: '异常', color: '#ff4d4f', icon: '⚠️' },
'退回': { text: '退回', color: '#ff4d4f', icon: '↩️' }
}
return statusMap[status] || { text: status, color: '#666', icon: '📋' }
}
// 计算预计送达时间
export const calculateEstimatedTime = (
sendTime: string,
expressCompany: string,
distance?: number
): string => {
const sendDate = new Date(sendTime)
let estimatedDays = 3 // 默认3天
// 根据快递公司调整预计时间
switch (expressCompany) {
case 'SF':
estimatedDays = 1 // 顺丰次日达
break
case 'JD':
estimatedDays = 1 // 京东次日达
break
case 'YTO':
case 'ZTO':
case 'STO':
estimatedDays = 2 // 三通一达2天
break
default:
estimatedDays = 3
}
// 根据距离调整(如果有距离信息)
if (distance) {
if (distance > 2000) estimatedDays += 1 // 超过2000公里加1天
if (distance > 3000) estimatedDays += 1 // 超过3000公里再加1天
}
const estimatedDate = new Date(sendDate.getTime() + estimatedDays * 24 * 60 * 60 * 1000)
return estimatedDate.toISOString()
}

View File

@@ -1,61 +0,0 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api';
import {UserVerify} from "@/api/system/userVerify/model";
import {ShopDealerWithdraw} from "@/api/shop/shopDealerWithdraw/model";
/**
* 升级为管理员
* 推送模版消息
*/
export async function pushByUpdateAdmin(userId: number) {
const res = await request.get<ApiResult<unknown>>(
'/sdy/sdy-template-message/' + userId
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 通知管理员审核操作提醒
*/
export async function pushReviewReminder(data: UserVerify) {
const res = await request.post<ApiResult<unknown>>(
'/sdy/sdy-template-message/pushReviewReminder',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 通知管理员去提现审核操作提醒
*/
export async function pushWithdrawalReviewReminder(data: ShopDealerWithdraw) {
const res = await request.post<ApiResult<unknown>>(
'/sdy/sdy-template-message/pushWithdrawalReviewReminder',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 提现成功通知
*/
export async function pushNoticeOfWithdrawalToAccount(data: ShopDealerWithdraw) {
const res = await request.post<ApiResult<unknown>>(
'/sdy/sdy-template-message/pushNoticeOfWithdrawalToAccount',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,123 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 商品文章
*/
export interface ShopArticle {
// 文章ID
articleId?: number;
// 文章标题
title?: string;
// 文章类型 0常规 1视频
type?: number;
// 模型
model?: string;
// 详情页模板
detail?: string;
// 文章分类ID
categoryId?: number;
// 上级id, 0是顶级
parentId?: number;
// 话题
topic?: string;
// 标签
tags?: string;
// 封面图
image?: string;
// 封面图宽
imageWidth?: number;
// 封面图高
imageHeight?: number;
// 付费金额
price?: string;
// 开始时间
startTime?: string;
// 结束时间
endTime?: string;
// 来源
source?: string;
// 产品概述
overview?: string;
// 虚拟阅读量(仅用作展示)
virtualViews?: number;
// 实际阅读量
actualViews?: number;
// 评分
rate?: string;
// 列表显示方式(10小图展示 20大图展示)
showType?: number;
// 访问密码
password?: string;
// 可见类型 0所有人 1登录可见 2密码可见
permission?: number;
// 发布来源客户端 (APP、H5、小程序等)
platform?: string;
// 文章附件
files?: string;
// 视频地址
video?: string;
// 接受的文件类型
accept?: string;
// 经度
longitude?: string;
// 纬度
latitude?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 街道地址
address?: string;
// 点赞数
likes?: number;
// 评论数
commentNumbers?: number;
// 提醒谁看
toUsers?: string;
// 作者
author?: string;
// 推荐
recommend?: number;
// 报名人数
bmUsers?: number;
// 用户ID
userId?: number;
// 项目ID
projectId?: number;
// 语言
lang?: string;
// 关联默认语言的文章ID
langArticleId?: number;
// 是否自动翻译
translation?: string;
// 编辑器类型 0 Markdown编辑器 1 富文本编辑器
editor?: string;
// pdf文件地址
pdfUrl?: string;
// 版本号
version?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0已发布, 1待审核 2已驳回 3违规内容
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 商品文章搜索条件
*/
export interface ShopArticleParam extends PageParam {
articleId?: number;
keywords?: string;
}

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api';
import type { PageParam } from '@/api/index';
/**
* 优惠券

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api';
import type { PageParam } from '@/api/index';
/**
* 分销商申请记录表
@@ -10,14 +10,6 @@ export interface ShopDealerApply {
userId?: number;
// 姓名
realName?: string;
// 分销商名称
dealerName?: string;
// 分销商编号
dealerCode?: string;
// 详细地址
address?: string;
// 金额
money?: number;
// 手机号
mobile?: string;
// 推荐人用户ID
@@ -25,9 +17,7 @@ export interface ShopDealerApply {
// 申请方式(10需后台审核 20无需审核)
applyType?: number;
// 申请时间
applyTime?: string;
// 签单时间
contractTime?: string;
applyTime?: number;
// 审核状态 (10待审核 20审核通过 30驳回)
applyStatus?: number;
// 审核时间
@@ -40,14 +30,6 @@ export interface ShopDealerApply {
createTime?: string;
// 修改时间
updateTime?: string;
// 过期时间
expirationTime?: string;
// 备注
comments?: string;
// 昵称
nickName?: string;
// 推荐人名称
refereeName?: string;
}
/**
@@ -55,10 +37,7 @@ export interface ShopDealerApply {
*/
export interface ShopDealerApplyParam extends PageParam {
applyId?: number;
type?: number;
dealerName?: string;
mobile?: string;
userId?: number;
keywords?: string;
applyStatus?: number; // 申请状态筛选 (10待审核 20审核通过 30驳回)
}

View File

@@ -1,45 +0,0 @@
import type { PageParam } from '@/api';
/**
* 分销商提现银行卡
*/
export interface ShopDealerBank {
// 主键ID
id?: number;
// 分销商用户ID
userId?: number;
// 开户行名称
bankName?: string;
// 银行开户名
bankAccount?: string;
// 银行卡号
bankCard?: string;
// 申请状态 (10待审核 20审核通过 30驳回)
applyStatus?: number;
// 审核时间
auditTime?: number;
// 驳回原因
rejectReason?: string;
// 是否默认
isDefault?: boolean;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
// 类型
type?: string;
// 名称
name?: string;
}
/**
* 分销商提现银行卡搜索条件
*/
export interface ShopDealerBankParam extends PageParam {
id?: number;
userId?: number;
isDefault?: boolean;
keywords?: string;
}

View File

@@ -99,17 +99,3 @@ export async function getShopDealerCapital(id: number) {
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询分销商资金明细表
*/
export async function getShopDealerCapitalByOrderNo(orderNo: string) {
const res = await request.get<ApiResult<ShopDealerCapital>>(
'/shop/shop-dealer-capital/getByCode/' + orderNo
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api';
import type { PageParam } from '@/api/index';
/**
* 分销商资金明细表
@@ -8,14 +8,14 @@ export interface ShopDealerCapital {
id?: number;
// 分销商用户ID
userId?: number;
// 订单编号
orderNo?: string;
// 订单ID
orderId?: number;
// 资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入)
flowType?: number;
// 金额
money?: string;
// 描述
comments?: string;
describe?: string;
// 对方用户ID
toUserId?: number;
// 商城ID
@@ -31,8 +31,11 @@ export interface ShopDealerCapital {
*/
export interface ShopDealerCapitalParam extends PageParam {
id?: number;
orderNo?: string;
// 仅查询当前分销商的收益/资金明细
userId?: number;
month?: string,
// 可选:按订单过滤
orderId?: number;
// 可选:资金流动类型过滤
flowType?: number;
keywords?: string;
}

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api';
import type { PageParam } from '@/api/index';
/**
* 分销商订单记录表
@@ -6,14 +6,13 @@ import type { PageParam } from '@/api';
export interface ShopDealerOrder {
// 主键ID
id?: number;
// 商品名称
title?: string;
// 买家用户ID
userId?: number;
// 昵称
nickname?: string;
// 订单编号
// 订单编号(部分接口会直接返回订单号字符串)
orderNo?: string;
// 订单ID
orderId?: number;
// 订单总金额(不含运费)
orderPrice?: string;
// 分销商用户id(一级)
@@ -28,32 +27,16 @@ export interface ShopDealerOrder {
secondMoney?: string;
// 分销佣金(三级)
thirdMoney?: string;
// 分销商昵称(一级)
firstNickname: undefined,
// 分销商昵称(二级)
secondNickname: undefined,
// 分销商昵称(三级)
thirdNickname: undefined,
// 订单结算金额
settledPrice?: string;
// 换算成度
degreePrice?: string;
// 单价
price?: string;
// 订单支付金额
payPrice?: string;
// 订单是否失效(0未失效 1已失效)
isInvalid?: number;
// 佣金结算(0未结算 1已结算)
isSettled?: number;
// 分销佣金比例
rate?: number;
// 订单月份
month?: string;
// 佣金解冻(0未解冻 1已解冻)
isUnfreeze?: number;
// 订单状态
orderStatus?: number;
// 结算时间
settleTime?: number;
// 订单备注
comments?: string;
// 商城ID
tenantId?: number;
// 创建时间
@@ -71,9 +54,7 @@ export interface ShopDealerOrderParam extends PageParam {
secondUserId?: number;
thirdUserId?: number;
userId?: number;
// 数据权限/资源ID通常传当前登录用户ID
resourceId?: number;
isInvalid?: number;
isSettled?: number;
month?: string;
keywords?: string;
}

View File

@@ -1,105 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopDealerRecord, ShopDealerRecordParam } from './model';
/**
* 分页查询客户跟进情况
*/
export async function pageShopDealerRecord(params: ShopDealerRecordParam) {
const res = await request.get<ApiResult<PageResult<ShopDealerRecord>>>(
'/shop/shop-dealer-record/page',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询客户跟进情况列表
*/
export async function listShopDealerRecord(params?: ShopDealerRecordParam) {
const res = await request.get<ApiResult<ShopDealerRecord[]>>(
'/shop/shop-dealer-record',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加客户跟进情况
*/
export async function addShopDealerRecord(data: ShopDealerRecord) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-dealer-record',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改客户跟进情况
*/
export async function updateShopDealerRecord(data: ShopDealerRecord) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-dealer-record',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除客户跟进情况
*/
export async function removeShopDealerRecord(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-dealer-record/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除客户跟进情况
*/
export async function removeBatchShopDealerRecord(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-dealer-record/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询客户跟进情况
*/
export async function getShopDealerRecord(id: number) {
const res = await request.get<ApiResult<ShopDealerRecord>>(
'/shop/shop-dealer-record/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,39 +0,0 @@
import type { PageParam } from '@/api';
/**
* 客户跟进情况
*/
export interface ShopDealerRecord {
// ID
id?: number;
// 上级id, 0是顶级
parentId?: number;
// 客户ID
dealerId?: number;
// 内容
content?: string;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0待处理, 1已完成
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 客户跟进情况搜索条件
*/
export interface ShopDealerRecordParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -99,16 +99,3 @@ export async function getShopDealerReferee(id: number) {
}
return Promise.reject(new Error(res.message));
}
/**
* 根据userId查询推荐关系
*/
export async function getShopDealerRefereeByUserId(userId: number) {
const res = await request.get<ApiResult<ShopDealerReferee>>(
'/shop/shop-dealer-referee/getByUserId/' + userId
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -8,20 +8,8 @@ export interface ShopDealerReferee {
id?: number;
// 分销商用户ID
dealerId?: number;
// 分销商名称
dealerName?: string;
// 分销商手机号
dealerPhone?: string;
// 用户id(被推荐人)
userId?: number;
// 用户头像
avatar?: string;
// 用户昵称
nickname?: string;
// 用户别名
alias?: string;
// 用户手机号
phone?: string;
// 推荐关系层级(1,2,3)
level?: number;
// 商城ID
@@ -38,8 +26,6 @@ export interface ShopDealerReferee {
export interface ShopDealerRefereeParam extends PageParam {
id?: number;
dealerId?: number;
deleted?: number;
roleId?: number;
isAdmin?: boolean;
keywords?: string;
deleted?: number;
}

View File

@@ -59,21 +59,6 @@ export async function updateShopDealerUser(data: ShopDealerUser) {
return Promise.reject(new Error(res.message));
}
/**
* 修改分销商用户记录表
* @param data
*/
export async function updateShopDealerUserByUserId(data: ShopDealerUser) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-dealer-user/updateByUserId',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除分销商用户记录表
*/
@@ -110,8 +95,9 @@ export async function getShopDealerUser(userId: number) {
const res = await request.get<ApiResult<ShopDealerUser>>(
'/shop/shop-dealer-user/' + userId
);
if (res.code === 0 && res.data) {
return res.data;
if (res.code === 0) {
// 未注册为分销商时,后端可能返回 data=null这里用 null 表示“没有分销商信息”
return res.data || null;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api';
import type { PageParam } from '@/api/index';
/**
* 分销商用户记录表
@@ -22,9 +22,6 @@ export interface ShopDealerUser {
totalMoney?: string;
// 推荐人用户ID
refereeId?: number;
dealerName?: string;
dealerPhone?: string;
dealerAvatar?: string;
// 成员数量(一级)
firstNum?: number;
// 成员数量(二级)

View File

@@ -2,6 +2,21 @@ import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopDealerWithdraw, ShopDealerWithdrawParam } from './model';
// WeChat transfer v3: backend may return `package_info` for MiniProgram to open the
// "confirm receipt" page via `wx.requestMerchantTransfer`.
export type ShopDealerWithdrawCreateResult =
| string
| {
package_info?: string;
packageInfo?: string;
[k: string]: any;
}
| null
| undefined;
// When applyStatus=20, user can "receive" (WeChat confirm receipt flow).
export type ShopDealerWithdrawReceiveResult = ShopDealerWithdrawCreateResult;
/**
* 分页查询分销商提现明细表
*/
@@ -33,11 +48,40 @@ export async function listShopDealerWithdraw(params?: ShopDealerWithdrawParam) {
/**
* 添加分销商提现明细表
*/
export async function addShopDealerWithdraw(data: ShopDealerWithdraw) {
const res = await request.post<ApiResult<unknown>>(
export async function addShopDealerWithdraw(data: ShopDealerWithdraw): Promise<ShopDealerWithdrawCreateResult> {
const res = await request.post<ApiResult<any>>(
'/shop/shop-dealer-withdraw',
data
);
if (res.code === 0) {
// Some backends return `message`, while WeChat transfer flow returns `data.package_info`.
return res.data ?? res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 用户领取(仅当 applyStatus=20 时)- 后台返回 package_info 供小程序调起确认收款页
*/
export async function receiveShopDealerWithdraw(id: number): Promise<ShopDealerWithdrawReceiveResult> {
const res = await request.post<ApiResult<any>>(
'/shop/shop-dealer-withdraw/receive/' + id,
{}
);
if (res.code === 0) {
return res.data ?? res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 领取成功回调:前端确认收款后通知后台把状态置为 applyStatus=40
*/
export async function receiveSuccessShopDealerWithdraw(id: number) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-dealer-withdraw/receive-success/' + id,
{}
);
if (res.code === 0) {
return res.message;
}

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api';
import type { PageParam } from '@/api/index';
/**
* 分销商提现明细表
@@ -30,18 +30,12 @@ export interface ShopDealerWithdraw {
rejectReason?: string;
// 来源客户端(APP、H5、小程序等)
platform?: string;
// 手机号
phone?: string;
// 备注
comments?: string;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
// 附件
image?: string;
}
/**
@@ -51,5 +45,4 @@ export interface ShopDealerWithdrawParam extends PageParam {
id?: number;
userId?: number;
keywords?: string;
applyStatus?: number; // 申请状态筛选
}

View File

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

View File

@@ -146,4 +146,7 @@ export interface ShopGoodsParam extends PageParam {
isShow?: number;
stock?: number;
keywords?: string;
recommend?: number;
// 0上架 1下架以实际后端约定为准
status?: number;
}

View File

@@ -1,4 +1,4 @@
import request from '@/utils/request';
import request, { ErrorType, RequestError } from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopOrder, ShopOrderParam, OrderCreateRequest } from './model';
@@ -113,6 +113,44 @@ export interface WxPayResult {
paySign: string;
}
/**
* 订单重新发起支付(对“已创建但未支付”的订单生成新的预支付参数,不应重复创建订单)
*
* 说明:不同后端版本可能暴露不同路径,这里做兼容探测;若全部失败,调用方可自行降级处理。
*/
export interface OrderPrepayRequest {
orderId: number;
payType: number;
}
export async function prepayShopOrder(data: OrderPrepayRequest) {
const urls = [
'/shop/shop-order/pay',
'/shop/shop-order/prepay',
'/shop/shop-order/repay'
];
let lastError: unknown;
let businessError: unknown;
for (const url of urls) {
try {
const res = await request.post<ApiResult<WxPayResult>>(url, data, { showError: false });
// request.ts 在 code!=0 时会直接 throw走到这里通常都是 code===0
if (res.code === 0) return res.data;
} catch (e) {
// 若已命中“业务错误”(例如订单已取消/已支付),优先保留该错误用于向上提示;
// 不要被后续的 404/网络错误覆盖掉,避免调用方误判为“不支持该接口”而降级走创建订单。
if (!businessError && e instanceof RequestError && e.type === ErrorType.BUSINESS_ERROR) {
businessError = e;
} else {
lastError = e;
}
}
}
return Promise.reject(businessError || lastError || new Error('发起支付失败'));
}
/**
* 创建订单
*/
@@ -140,3 +178,18 @@ export async function repairOrder(data: ShopOrder) {
}
return Promise.reject(new Error(res.message));
}
/**
* 申请|同意退款
*/
export async function refundShopOrder(data: ShopOrder) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-order/refund',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,4 +1,5 @@
import type { PageParam } from '@/api/index';
import type { ShopOrderGoods } from '@/api/shop/shopOrderGoods/model';
/**
* 订单
@@ -26,6 +27,14 @@ export interface ShopOrder {
merchantName?: string;
// 商户编号
merchantCode?: string;
// 归属门店IDshop_store.id
storeId?: number;
// 归属门店名称
storeName?: string;
// 配送员用户ID优先级派单
riderId?: number;
// 发货仓库ID
warehouseId?: number;
// 使用的优惠券id
couponId?: number;
// 使用的会员卡id
@@ -60,6 +69,8 @@ export interface ShopOrder {
sendStartTime?: string;
// 配送结束时间
sendEndTime?: string;
// 配送员送达拍照(选填)
sendEndImg?: string;
// 发货店铺id
expressMerchantId?: number;
// 发货店铺
@@ -82,6 +93,8 @@ export interface ShopOrder {
totalNum?: number;
// 教练id
coachId?: number;
// 商品ID
formId?: number;
// 支付的用户id
payUserId?: number;
// 0余额支付, 1微信支付102微信Native2会员卡支付3支付宝4现金5POS机6VIP月卡7VIP年卡8VIP次卡9IC月卡10IC年卡11IC次卡12免费13VIP充值卡14IC充值卡15积分支付16VIP季卡17IC季卡18代付
@@ -144,6 +157,8 @@ export interface ShopOrder {
selfTakeCode?: string;
// 是否已收到赠品
hasTakeGift?: string;
// 订单商品项
orderGoods?: ShopOrderGoods[];
}
/**
@@ -162,6 +177,14 @@ export interface OrderGoodsItem {
export interface OrderCreateRequest {
// 商品信息列表
goodsItems: OrderGoodsItem[];
// 归属门店IDshop_store.id
storeId?: number;
// 归属门店名称(可选)
storeName?: string;
// 配送员用户ID优先级派单
riderId?: number;
// 发货仓库ID
warehouseId?: number;
// 收货地址ID
addressId?: number;
// 支付方式
@@ -170,6 +193,8 @@ export interface OrderCreateRequest {
couponId?: number;
// 备注
comments?: string;
// 配送开始时间(用于预约/配送时间)
sendStartTime?: string;
// 配送方式 0快递 1自提
deliveryType?: number;
// 自提店铺ID
@@ -194,6 +219,14 @@ export interface OrderGoodsItem {
export interface OrderCreateRequest {
// 商品信息列表
goodsItems: OrderGoodsItem[];
// 归属门店IDshop_store.id
storeId?: number;
// 归属门店名称(可选)
storeName?: string;
// 配送员用户ID优先级派单
riderId?: number;
// 发货仓库ID
warehouseId?: number;
// 收货地址ID
addressId?: number;
// 支付方式
@@ -202,6 +235,8 @@ export interface OrderCreateRequest {
couponId?: number;
// 备注
comments?: string;
// 配送开始时间(用于预约/配送时间)
sendStartTime?: string;
// 配送方式 0快递 1自提
deliveryType?: number;
// 自提店铺ID
@@ -220,6 +255,12 @@ export interface ShopOrderParam extends PageParam {
payType?: number;
isInvoice?: boolean;
userId?: number;
// 归属门店IDshop_store.id
storeId?: number;
// 配送员用户ID
riderId?: number;
// 发货仓库ID
warehouseId?: number;
keywords?: string;
deliveryStatus?: number;
statusFilter?: number;

View File

@@ -0,0 +1,101 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStore, ShopStoreParam } from './model';
/**
* 分页查询门店
*/
export async function pageShopStore(params: ShopStoreParam) {
const res = await request.get<ApiResult<PageResult<ShopStore>>>(
'/shop/shop-store/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询门店列表
*/
export async function listShopStore(params?: ShopStoreParam) {
const res = await request.get<ApiResult<ShopStore[]>>(
'/shop/shop-store',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加门店
*/
export async function addShopStore(data: ShopStore) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改门店
*/
export async function updateShopStore(data: ShopStore) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除门店
*/
export async function removeShopStore(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-store/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除门店
*/
export async function removeBatchShopStore(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-store/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询门店
*/
export async function getShopStore(id: number) {
const res = await request.get<ApiResult<ShopStore>>(
'/shop/shop-store/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,63 @@
import type { PageParam } from '@/api';
/**
* 门店
*/
export interface ShopStore {
// 自增ID
id?: number;
// 店铺名称
name?: string;
// 门店地址
address?: string;
// 手机号码
phone?: string;
// 邮箱
email?: string;
// 门店经理
managerName?: string;
// 门店banner
shopBanner?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 经度和纬度
lngAndLat?: string;
// 位置
location?:string;
// 区域
district?: string;
// 轮廓
points?: string;
// 用户ID
userId?: number;
// 默认仓库IDshop_warehouse.id
warehouseId?: number;
// 默认仓库名称(可选)
warehouseName?: string;
// 状态
status?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 门店搜索条件
*/
export interface ShopStoreParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,101 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
import type { ShopStoreFence, ShopStoreFenceParam } from './model';
/**
* 分页查询黄家明_电子围栏
*/
export async function pageShopStoreFence(params: ShopStoreFenceParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreFence>>>(
'/shop/shop-store-fence/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询黄家明_电子围栏列表
*/
export async function listShopStoreFence(params?: ShopStoreFenceParam) {
const res = await request.get<ApiResult<ShopStoreFence[]>>(
'/shop/shop-store-fence',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加黄家明_电子围栏
*/
export async function addShopStoreFence(data: ShopStoreFence) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store-fence',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改黄家明_电子围栏
*/
export async function updateShopStoreFence(data: ShopStoreFence) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store-fence',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除黄家明_电子围栏
*/
export async function removeShopStoreFence(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-store-fence/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除黄家明_电子围栏
*/
export async function removeBatchShopStoreFence(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-store-fence/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询黄家明_电子围栏
*/
export async function getShopStoreFence(id: number) {
const res = await request.get<ApiResult<ShopStoreFence>>(
'/shop/shop-store-fence/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,43 @@
import type { PageParam } from '@/api/index';
/**
* 黄家明_电子围栏
*/
export interface ShopStoreFence {
// 自增ID
id?: number;
// 围栏名称
name?: string;
// 类型 0圆形 1方形
type?: number;
// 定位
location?: string;
// 经度
longitude?: string;
// 纬度
latitude?: string;
// 区域
district?: string;
// 电子围栏轮廓
points?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 黄家明_电子围栏搜索条件
*/
export interface ShopStoreFenceParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -1,13 +1,13 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopDealerBank, ShopDealerBankParam } from './model';
import type { ShopStoreRider, ShopStoreRiderParam } from './model';
/**
*
*
*/
export async function pageShopDealerBank(params: ShopDealerBankParam) {
const res = await request.get<ApiResult<PageResult<ShopDealerBank>>>(
'/shop/shop-dealer-bank/page',
export async function pageShopStoreRider(params: ShopStoreRiderParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreRider>>>(
'/shop/shop-store-rider/page',
params
);
if (res.code === 0) {
@@ -17,11 +17,11 @@ export async function pageShopDealerBank(params: ShopDealerBankParam) {
}
/**
*
*
*/
export async function listShopDealerBank(params?: ShopDealerBankParam) {
const res = await request.get<ApiResult<ShopDealerBank[]>>(
'/shop/shop-dealer-bank',
export async function listShopStoreRider(params?: ShopStoreRiderParam) {
const res = await request.get<ApiResult<ShopStoreRider[]>>(
'/shop/shop-store-rider',
params
);
if (res.code === 0 && res.data) {
@@ -31,11 +31,11 @@ export async function listShopDealerBank(params?: ShopDealerBankParam) {
}
/**
*
*
*/
export async function addShopDealerBank(data: ShopDealerBank) {
export async function addShopStoreRider(data: ShopStoreRider) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-dealer-bank',
'/shop/shop-store-rider',
data
);
if (res.code === 0) {
@@ -45,11 +45,11 @@ export async function addShopDealerBank(data: ShopDealerBank) {
}
/**
*
*
*/
export async function updateShopDealerBank(data: ShopDealerBank) {
export async function updateShopStoreRider(data: ShopStoreRider) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-dealer-bank',
'/shop/shop-store-rider',
data
);
if (res.code === 0) {
@@ -59,11 +59,11 @@ export async function updateShopDealerBank(data: ShopDealerBank) {
}
/**
*
*
*/
export async function removeShopDealerBank(id?: number) {
export async function removeShopStoreRider(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-dealer-bank/' + id
'/shop/shop-store-rider/' + id
);
if (res.code === 0) {
return res.message;
@@ -72,11 +72,11 @@ export async function removeShopDealerBank(id?: number) {
}
/**
*
*
*/
export async function removeBatchShopDealerBank(data: (number | undefined)[]) {
export async function removeBatchShopStoreRider(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-dealer-bank/batch',
'/shop/shop-store-rider/batch',
{
data
}
@@ -88,11 +88,11 @@ export async function removeBatchShopDealerBank(data: (number | undefined)[]) {
}
/**
* id查询分销商银行卡
* id查询配送员
*/
export async function getShopDealerBank(id: number) {
const res = await request.get<ApiResult<ShopDealerBank>>(
'/shop/shop-dealer-bank/' + id
export async function getShopStoreRider(id: number) {
const res = await request.get<ApiResult<ShopStoreRider>>(
'/shop/shop-store-rider/' + id
);
if (res.code === 0 && res.data) {
return res.data;

View File

@@ -0,0 +1,71 @@
import type { PageParam } from '@/api';
/**
* 配送员
*/
export interface ShopStoreRider {
// 主键ID
id?: string;
// 配送点IDshop_dealer.id
dealerId?: number;
// 骑手编号(可选)
riderNo?: string;
// 姓名
realName?: string;
// 手机号
mobile?: string;
// 头像
avatar?: string;
// 身份证号(可选)
idCardNo?: string;
// 状态1启用0禁用
status?: number;
// 接单状态0休息/下线1在线2忙碌
workStatus?: number;
// 是否开启自动派单1是0否
autoDispatchEnabled?: number;
// 派单优先级(同小区多骑手时可用,值越大越优先)
dispatchPriority?: number;
// 最大同时配送单数0表示不限制
maxOnhandOrders?: number;
// 是否计算工资(提成)1计算0不计算如三方配送点可设0
commissionCalcEnabled?: number;
// 水每桶提成金额(元/桶)
waterBucketUnitFee?: string;
// 其他商品提成方式1按订单固定金额2按订单金额比例3按商品规则(另表)
otherGoodsCommissionType?: number;
// 其他商品提成值:固定金额(元)或比例(%)
otherGoodsCommissionValue?: string;
// 用户ID
userId?: number;
// 经度(配送员当前位置)
longitude?: string;
// 纬度(配送员当前位置)
latitude?: string;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 配送员搜索条件
*/
export interface ShopStoreRiderParam extends PageParam {
id?: number;
keywords?: string;
// 配送点/门店ID后端可能用 dealerId 或 storeId
dealerId?: number;
storeId?: number;
status?: number;
workStatus?: number;
autoDispatchEnabled?: number;
}

View File

@@ -0,0 +1,101 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStoreUser, ShopStoreUserParam } from './model';
/**
* 分页查询店员
*/
export async function pageShopStoreUser(params: ShopStoreUserParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreUser>>>(
'/shop/shop-store-user/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询店员列表
*/
export async function listShopStoreUser(params?: ShopStoreUserParam) {
const res = await request.get<ApiResult<ShopStoreUser[]>>(
'/shop/shop-store-user',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加店员
*/
export async function addShopStoreUser(data: ShopStoreUser) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store-user',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改店员
*/
export async function updateShopStoreUser(data: ShopStoreUser) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store-user',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除店员
*/
export async function removeShopStoreUser(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-store-user/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除店员
*/
export async function removeBatchShopStoreUser(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-store-user/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询店员
*/
export async function getShopStoreUser(id: number) {
const res = await request.get<ApiResult<ShopStoreUser>>(
'/shop/shop-store-user/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,36 @@
import type { PageParam } from '@/api';
/**
* 店员
*/
export interface ShopStoreUser {
// 主键ID
id?: number;
// 配送点IDshop_dealer.id
storeId?: number;
// 用户ID
userId?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 店员搜索条件
*/
export interface ShopStoreUserParam extends PageParam {
id?: number;
keywords?: string;
storeId?: number;
userId?: number;
isDelete?: number;
}

View File

@@ -0,0 +1,101 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
import type { ShopStoreWarehouse, ShopStoreWarehouseParam } from './model';
/**
* 分页查询仓库
*/
export async function pageShopStoreWarehouse(params: ShopStoreWarehouseParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreWarehouse>>>(
'/shop/shop-store-warehouse/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询仓库列表
*/
export async function listShopStoreWarehouse(params?: ShopStoreWarehouseParam) {
const res = await request.get<ApiResult<ShopStoreWarehouse[]>>(
'/shop/shop-store-warehouse',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加仓库
*/
export async function addShopStoreWarehouse(data: ShopStoreWarehouse) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store-warehouse',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改仓库
*/
export async function updateShopStoreWarehouse(data: ShopStoreWarehouse) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store-warehouse',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除仓库
*/
export async function removeShopStoreWarehouse(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-store-warehouse/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除仓库
*/
export async function removeBatchShopStoreWarehouse(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-store-warehouse/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询仓库
*/
export async function getShopStoreWarehouse(id: number) {
const res = await request.get<ApiResult<ShopStoreWarehouse>>(
'/shop/shop-store-warehouse/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,53 @@
import type { PageParam } from '@/api/index';
/**
* 仓库
*/
export interface ShopStoreWarehouse {
// 自增ID
id?: number;
// 仓库名称
name?: string;
// 唯一标识
code?: string;
// 类型 中心仓,区域仓,门店仓
type?: string;
// 仓库地址
address?: string;
// 真实姓名
realName?: string;
// 联系电话
phone?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 经纬度
lngAndLat?: string;
// 用户ID
userId?: number;
// 状态
status?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 仓库搜索条件
*/
export interface ShopStoreWarehouseParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -38,6 +38,8 @@ export interface ShopUserAddress {
tenantId?: number;
// 注册时间
createTime?: string;
// 更新时间
updateTime?: string;
}
/**

View File

@@ -138,3 +138,18 @@ export async function getMyExpiredCoupons() {
}
return Promise.reject(new Error(res.message));
}
/**
* 领取优惠券
*/
export async function takeCoupon(params: { couponId: number; userId: number }) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-user-coupon/take',
params
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,7 +1,6 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopUserReferee, ShopUserRefereeParam } from './model';
import type {ShopDealerReferee} from "@/api/shop/shopDealerReferee/model";
/**
* 分页查询用户推荐关系表
@@ -100,17 +99,3 @@ export async function getShopUserReferee(id: number) {
}
return Promise.reject(new Error(res.message));
}
/**
* 根据userId查询推荐关系
*/
export async function getShopUserRefereeByUserId(userId: number) {
const res = await request.get<ApiResult<ShopDealerReferee>>(
'/shop/shop-user-referee/getByUserId/' + userId
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -22,8 +22,7 @@ export interface ChatMessage {
withdraw?: number;
// 文件信息
fileInfo?: string;
//
toUserName?: string;
toUserName?: any;
formUserName?: string;
// 批量发送
toUserIds?: any[];

View File

@@ -1,7 +1,6 @@
import request from '@/utils/request';
import Taro from '@tarojs/taro'
import dayjs from 'dayjs';
// @ts-ignore
import crypto from 'crypto-js';
import {Base64} from 'js-base64';
import {FileRecord} from "@/api/system/file/model";
@@ -22,7 +21,7 @@ export async function uploadOssByPath(filePath: string) {
let stsExpired = Taro.getStorageSync('stsExpiredAt');
if (!sts || (stsExpired && dayjs().isBefore(dayjs(stsExpired)))) {
// @ts-ignore
const {data: {data: {credentials}}} = await request.get(`https://server.websoft.top/api/oss/getSTSToken`)
const {data: {data: {credentials}}} = await request.get(`https://gle-server.websoft.top/api/oss/getSTSToken`)
Taro.setStorageSync('sts', credentials)
Taro.setStorageSync('stsExpiredAt', credentials.expiration)
sts = credentials
@@ -50,7 +49,7 @@ export async function uploadOssByPath(filePath: string) {
})
}
const computeSignature = (accessKeySecret: string, canonicalString: string) => {
const computeSignature = (accessKeySecret: string, canonicalString: string): string => {
return crypto.enc.Base64.stringify(crypto.HmacSHA1(canonicalString, accessKeySecret));
}

View File

@@ -30,3 +30,18 @@ export async function updateUserRole(data: UserRole) {
}
return Promise.reject(new Error(res.message));
}
/**
* 新增用户角色
* 说明:部分后端实现为 POST 新增、PUT 修改;这里补齐 API 以便新用户无角色时可以创建默认角色。
*/
export async function addUserRole(data: UserRole) {
const res = await request.post<ApiResult<unknown>>(
SERVER_API_URL + '/system/user-role',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -53,7 +53,6 @@ export interface UserVerify {
*/
export interface UserVerifyParam extends PageParam {
id?: number;
userId?: number;
status?: number;
keywords?: string;
}

View File

@@ -43,6 +43,15 @@ export interface UserOrderStats {
total: number
}
// 用户卡片统计(个人中心头部:余额/积分/优惠券/水票)
export interface UserCardStats {
balance: string
points: number
coupons: number
giftCards: number
lastUpdateTime?: string
}
// 用户完整数据
export interface UserDashboard {
balance: UserBalance
@@ -108,6 +117,17 @@ export async function getUserOrderStats() {
return Promise.reject(new Error(res.message))
}
/**
* 获取用户卡片统计(一次性返回余额/积分/可用优惠券/未使用礼品卡数量)
*/
export async function getUserCardStats() {
const res = await request.get<ApiResult<UserCardStats>>('/user/card/stats')
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
* 获取用户完整仪表板数据(一次性获取所有数据)
*/

View File

@@ -1,4 +1,4 @@
export default defineAppConfig({
export default {
pages: [
'pages/index/index',
'pages/cart/cart',
@@ -27,10 +27,20 @@ export default defineAppConfig({
"detail/index"
]
},
{
"root": "coupon",
"pages": [
"index"
]
},
{
"root": "user",
"pages": [
"order/order",
"order/logistics/index",
"order/evaluate/index",
"order/refund/index",
"order/progress/index",
"company/company",
"profile/profile",
"setting/setting",
@@ -43,11 +53,17 @@ export default defineAppConfig({
"wallet/wallet",
"coupon/index",
"points/points",
"gift/index",
"gift/redeem",
"gift/detail",
"ticket/index",
"ticket/use",
"ticket/orders/index",
// "gift/index",
// "gift/redeem",
// "gift/detail",
// "gift/add",
"store/verification",
"store/orders/index",
"theme/index",
"poster/poster",
"chat/conversation/index",
"chat/message/index",
"chat/message/add",
@@ -60,38 +76,45 @@ export default defineAppConfig({
"index",
"apply/add",
"withdraw/index",
"withdraw/admin",
"orders/index",
"capital/index",
"capital/detail",
"capital/record",
"team/index",
"qrcode/index",
"invite-stats/index",
"info",
"customer/index",
"customer/add",
"customer/trading",
"wechat/index",
"bank/index",
"bank/add"
"info"
]
},
{
"root": "shop",
"pages": [
'category/index',
'orderDetail/index',
'goodsDetail/index',
'orderConfirm/index',
'orderConfirmCart/index',
'comments/index',
'search/index']
},
{
"root": "store",
"pages": [
"index",
"orders/index"
]
},
{
"root": "rider",
"pages": [
"index",
"orders/index",
"ticket/verification/index"
]
},
// {
// "root": "shop",
// "pages": ['category/index',
// 'orderDetail/index',
// 'goodsDetail/index',
// 'orderConfirm/index',
// 'orderConfirmCart/index',
// 'search/index']
// },
{
"root": "admin",
"pages": [
"index",
"article/index",
"userVerify/index"
]
}
],
@@ -135,6 +158,9 @@ export default defineAppConfig({
permission: {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
},
"scope.writePhotosAlbum": {
"desc": "用于保存小程序码到相册,方便分享给好友"
}
}
})
}

View File

@@ -10,14 +10,14 @@ page{
background-position: bottom;
}
// 在全局样式文件中添加
/* 在全局样式文件中添加 */
button {
&::after {
border: none !important;
}
}
// 去掉 Grid 组件的边框
/* 去掉 Grid 组件的边框 */
.no-border-grid {
.nut-grid-item {
border: none !important;
@@ -38,7 +38,7 @@ button {
}
}
// 微信授权按钮的特殊样式
/* 微信授权按钮的特殊样式 */
button[open-type="getPhoneNumber"] {
background: none !important;
padding: 0 !important;
@@ -87,8 +87,21 @@ button[open-type="chooseAvatar"] {
justify-content: center;
height: 80px;
}
.cart-buy-only{
border-radius: 20px;
flex: 1;
}
}
image {
margin: 0; /* 全局设置图片的 margin */
}
/* 管理员面板功能项交互效果 */
.admin-feature-item {
transition: transform 0.15s ease-in-out;
}
.admin-feature-item:active {
transform: scale(0.95);
}

View File

@@ -7,9 +7,11 @@ import {loginByOpenId} from "@/api/layout";
import {TenantId} from "@/config/app";
import {saveStorageByLoginUser} from "@/utils/server";
import {parseInviteParams, saveInviteParams, trackInviteSource, handleInviteRelation} from "@/utils/invite";
import {configWebsiteField} from "@/api/cms/cmsWebsiteField";
import { useConfig } from "@/hooks/useConfig"; // 引入新的自定义Hook
function App(props: { children: any; }) {
const { refetch: handleTheme } = useConfig(); // 使用新的Hook
const reload = () => {
Taro.login({
success: (res) => {
@@ -38,6 +40,8 @@ function App(props: { children: any; }) {
};
// 可以使用所有的 React Hooks
useEffect(() => {
// 设置主题 (现在由useConfig Hook处理)
handleTheme()
// Taro.getSetting获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
success: (res) => {
@@ -53,13 +57,12 @@ function App(props: { children: any; }) {
// 处理小程序启动参数中的邀请信息
const options = Taro.getLaunchOptionsSync()
handleLaunchOptions(options)
handleTheme()
})
// 处理启动参数
const handleLaunchOptions = (options: any) => {
try {
console.log('=== 小程序启动参数处理开始 ===')
console.log('=== 小程 序启动参数处理开始 ===')
console.log('完整启动参数:', JSON.stringify(options, null, 2))
// 解析邀请参数
@@ -75,11 +78,7 @@ function App(props: { children: any; }) {
// 显示邀请提示
setTimeout(() => {
Taro.showToast({
title: `检测到邀请信息 ID:${inviteParams.inviter}`,
icon: 'success',
duration: 3000
})
console.log(`检测到邀请信息 ID:${inviteParams.inviter}`)
}, 1000)
} else {
@@ -92,19 +91,6 @@ function App(props: { children: any; }) {
}
}
const handleTheme = () => {
configWebsiteField().then(data => {
// 设置主题
if(data.theme && !Taro.getStorageSync('user_theme')){
Taro.setStorageSync('user_theme', data.theme)
}
// 自定义接口
if(data.apiUrl && process.env.NODE_ENV !== 'development'){
Taro.setStorageSync('ApiUrl', data.apiUrl)
}
})
}
// 对应 onHide
useDidHide(() => {
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
src/assets/tabbar/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -1,25 +1,16 @@
import {Image, Cell} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
import {CmsArticle} from "@/api/cms/cmsArticle/model";
const ArticleList = (props: any) => {
return (
<>
<View className={'px-3'}>
{props.data.map((item: any, index: number) => {
<div className={'px-3'}>
{props.data.map((item: CmsArticle, index: number) => {
return (
<Cell
title={
<View>
<View className="text-base font-medium mb-1">{item.title}</View>
{item.comments && (
<Text className="text-sm text-gray-500 leading-relaxed">
{item.comments}
</Text>
)}
</View>
}
title={item.title}
extra={
<Image src={item.image} mode={'aspectFit'} lazyLoad={false} width={100} height="100"/>
}
@@ -28,7 +19,7 @@ const ArticleList = (props: any) => {
/>
)
})}
</View>
</div>
</>
)
}

View File

@@ -1,5 +1,5 @@
import Taro from '@tarojs/taro'
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
import {useShareAppMessage} from "@tarojs/taro"
import {Loading} from '@nutui/nutui-react-taro'
import {useEffect, useState} from "react"
import {useRouter} from '@tarojs/taro'
@@ -42,13 +42,6 @@ function Category() {
})
}, []);
useShareTimeline(() => {
return {
title: `${nav?.categoryName}_易赊宝`,
path: `/shop/category/index?id=${categoryId}`
};
});
useShareAppMessage(() => {
return {
title: `${nav?.categoryName}_易赊宝`,

View File

@@ -17,8 +17,8 @@ function Detail() {
const reload = async () => {
const item = await getCmsArticle(Number(params.id))
if (item) {
item.content = wxParse(`${item.content}`)
if (item && item.content) {
item.content = wxParse(item.content)
setItem(item)
Taro.setNavigationBarTitle({
title: `${item?.categoryName}`
@@ -43,6 +43,10 @@ function Detail() {
<div className={'p-4 font-bold text-lg'}>{item?.title}</div>
<div className={'text-gray-400 text-sm px-4 '}>{item?.createTime}</div>
<View className={'content p-4'}>
{/*如果有视频就显示视频 视频沾满宽度*/}
{item?.video && <View className={'w-full'}>
<video src={item?.video} controls={true} width={'100%'}></video>
</View>}
<RichText nodes={item?.content}/>
</View>
<Line height={44}/>

View File

@@ -5,6 +5,7 @@ import {getUserInfo} from "@/api/layout";
import {useEffect, useState} from "react";
import {getCmsArticle} from "@/api/cms/cmsArticle";
import {CmsArticle} from "@/api/cms/cmsArticle/model";
import { goToRegister } from '@/utils/auth'
function AddCartBar() {
const { router } = getCurrentInstance();
@@ -13,13 +14,8 @@ function AddCartBar() {
const [IsLogin, setIsLogin] = useState<boolean>(false)
const onPay = () => {
if (!IsLogin) {
Taro.showToast({title: `请先登录`, icon: 'error'})
setTimeout(() => {
Taro.switchTab(
{
url: '/pages/user/user',
},
)
goToRegister({ redirect: '/pages/user/user' })
}, 1000)
return false;
}
@@ -30,7 +26,7 @@ function AddCartBar() {
navTo('/bszx/pay/pay?id=' + id)
}
}
const reload = (id) => {
const reload = (id: number) => {
getCmsArticle(id).then(data => {
setArticle(data)
})
@@ -47,7 +43,7 @@ function AddCartBar() {
useEffect(() => {
const id = router?.params.id as number | undefined;
setId(id)
reload(id);
reload(Number(id));
}, []);
return (

View File

@@ -0,0 +1,139 @@
import React from 'react';
import { View, Text } from '@tarojs/components';
import { Button } from '@nutui/nutui-react-taro';
import { Scan, Setting, User, Shop } from '@nutui/icons-react-taro';
import navTo from '@/utils/common';
export interface AdminPanelProps {
/** 是否显示面板 */
visible: boolean;
/** 关闭面板回调 */
onClose?: () => void;
/** 自定义样式类名 */
className?: string;
}
/**
* 管理员功能面板组件
*/
const AdminPanel: React.FC<AdminPanelProps> = ({
visible,
onClose,
className = ''
}) => {
if (!visible) return null;
// 管理员功能列表
const adminFeatures = [
{
id: 'unified-qr',
title: '统一扫码',
description: '扫码登录和核销一体化功能',
icon: <Scan className="text-blue-500" size="24" />,
color: 'bg-blue-50 border-blue-200',
onClick: () => {
navTo('/passport/unified-qr/index', true);
onClose?.();
}
},
{
id: 'user-management',
title: '用户管理',
description: '管理系统用户信息',
icon: <User className="text-purple-500" size="24" />,
color: 'bg-purple-50 border-purple-200',
onClick: () => {
// TODO: 跳转到用户管理页面
console.log('跳转到用户管理');
onClose?.();
}
},
{
id: 'store-management',
title: '门店管理',
description: '管理门店信息和设置',
icon: <Shop className="text-orange-500" size="24" />,
color: 'bg-orange-50 border-orange-200',
onClick: () => {
// TODO: 跳转到门店管理页面
console.log('跳转到门店管理');
onClose?.();
}
},
{
id: 'system-settings',
title: '系统设置',
description: '系统配置和参数管理',
icon: <Setting className="text-gray-500" size="24" />,
color: 'bg-gray-50 border-gray-200',
onClick: () => {
// TODO: 跳转到系统设置页面
console.log('跳转到系统设置');
onClose?.();
}
}
];
return (
<View className={`admin-panel ${className}`}>
{/* 遮罩层 */}
<View
className="fixed inset-0 bg-black bg-opacity-50 z-40"
onClick={onClose}
/>
{/* 面板内容 */}
<View className="fixed bottom-0 left-0 right-0 bg-white rounded-t-3xl z-50 overflow-hidden">
{/* 面板头部 */}
<View className="flex items-center justify-between p-4 border-b border-gray-100">
<View className="flex items-center">
<Setting className="text-blue-500 mr-2" size="20" />
<Text className="text-lg font-bold text-gray-800"></Text>
</View>
<Button
size="small"
type="default"
onClick={onClose}
className="text-gray-500"
>
</Button>
</View>
{/* 功能网格 */}
<View className="p-4 pb-8">
<View className="grid grid-cols-2 gap-3">
{adminFeatures.map((feature) => (
<View
key={feature.id}
className={`${feature.color} border rounded-xl p-4 admin-feature-item`}
onClick={feature.onClick}
>
<View className="flex items-center mb-2">
{feature.icon}
<Text className="ml-2 font-medium text-gray-800">
{feature.title}
</Text>
</View>
<Text className="text-xs text-gray-600 leading-relaxed">
{feature.description}
</Text>
</View>
))}
</View>
</View>
{/* 底部提示 */}
<View className="px-4 pb-4">
<View className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<Text className="text-xs text-yellow-700 text-center">
💡
</Text>
</View>
</View>
</View>
</View>
);
};
export default AdminPanel;

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Badge } from "@nutui/nutui-react-taro";
import { Cart } from "@nutui/icons-react-taro";
import Taro from '@tarojs/taro';
import { useCart } from "@/hooks/useCart";
import { switchTab } from '@/utils/navigation';
interface CartIconProps {
style?: React.CSSProperties;
@@ -26,13 +26,13 @@ const CartIcon: React.FC<CartIconProps> = ({
onClick();
} else {
// 默认跳转到购物车页面
Taro.switchTab({ url: '/pages/cart/cart' });
switchTab('cart/cart');
}
};
if (showBadge) {
return (
<div
<div
className={className}
style={style}
onClick={handleClick}
@@ -47,7 +47,7 @@ const CartIcon: React.FC<CartIconProps> = ({
}
return (
<div
<div
className={className}
style={style}
onClick={handleClick}

View File

@@ -2,115 +2,177 @@
position: relative;
display: flex;
width: 100%;
height: 120px;
margin-bottom: 16px;
border-radius: 12px;
height: 160px;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
background: #fff;
transition: all 0.3s ease;
border: 2px solid #f0f0f0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* 更精美的阴影效果 */
/*box-shadow:
0 4px 20px rgba(0, 0, 0, 0.08),
0 1px 3px rgba(0, 0, 0, 0.1);*/
/* 边框光晕效果 */
/*&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 16px;
padding: 1px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1));
mask-composite: exclude;
pointer-events: none;
}*/
&:active {
transform: scale(0.98);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
transform: scale(0.98) translateY(1px);
box-shadow:
0 2px 12px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.15);
}
&.disabled {
opacity: 0.6;
/* filter: grayscale(0.3); 小程序不支持filter属性 */
}
.coupon-left {
flex-shrink: 0;
width: 110px;
width: 140px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
position: relative;
overflow: hidden;
/* 添加光泽效果 */
&::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
transform: rotate(45deg);
animation: shimmer 3s infinite;
}
&.theme-red {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 50%, #e53e3e 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
&.theme-orange {
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
background: linear-gradient(135deg, #ffa726 0%, #ff9800 50%, #f57c00 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
&.theme-blue {
background: linear-gradient(135deg, #42a5f5 0%, #2196f3 100%);
background: linear-gradient(135deg, #42a5f5 0%, #2196f3 50%, #1976d2 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
&.theme-purple {
background: linear-gradient(135deg, #ab47bc 0%, #9c27b0 100%);
background: linear-gradient(135deg, #ab47bc 0%, #9c27b0 50%, #7b1fa2 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
&.theme-green {
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 50%, #388e3c 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.amount-wrapper {
display: flex;
align-items: baseline;
margin-bottom: 8px;
margin-bottom: 12px;
position: relative;
z-index: 2;
.currency {
font-size: 28px;
font-weight: 600;
margin-right: 2px;
font-size: 30px;
font-weight: 700;
margin-right: 3px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.amount {
font-size: 36px;
font-weight: bold;
font-size: 42px;
font-weight: 800;
line-height: 1;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
letter-spacing: -1px;
}
}
.condition {
font-size: 22px;
opacity: 0.9;
font-size: 24px;
opacity: 0.95;
text-align: center;
line-height: 1.2;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 2;
}
}
.coupon-divider {
flex-shrink: 0;
width: 2px;
width: 3px;
position: relative;
background: #f5f5f5;
background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 50%, #f8fafc 100%);
.divider-line {
width: 100%;
height: 100%;
position: absolute;
top: 24px;
bottom: 24px;
left: 50%;
width: 1px;
transform: translateX(-50%);
background: repeating-linear-gradient(
to bottom,
transparent 0px,
transparent 4px,
#ddd 4px,
#ddd 8px
#cbd5e1 0px,
#cbd5e1 6px,
transparent 6px,
transparent 12px
);
}
.divider-circle-top {
position: absolute;
width: 16px;
height: 16px;
width: 20px;
height: 20px;
background: #f5f5f5;
border-radius: 50%;
top: -8px;
left: -7px;
top: -10px;
left: -8.5px;
border: 2px solid #e2e8f0;
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
.divider-circle-bottom {
position: absolute;
width: 16px;
height: 16px;
width: 20px;
height: 20px;
background: #f5f5f5;
border-radius: 50%;
bottom: -8px;
left: -7px;
bottom: -10px;
left: -8.5px;
border: 2px solid #e2e8f0;
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
}
@@ -120,7 +182,8 @@
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 16px;
padding: 20px 18px;
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
.coupon-info {
flex: 1;
@@ -129,17 +192,28 @@
justify-content: center;
.coupon-title {
font-size: 32px;
font-weight: 600;
color: #1f2937;
margin-bottom: 6px;
font-size: 34px;
font-weight: 700;
color: #1a202c;
margin-bottom: 12px;
line-height: 1.3;
letter-spacing: -0.5px;
/* 文字渐变效果在小程序中不支持,使用纯色替代 */
}
.coupon-validity {
font-size: 26px;
color: #6b7280;
color: #718096;
line-height: 1.2;
font-weight: 500;
/* 添加图标前缀 */
&::before {
content: '';
margin-right: 6px;
font-size: 24px;
}
}
}
@@ -150,45 +224,71 @@
flex-shrink: 0;
.coupon-btn {
min-width: 120px;
height: 60px;
border-radius: 30px;
font-size: 26px;
min-width: 140px;
height: 72px;
border-radius: 34px;
font-size: 28px;
border: none;
color: #fff;
font-weight: 600;
transition: all 0.2s ease;
font-weight: 700;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
/* 添加按钮光泽效果 */
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
&:hover::before {
left: 100%;
}
&:active {
transform: scale(0.95);
transform: scale(0.96) translateY(1px);
}
&.theme-red {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 50%, #e53e3e 100%);
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
}
&.theme-orange {
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
background: linear-gradient(135deg, #ffa726 0%, #ff9800 50%, #f57c00 100%);
box-shadow: 0 4px 12px rgba(255, 167, 38, 0.3);
}
&.theme-blue {
background: linear-gradient(135deg, #42a5f5 0%, #2196f3 100%);
background: linear-gradient(135deg, #42a5f5 0%, #2196f3 50%, #1976d2 100%);
box-shadow: 0 4px 12px rgba(66, 165, 245, 0.3);
}
&.theme-purple {
background: linear-gradient(135deg, #ab47bc 0%, #9c27b0 100%);
background: linear-gradient(135deg, #ab47bc 0%, #9c27b0 50%, #7b1fa2 100%);
box-shadow: 0 4px 12px rgba(171, 71, 188, 0.3);
}
&.theme-green {
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 50%, #388e3c 100%);
box-shadow: 0 4px 12px rgba(102, 187, 106, 0.3);
}
}
.status-text {
font-size: 26px;
color: #9ca3af;
padding: 8px 12px;
font-weight: 500;
font-size: 28px;
color: #a0aec0;
padding: 12px 16px;
font-weight: 600;
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
border-radius: 20px;
border: 1px solid #e2e8f0;
}
}
}
@@ -199,19 +299,84 @@
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.15);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
/* backdrop-filter: blur(2px); 小程序不支持backdrop-filter属性 */
.status-badge {
background: rgba(0, 0, 0, 0.6);
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 4px 12px;
border-radius: 12px;
font-size: 28px;
font-weight: 500;
padding: 8px 16px;
border-radius: 16px;
font-size: 30px;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
}
}
/* 动画效果 */
@keyframes shimmer {
0% {
transform: translateX(-100%) translateY(-100%) rotate(45deg);
}
100% {
transform: translateX(100%) translateY(100%) rotate(45deg);
}
}
/* 响应式优化 */
@media (max-width: 768px) {
.coupon-card {
height: 150px;
.coupon-left {
width: 130px;
.amount-wrapper {
.currency {
font-size: 26px;
}
.amount {
font-size: 38px;
}
}
.condition {
font-size: 22px;
}
}
.coupon-right {
padding: 24px 20px;
.coupon-info {
.coupon-title {
font-size: 30px;
}
.coupon-validity {
font-size: 24px;
}
}
.coupon-actions {
.coupon-btn {
min-width: 130px;
height: 64px;
font-size: 26px;
}
.status-text {
font-size: 26px;
}
}
}
}
}

View File

@@ -0,0 +1,67 @@
.coupon-list-container {
padding: 0 20px;
background: #f5f5f5;
min-height: 100%;
height: 100%;
}
.coupon-list-title {
font-size: 32px;
font-weight: 600;
color: #1f2937;
margin-bottom: 32px;
padding-top: 24px;
}
.coupon-list-empty {
text-align: center;
padding: 120px 20px;
color: #9ca3af;
font-size: 28px;
}
.coupon-list-content {
padding-bottom: 40px;
}
.coupon-list-item {
margin-bottom: 32px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
}
&:active {
transform: translateY(0);
}
&:last-child {
margin-bottom: 24px;
}
}
/* 水平滚动布局样式 */
.coupon-horizontal-container {
.coupon-horizontal-title {
font-size: 32px;
font-weight: 600;
color: #1f2937;
margin-bottom: 24px;
padding-left: 16px;
}
.coupon-horizontal-scroll {
padding: 0 16px;
.coupon-horizontal-item {
flex-shrink: 0;
width: 240px;
margin-right: 16px;
&:last-child {
margin-right: 16px; // 保持右边距
}
}
}
}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import { View, ScrollView } from '@tarojs/components'
import CouponCard, { CouponCardProps } from './CouponCard'
import './CouponList.scss'
export interface CouponListProps {
/** 优惠券列表数据 */
@@ -32,26 +33,29 @@ const CouponList: React.FC<CouponListProps> = ({
// 垂直布局
if (layout === 'vertical') {
return (
<View className="p-4">
<View className="coupon-list-container">
{title && (
<View className="font-semibold text-gray-800 mb-4">{title}</View>
<View className="coupon-list-title">{title}</View>
)}
{coupons.length === 0 ? (
showEmpty && (
<View className="text-center py-10 px-5 text-gray-500">
<View className="coupon-list-empty">
{emptyText}
</View>
)
) : (
coupons.map((coupon, index) => (
<View
key={index}
onClick={() => handleCouponClick(coupon, index)}
>
<CouponCard {...coupon} />
</View>
))
<View className="coupon-list-content">
{coupons.map((coupon, index) => (
<View
key={index}
className="coupon-list-item"
onClick={() => handleCouponClick(coupon, index)}
>
<CouponCard {...coupon} />
</View>
))}
</View>
)}
</View>
)
@@ -59,29 +63,29 @@ const CouponList: React.FC<CouponListProps> = ({
// 水平滚动布局
return (
<View>
<View className="coupon-horizontal-container">
{title && (
<View className="font-semibold text-gray-800 mb-4 pl-4">
<View className="coupon-horizontal-title">
{title}
</View>
)}
{coupons.length === 0 ? (
showEmpty && (
<View className="text-center py-10 px-5 text-gray-500">
<View className="coupon-list-empty">
{emptyText}
</View>
)
) : (
<ScrollView
scrollX
className="flex p-4 gap-2 overflow-x-auto"
className="coupon-horizontal-scroll flex overflow-x-auto"
showScrollbar={false}
>
{coupons.map((coupon, index) => (
<View
key={index}
className="flex-shrink-0 w-60 mb-0"
className="coupon-horizontal-item"
onClick={() => handleCouponClick(coupon, index)}
>
<CouponCard {...coupon} />

View File

@@ -24,7 +24,7 @@ export interface GiftCardProps {
faceValue?: string
/** 商品原价 */
originalPrice?: string
/** 礼品卡类型10-实物礼品 20-虚拟礼品卡 30-服务礼品卡 */
/** 礼品卡类型10-礼品 20-虚拟礼品卡 30-服务礼品卡 */
type?: number
/** 状态0-未使用 1-已使用 2-失效 */
status?: number
@@ -112,10 +112,10 @@ const GiftCard: React.FC<GiftCardProps> = ({
// 获取礼品卡类型文本
const getTypeText = () => {
switch (type) {
case 10: return '实物礼品'
case 10: return '礼品'
case 20: return '虚拟礼品卡'
case 30: return '服务礼品卡'
default: return '礼品卡'
default: return '水票'
}
}

View File

@@ -51,7 +51,7 @@ const GiftCardGuide: React.FC<GiftCardGuideProps> = ({
title: '礼品卡类型说明',
icon: <Gift size="24" className="text-purple-500" />,
content: [
'🎁 实物礼品:需到指定地址领取商品',
'🎁 礼品:需到指定地址领取商品',
'💻 虚拟礼品卡:自动发放到账户余额',
'🛎️ 服务礼品卡:联系客服预约服务',
'⏰ 注意查看有效期,过期无法使用'

View File

@@ -28,10 +28,10 @@ const GiftCardShare: React.FC<GiftCardShareProps> = ({
// 获取礼品卡类型文本
const getTypeText = () => {
switch (giftCard.type) {
case 10: return '实物礼品'
case 10: return '礼品'
case 20: return '虚拟礼品卡'
case 30: return '服务礼品卡'
default: return '礼品卡'
default: return '水票'
}
}

View File

@@ -2,7 +2,7 @@ import {NavBar} from '@nutui/nutui-react-taro'
import {ArrowLeft} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
function Header(props) {
function Header(props: any) {
return (
<>
<NavBar

View File

@@ -1,6 +1,6 @@
# PaymentCountdown 支付倒计时组件
基于订单创建时间的支付倒计时组件,支持静态显示和实时更新两种模式。
基于订单过期时间(`expirationTime`的支付倒计时组件,支持静态显示和实时更新两种模式。
## 功能特性
@@ -19,7 +19,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
// 订单列表页 - 静态显示
<PaymentCountdown
createTime={order.createTime}
expirationTime={order.expirationTime}
payStatus={order.payStatus}
realTime={false}
mode="badge"
@@ -27,7 +27,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
// 订单详情页 - 实时更新
<PaymentCountdown
createTime={order.createTime}
expirationTime={order.expirationTime}
payStatus={order.payStatus}
realTime={true}
showSeconds={true}
@@ -43,7 +43,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
```tsx
// 自定义超时时间12小时
<PaymentCountdown
createTime={order.createTime}
expirationTime={order.expirationTime}
payStatus={order.payStatus}
realTime={true}
timeoutHours={12}
@@ -55,7 +55,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
// 纯文本模式
<PaymentCountdown
createTime={order.createTime}
expirationTime={order.expirationTime}
payStatus={order.payStatus}
realTime={false}
mode="text"
@@ -67,6 +67,7 @@ import PaymentCountdown from '@/components/PaymentCountdown';
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| createTime | string | - | 订单创建时间 |
| expirationTime | string | - | 订单过期时间(推荐) |
| payStatus | boolean | false | 支付状态 |
| realTime | boolean | false | 是否实时更新 |
| timeoutHours | number | 24 | 超时小时数 |
@@ -102,12 +103,13 @@ import PaymentCountdown from '@/components/PaymentCountdown';
import { usePaymentCountdown, formatCountdownText } from '@/hooks/usePaymentCountdown';
const MyComponent = ({ order }) => {
const timeLeft = usePaymentCountdown(
order.createTime,
order.payStatus,
true, // 实时更新
24 // 24小时超时
);
const timeLeft = usePaymentCountdown({
expirationTime: order.expirationTime,
createTime: order.createTime, // expirationTime 缺失时回退
payStatus: order.payStatus,
realTime: true,
timeoutHours: 24
});
const countdownText = formatCountdownText(timeLeft, true);

View File

@@ -133,7 +133,6 @@
margin: 8px 0;
.countdown-text {
font-size: 14px;
font-weight: 600;
}
}

View File

@@ -11,6 +11,8 @@ import './PaymentCountdown.scss';
export interface PaymentCountdownProps {
/** 订单创建时间 */
createTime?: string;
/** 订单过期时间(推荐直接传后端返回的 expirationTime */
expirationTime?: string;
/** 支付状态 */
payStatus?: boolean;
/** 是否实时更新详情页用true列表页用false */
@@ -29,18 +31,25 @@ export interface PaymentCountdownProps {
const PaymentCountdown: React.FC<PaymentCountdownProps> = ({
createTime,
expirationTime,
payStatus = false,
realTime = false,
timeoutHours = 1,
timeoutHours = 24,
showSeconds = false,
className = '',
onExpired,
mode = 'badge'
}) => {
const timeLeft = usePaymentCountdown(createTime, payStatus, realTime, timeoutHours);
const timeLeft = usePaymentCountdown({
createTime,
expirationTime,
payStatus,
realTime,
timeoutHours
});
// 如果已支付或没有创建时间,不显示倒计时
if (payStatus || !createTime) {
// 如果已支付或没有可计算的截止时间,不显示倒计时
if (payStatus || (!expirationTime && !createTime)) {
return null;
}

View File

@@ -0,0 +1,89 @@
import React from 'react';
import { Button } from '@nutui/nutui-react-taro';
import {View} from '@tarojs/components'
import { Scan } from '@nutui/icons-react-taro';
import Taro from '@tarojs/taro';
import { useQRLogin } from '@/hooks/useQRLogin';
export interface QRLoginButtonProps {
/** 按钮类型 */
type?: 'primary' | 'success' | 'warning' | 'danger' | 'default';
/** 按钮大小 */
size?: 'large' | 'normal' | 'small';
/** 按钮文本 */
text?: string;
/** 是否显示图标 */
showIcon?: boolean;
/** 自定义样式类名 */
className?: string;
/** 点击成功回调 */
onSuccess?: (result: any) => void;
/** 点击失败回调 */
onError?: (error: string) => void;
/** 是否使用页面模式(跳转到专门页面) */
usePageMode?: boolean;
}
/**
* 扫码登录按钮组件
*/
const QRLoginButton: React.FC<QRLoginButtonProps> = ({
type = 'default',
size = 'small',
text = '扫码登录',
showIcon = true,
onSuccess,
onError,
usePageMode = false
}) => {
const { startScan, isLoading, canScan } = useQRLogin();
// 处理点击事件
const handleClick = async () => {
console.log('处理点击事件handleClick', usePageMode)
if (usePageMode) {
// 跳转到专门的扫码登录页面
if (canScan()) {
Taro.navigateTo({
url: '/passport/qr-login/index'
});
} else {
Taro.showToast({
title: '请先登录小程序',
icon: 'error'
});
}
return;
}
// 直接执行扫码登录
try {
await startScan();
// 成功回调会在Hook内部处理
} catch (error: any) {
onError?.(error.message || '扫码登录失败');
}
};
console.log(onSuccess,'onSuccess')
const disabled = !canScan() || isLoading;
return (
<Button
type={type}
size={size}
loading={isLoading}
disabled={disabled}
onClick={handleClick}
>
<View className="flex items-center justify-center">
{showIcon && !isLoading && (
<Scan className="mr-1" />
)}
{isLoading ? '扫码中...' : (disabled && !canScan() ? '请先登录' : text)}
</View>
</Button>
);
};
export default QRLoginButton;

View File

@@ -0,0 +1,182 @@
import React from 'react';
import { View, Text } from '@tarojs/components';
import { Button, Loading } from '@nutui/nutui-react-taro';
import { Scan, Success, Failure } from '@nutui/icons-react-taro';
import { useQRLogin, ScanLoginState } from '@/hooks/useQRLogin';
export interface QRLoginScannerProps {
/** 扫码成功回调 */
onSuccess?: (result: any) => void;
/** 扫码失败回调 */
onError?: (error: string) => void;
/** 自定义样式类名 */
className?: string;
/** 按钮文本 */
buttonText?: string;
/** 是否显示状态信息 */
showStatus?: boolean;
}
/**
* 扫码登录组件
*/
const QRLoginScanner: React.FC<QRLoginScannerProps> = ({
onSuccess,
onError,
className = '',
buttonText = '扫码登录',
showStatus = true
}) => {
const {
state,
error,
result,
isLoading,
startScan,
cancel,
reset,
canScan
} = useQRLogin();
// 处理扫码成功
React.useEffect(() => {
if (state === ScanLoginState.SUCCESS && result) {
onSuccess?.(result);
}
}, [state, result, onSuccess]);
// 处理扫码失败
React.useEffect(() => {
if (state === ScanLoginState.ERROR && error) {
onError?.(error);
}
}, [state, error, onError]);
// 获取状态显示内容
const getStatusContent = () => {
switch (state) {
case ScanLoginState.SCANNING:
return (
<View className="flex items-center justify-center text-blue-500">
<Loading className="mr-2" />
<Text>...</Text>
</View>
);
case ScanLoginState.CONFIRMING:
return (
<View className="flex items-center justify-center text-orange-500">
<Loading className="mr-2" />
<Text>...</Text>
</View>
);
case ScanLoginState.SUCCESS:
return (
<View className="flex items-center justify-center text-green-500">
<Success className="mr-2" />
<Text></Text>
</View>
);
case ScanLoginState.ERROR:
return (
<View className="flex items-center justify-center text-red-500">
<Failure className="mr-2" />
<Text>{error || '扫码登录失败'}</Text>
</View>
);
default:
return null;
}
};
// 获取按钮状态
const getButtonProps = () => {
const disabled = !canScan() || isLoading;
switch (state) {
case ScanLoginState.SCANNING:
case ScanLoginState.CONFIRMING:
return {
loading: true,
disabled: true,
text: state === ScanLoginState.SCANNING ? '扫码中...' : '确认中...',
onClick: cancel
};
case ScanLoginState.SUCCESS:
return {
loading: false,
disabled: false,
text: '重新扫码',
onClick: reset
};
case ScanLoginState.ERROR:
return {
loading: false,
disabled: false,
text: '重试',
onClick: startScan
};
default:
return {
loading: false,
disabled,
text: disabled ? '请先登录' : buttonText,
onClick: startScan
};
}
};
const buttonProps = getButtonProps();
return (
<View className={`qr-login-scanner ${className}`}>
{/* 扫码按钮 */}
<Button
type="primary"
size="large"
loading={buttonProps.loading}
disabled={buttonProps.disabled}
onClick={buttonProps.onClick}
className="w-full"
>
{!buttonProps.loading && (
<Scan className="mr-2" />
)}
{buttonProps.text}
</Button>
{/* 状态显示 */}
{showStatus && (
<View className="mt-4 text-center">
{getStatusContent()}
</View>
)}
{/* 成功结果显示 */}
{state === ScanLoginState.SUCCESS && result && (
<View className="mt-4 p-4 bg-green-50 rounded-lg">
<Text className="text-sm text-green-700">
{result.userInfo?.nickname || result.userInfo?.userId}
</Text>
</View>
)}
{/* 使用说明 */}
{state === ScanLoginState.IDLE && (
<View className="mt-4 text-center">
<Text className="text-xs text-gray-500">
</Text>
</View>
)}
</View>
);
};
export default QRLoginScanner;

View File

@@ -0,0 +1,272 @@
import React, { useState } from 'react';
import { View, Text } from '@tarojs/components';
import { Button, Popup, Loading } from '@nutui/nutui-react-taro';
import { Scan, Close, Success, Failure } from '@nutui/icons-react-taro';
import Taro from '@tarojs/taro';
import { parseQRContent, confirmQRLogin } from '@/api/passport/qr-login';
import { useUser } from '@/hooks/useUser';
export interface QRScanModalProps {
/** 是否显示弹窗 */
visible: boolean;
/** 关闭弹窗回调 */
onClose: () => void;
/** 扫码成功回调 */
onSuccess?: (result: any) => void;
/** 扫码失败回调 */
onError?: (error: string) => void;
/** 弹窗标题 */
title?: string;
/** 描述文本 */
description?: string;
/** 是否自动确认登录 */
autoConfirm?: boolean;
}
/**
* 二维码扫描弹窗组件(用于扫码登录)
*/
const QRScanModal: React.FC<QRScanModalProps> = ({
visible,
onClose,
onSuccess,
onError,
title = '扫描登录二维码',
description = '扫描网页端显示的登录二维码',
autoConfirm = true
}) => {
const { user } = useUser();
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState<'idle' | 'scanning' | 'confirming' | 'success' | 'error'>('idle');
const [errorMsg, setErrorMsg] = useState('');
// 开始扫码
const handleScan = async () => {
if (!user?.userId) {
onError?.('请先登录小程序');
return;
}
try {
setLoading(true);
setStatus('scanning');
setErrorMsg('');
// 扫码
const scanResult = await new Promise<string>((resolve, reject) => {
Taro.scanCode({
onlyFromCamera: true,
scanType: ['qrCode'],
success: (res) => {
if (res.result) {
resolve(res.result);
} else {
reject(new Error('扫码结果为空'));
}
},
fail: (err) => {
reject(new Error(err.errMsg || '扫码失败'));
}
});
});
// 解析二维码内容
const token = parseQRContent(scanResult);
if (!token) {
throw new Error('无效的登录二维码');
}
if (autoConfirm) {
// 自动确认登录
setStatus('confirming');
const result = await confirmQRLogin({
token,
userId: user.userId,
platform: 'wechat',
wechatInfo: {
nickname: user.nickname,
avatar: user.avatar
}
});
if (result.success) {
setStatus('success');
onSuccess?.(result);
// 显示成功提示
Taro.showToast({
title: '登录确认成功',
icon: 'success'
});
// 延迟关闭
setTimeout(() => {
onClose();
setStatus('idle');
}, 1500);
} else {
throw new Error(result.message || '登录确认失败');
}
} else {
// 只返回扫码结果
onSuccess?.(scanResult);
onClose();
setStatus('idle');
}
} catch (error: any) {
setStatus('error');
const errorMessage = error.message || '操作失败';
setErrorMsg(errorMessage);
onError?.(errorMessage);
} finally {
setLoading(false);
}
};
// 重试
const handleRetry = () => {
setStatus('idle');
setErrorMsg('');
handleScan();
};
// 关闭弹窗
const handleClose = () => {
setStatus('idle');
setErrorMsg('');
setLoading(false);
onClose();
};
// 获取状态显示内容
const getStatusContent = () => {
switch (status) {
case 'scanning':
return {
icon: <Loading className="text-blue-500" />,
title: '正在扫码...',
description: '请将二维码对准摄像头'
};
case 'confirming':
return {
icon: <Loading className="text-orange-500" />,
title: '正在确认登录...',
description: '请稍候,正在为您确认登录'
};
case 'success':
return {
icon: <Success size="32" className="text-green-500" />,
title: '登录确认成功',
description: '网页端将自动完成登录'
};
case 'error':
return {
icon: <Failure size="32" className="text-red-500" />,
title: '操作失败',
description: errorMsg || '请重试'
};
default:
return {
icon: <Scan size="32" className="text-blue-500" />,
title,
description
};
}
};
const statusContent = getStatusContent();
return (
<Popup
visible={visible}
position="center"
closeable={false}
onClose={handleClose}
style={{ width: '85%', borderRadius: '12px' }}
>
<View className="p-6 text-center relative">
{/* 关闭按钮 */}
{status !== 'scanning' && status !== 'confirming' && (
<View className="absolute top-4 right-4">
<Button
size="small"
type="default"
onClick={handleClose}
className="w-8 h-8 p-0"
>
<Close size="16" />
</Button>
</View>
)}
{/* 图标 */}
<View className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
{statusContent.icon}
</View>
{/* 标题 */}
<Text className="text-lg font-bold text-gray-800 mb-2 block">
{statusContent.title}
</Text>
{/* 描述 */}
<Text className="text-gray-600 mb-6 block">
{statusContent.description}
</Text>
{/* 操作按钮 */}
{status === 'idle' && (
<Button
type="primary"
size="large"
onClick={handleScan}
className="w-full"
disabled={!user?.userId}
>
<Scan className="mr-2" />
{user?.userId ? '开始扫码' : '请先登录'}
</Button>
)}
{status === 'error' && (
<View className="space-y-2">
<Button
type="primary"
size="large"
onClick={handleRetry}
className="w-full"
>
</Button>
<Button
type="default"
size="normal"
onClick={handleClose}
className="w-full"
>
</Button>
</View>
)}
{(status === 'scanning' || status === 'confirming') && (
<Button
type="default"
size="large"
onClick={handleClose}
className="w-full"
>
</Button>
)}
</View>
{loading}
</Popup>
);
};
export default QRScanModal;

View File

@@ -81,7 +81,7 @@ const SimpleQRCodeModal: React.FC<SimpleQRCodeModalProps> = ({
{qrContent ? (
<View className={'flex flex-col justify-center'}>
<img
src={`https://cms-api.websoft.top/api/qr-code/create-encrypted-qr-image?size=300x300&expireMinutes=60&businessType=gift&data=${encodeURIComponent(qrContent)}`}
src={`https://ysb-api.websoft.top/api/qr-code/create-encrypted-qr-image?size=300x300&expireMinutes=60&businessType=gift&data=${encodeURIComponent(qrContent)}`}
alt="二维码"
style={{width: '200px', height: '200px'}}
className="mx-auto"

View File

@@ -1,6 +1,6 @@
import { Tabbar } from '@nutui/nutui-react-taro'
import { Home, User } from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import { switchTab } from '@/utils/navigation'
function TabBar(){
return (
@@ -9,13 +9,13 @@ function TabBar(){
onSwitch={(index) => {
console.log(index)
if(index == 0){
Taro.switchTab({ url: '/pages/index/index' })
switchTab('index/index')
}
// if(index == 1){
// Taro.navigateTo({ url: '/pages/detail/detail' })
// goTo('detail/detail')
// }
if(index == 1){
Taro.switchTab({ url: '/pages/user/user' })
switchTab('user/user')
}
}}
>

View File

@@ -1,4 +1,6 @@
import React from 'react';
import { Button } from '@nutui/nutui-react-taro';
import { View } from '@tarojs/components';
import { Scan } from '@nutui/icons-react-taro';
import Taro from '@tarojs/taro';
import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
@@ -27,11 +29,15 @@ export interface UnifiedQRButtonProps {
* 支持登录和核销两种类型的二维码扫描
*/
const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
type = 'danger',
size = 'small',
text = '扫码',
showIcon = true,
onSuccess,
onError,
usePageMode = false
}) => {
const { startScan, canScan, result } = useUnifiedQRScan();
const { startScan, isLoading, canScan, state, result } = useUnifiedQRScan();
console.log(result,'useUnifiedQRScan>>result')
// 处理点击事件
const handleClick = async () => {
@@ -62,7 +68,7 @@ const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
setTimeout(() => {
Taro.showModal({
title: '核销成功',
content: '是否继续扫码核销其他礼品卡?',
content: '是否继续扫码核销其他水票/礼品卡?',
success: (res) => {
if (res.confirm) {
handleClick(); // 递归调用继续扫码
@@ -77,8 +83,43 @@ const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
}
};
const disabled = !canScan() || isLoading;
// 根据当前状态动态显示文本
const getButtonText = () => {
if (isLoading) {
switch (state) {
case 'scanning':
return '扫码中...';
case 'processing':
return '处理中...';
default:
return '扫码中...';
}
}
if (disabled && !canScan()) {
return '请先登录';
}
return text;
};
return (
<Scan className={'text-white'} onClick={handleClick} />
<Button
type={type}
size={size}
loading={isLoading}
disabled={disabled}
onClick={handleClick}
>
<View className="flex items-center justify-center">
{showIcon && !isLoading && (
<Scan className="mr-1" />
)}
{getButtonText()}
</View>
</Button>
);
};

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { View, Text } from '@tarojs/components';
import { View, Text, Image } from '@tarojs/components';
import { Button, Avatar } from '@nutui/nutui-react-taro';
import { useUser } from '@/hooks/useUser';
import navTo from '@/utils/common';

View File

@@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '客户报备',
navigationBarTitleText: '领劵中心',
navigationBarTextStyle: 'black'
})

368
src/coupon/index.tsx Normal file
View File

@@ -0,0 +1,368 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {
Empty,
ConfigProvider,
InfiniteLoading,
Loading,
PullToRefresh,
Tabs,
TabPane
} from '@nutui/nutui-react-taro'
import {View} from '@tarojs/components'
import {ShopCoupon} from "@/api/shop/shopCoupon/model";
import {pageShopCoupon} from "@/api/shop/shopCoupon";
import CouponList from "@/components/CouponList";
import CouponGuide from "@/components/CouponGuide";
import CouponFilter from "@/components/CouponFilter";
import {CouponCardProps} from "@/components/CouponCard";
import {takeCoupon} from "@/api/shop/shopUserCoupon";
const CouponReceiveCenter = () => {
const [list, setList] = useState<ShopCoupon[]>([])
const [loading, setLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [page, setPage] = useState(1)
const [activeTab, setActiveTab] = useState('0') // 0-全部 1-满减券 2-折扣券 3-免费券
const [showGuide, setShowGuide] = useState(false)
const [showFilter, setShowFilter] = useState(false)
const [filters, setFilters] = useState({
type: [] as number[],
minAmount: undefined as number | undefined,
sortBy: 'createTime' as 'createTime' | 'amount' | 'expireTime',
sortOrder: 'desc' as 'asc' | 'desc'
})
// 获取优惠券类型过滤条件
const getTypeFilter = () => {
switch (String(activeTab)) {
case '0': // 全部
return {}
case '1': // 满减券
return { type: 10 }
case '2': // 折扣券
return { type: 20 }
case '3': // 免费券
return { type: 30 }
default:
return {}
}
}
// 根据传入的值获取类型过滤条件
const getTypeFilterByValue = (value: string | number) => {
switch (String(value)) {
case '0': // 全部
return {}
case '1': // 满减券
return { type: 10 }
case '2': // 折扣券
return { type: 20 }
case '3': // 免费券
return { type: 30 }
default:
return {}
}
}
// 根据类型过滤条件加载优惠券
const loadCouponsByType = async (typeFilter: any) => {
setLoading(true)
try {
const currentPage = 1
// 获取可领取的优惠券(启用状态且未过期)
const res = await pageShopCoupon({
page: currentPage,
limit: 10,
keywords: '',
enabled: 1, // 启用状态
isExpire: 0, // 未过期
...typeFilter
})
console.log('API返回数据:', res)
if (res && res.list) {
setList(res.list)
setHasMore(res.list.length === 10)
setPage(2)
} else {
setList([])
setHasMore(false)
}
} catch (error) {
console.error('获取优惠券失败:', error)
Taro.showToast({
title: '获取优惠券失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}
const reload = async (isRefresh = false) => {
if (isRefresh) {
setPage(1)
setList([])
setHasMore(true)
}
setLoading(true)
try {
const currentPage = isRefresh ? 1 : page
const typeFilter = getTypeFilter()
console.log('reload - 当前activeTab:', activeTab, '类型过滤:', typeFilter)
// 获取可领取的优惠券(启用状态且未过期)
const res = await pageShopCoupon({
page: currentPage,
limit: 10,
keywords: '',
enabled: 1, // 启用状态
isExpire: 0, // 未过期
...typeFilter,
// 应用筛选条件
...(filters.type.length > 0 && { type: filters.type[0] }),
sortBy: filters.sortBy,
sortOrder: filters.sortOrder
})
console.log('reload - API返回数据:', res)
if (res && res.list) {
const newList = isRefresh ? res.list : [...list, ...res.list]
setList(newList)
// 判断是否还有更多数据
setHasMore(res.list.length === 10)
if (!isRefresh) {
setPage(currentPage + 1)
} else {
setPage(2)
}
} else {
setHasMore(false)
}
} catch (error) {
console.error('获取优惠券失败:', error)
Taro.showToast({
title: '获取优惠券失败',
icon: 'error'
});
} finally {
setLoading(false)
}
}
// 下拉刷新
const handleRefresh = async () => {
await reload(true)
}
// Tab切换
const handleTabChange = (value: string | number) => {
console.log('Tab切换到:', value)
setActiveTab(String(value))
setPage(1)
setList([])
setHasMore(true)
// 直接传递类型值,避免异步状态更新问题
const typeFilter = getTypeFilterByValue(value)
console.log('类型过滤条件:', typeFilter)
// 立即加载数据
loadCouponsByType(typeFilter)
}
// 转换优惠券数据为CouponCard组件所需格式
const transformCouponData = (coupon: ShopCoupon): CouponCardProps => {
let amount = 0
let type: 10 | 20 | 30 = 10
if (coupon.type === 10) { // 满减券
type = 10
amount = parseFloat(coupon.reducePrice || '0')
} else if (coupon.type === 20) { // 折扣券
type = 20
amount = coupon.discount || 0
} else if (coupon.type === 30) { // 免费券
type = 30
amount = 0
}
return {
id: coupon.id?.toString(),
amount,
type,
status: 0, // 可领取状态
minAmount: parseFloat(coupon.minPrice || '0'),
title: coupon.name || '优惠券',
description: coupon.description,
startTime: coupon.startTime,
endTime: coupon.endTime,
showReceiveBtn: true, // 显示领取按钮
onReceive: () => handleReceiveCoupon(coupon),
theme: getThemeByType(coupon.type)
}
}
// 根据优惠券类型获取主题色
const getThemeByType = (type?: number): 'red' | 'orange' | 'blue' | 'purple' | 'green' => {
switch (type) {
case 10: return 'red' // 满减券-红色
case 20: return 'orange' // 折扣券-橙色
case 30: return 'green' // 免费券-绿色
default: return 'blue'
}
}
// 领取优惠券
const handleReceiveCoupon = async (coupon: ShopCoupon) => {
try {
// 检查是否已登录
const userId = Taro.getStorageSync('UserId')
if (!userId) {
Taro.showToast({
title: '请先登录',
icon: 'error'
})
return
}
// 调用领取接口
await takeCoupon({
couponId: coupon.id!,
userId: userId
})
Taro.showToast({
title: '领取成功',
icon: 'success'
})
// 刷新列表
reload(true)
} catch (error: any) {
console.error('领取优惠券失败:', error)
Taro.showToast({
title: error.message || '领取失败',
icon: 'none'
})
}
}
// 筛选条件变更
const handleFiltersChange = (newFilters: any) => {
setFilters(newFilters)
reload(true)
}
// 查看我的优惠券
const handleViewMyCoupons = () => {
Taro.navigateTo({
url: '/user/coupon/index'
})
}
// 加载更多
const loadMore = async () => {
if (!loading && hasMore) {
await reload(false) // 不刷新,追加数据
}
}
useDidShow(() => {
reload(true).then()
});
return (
<ConfigProvider className="h-screen flex flex-col">
{/* Tab切换 */}
<View className="bg-white hidden">
<Tabs value={activeTab} onChange={handleTabChange}>
<TabPane title="全部" value="0">
</TabPane>
<TabPane title="满减券" value="1">
</TabPane>
<TabPane title="折扣券" value="2">
</TabPane>
<TabPane title="免费券" value="3">
</TabPane>
</Tabs>
</View>
{/* 优惠券列表 - 占满剩余空间 */}
<View className="flex-1 overflow-hidden">
<PullToRefresh
onRefresh={handleRefresh}
headHeight={60}
>
<View
style={{
height: 'calc(100vh - 60px)',
overflowY: 'auto',
paddingTop: '24px',
paddingBottom: '32px'
}}
id="coupon-scroll"
>
{list.length === 0 && !loading ? (
<View className="flex flex-col justify-center items-center h-full">
<Empty
description="暂无可领取的优惠券"
style={{backgroundColor: 'transparent'}}
actions={[
{
text: '查看我的优惠券',
onClick: handleViewMyCoupons
}
]}
/>
</View>
) : (
<InfiniteLoading
target="coupon-scroll"
hasMore={hasMore}
onLoadMore={loadMore}
loadingText={
<View className="flex justify-center items-center py-4">
<Loading />
<View className="ml-2">...</View>
</View>
}
loadMoreText={
<View className="text-center py-4 text-gray-500">
{list.length === 0 ? "暂无数据" : "没有更多了"}
</View>
}
>
<CouponList
coupons={list.map(transformCouponData)}
showEmpty={false}
/>
</InfiniteLoading>
)}
</View>
</PullToRefresh>
</View>
{/* 使用指南弹窗 */}
<CouponGuide
visible={showGuide}
onClose={() => setShowGuide(false)}
/>
{/* 筛选弹窗 */}
<CouponFilter
visible={showFilter}
filters={filters}
onFiltersChange={handleFiltersChange}
onClose={() => setShowFilter(false)}
/>
</ConfigProvider>
);
};
export default CouponReceiveCenter;

View File

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

View File

@@ -9,9 +9,10 @@ import {TenantId} from "@/config/app";
import {updateUser} from "@/api/system/user";
import {User} from "@/api/system/user/model";
import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite";
import {addShopDealerUser, updateShopDealerUserByUserId} from "@/api/shop/shopDealerUser";
import {listUserRole, updateUserRole} from "@/api/system/userRole";
import {addShopDealerCapital} from "@/api/shop/shopDealerCapital";
import {addShopDealerUser} from "@/api/shop/shopDealerUser";
import {addUserRole, listUserRole, updateUserRole} from "@/api/system/userRole";
import { listRoles } from "@/api/system/role";
import type { UserRole } from "@/api/system/userRole/model";
// 类型定义
interface ChooseAvatarEvent {
@@ -27,9 +28,8 @@ interface InputEvent {
}
const AddUserAddress = () => {
const {user, loginUser} = useUser()
const {user, loginUser, fetchUserInfo} = useUser()
const [loading, setLoading] = useState<boolean>(true)
const [submitting, setSubmitting] = useState<boolean>(false)
const [FormData, setFormData] = useState<User>()
const formRef = useRef<any>(null)
@@ -129,14 +129,7 @@ const AddUserAddress = () => {
}
// 提交表单
const submitSucceed = async (values: any) => {
// 防止重复提交
if (submitting) {
console.log('正在提交中,请勿重复点击')
return
}
setSubmitting(true)
const submitSucceed = async (values: User) => {
try {
// 验证必填字段
if (!values.phone && !FormData?.phone) {
@@ -151,8 +144,8 @@ const AddUserAddress = () => {
const nickname = values.realName || FormData?.nickname || '';
if (!nickname || nickname.trim() === '') {
Taro.showToast({
title: '请填写昵称',
icon: 'error'
title: '请上传头像和填写昵称',
icon: 'none'
});
return;
}
@@ -185,12 +178,27 @@ const AddUserAddress = () => {
}
console.log(values,FormData)
const roles = await listUserRole({userId: user?.userId})
console.log(roles, 'roles...')
if (!user?.userId) {
Taro.showToast({
title: '用户信息缺失,请先登录',
icon: 'error'
});
return;
}
let roles: UserRole[] = [];
try {
roles = await listUserRole({userId: user.userId})
console.log(roles, 'roles...')
} catch (e) {
// 新用户/权限限制时可能查不到角色列表,不影响基础注册流程
console.warn('查询用户角色失败,将尝试直接写入默认角色:', e)
roles = []
}
// 准备提交的数据
await updateUser({
userId: user?.userId,
userId: user.userId,
nickname: values.realName || FormData?.nickname,
phone: values.phone || FormData?.phone,
avatar: values.avatar || FormData?.avatar,
@@ -198,33 +206,57 @@ const AddUserAddress = () => {
});
await addShopDealerUser({
userId: user?.userId,
userId: user.userId,
realName: values.realName || FormData?.nickname,
mobile: values.phone || FormData?.phone,
refereeId: values.refereeId || FormData?.refereeId
refereeId: Number(values.refereeId) || Number(FormData?.refereeId)
})
if (roles.length > 0) {
await updateUserRole({
...roles[0],
roleId: 1848
})
// 通知其他页面(如“我的”页、分销中心页)刷新经销商信息
Taro.eventCenter.trigger('dealerUser:changed')
// 角色为空时这里会导致“注册成功但没有角色”,这里做一次兜底写入默认 user 角色
try {
// 1) 先尝试通过 roleCode=user 查询角色ID避免硬编码
// 2) 取不到就回退到旧的默认ID1848
let userRoleId: number | undefined;
try {
// 注意:当前 request.get 的封装不支持 axios 风格的 { params: ... }
// 某些自动生成的 API 可能无法按参数过滤;这里直接取全量再本地查找更稳。
const roleList = await listRoles();
userRoleId = roleList?.find(r => r.roleCode === 'user')?.roleId;
} catch (_) {
// ignore
}
if (!userRoleId) userRoleId = 1848;
const baseRolePayload = {
userId: user.userId,
tenantId: Number(TenantId),
roleId: userRoleId
};
// 后端若已创建 user-role 记录则更新否则尝试“无id更新”触发创建多数实现会 upsert
if (roles.length > 0) {
await updateUserRole({
...roles[0],
roleId: userRoleId
});
} else {
try {
await addUserRole(baseRolePayload);
} catch (_) {
// 兼容后端仅支持 PUT upsert 的情况
await updateUserRole(baseRolePayload);
}
}
// 刷新一次用户信息,确保 roles 写回本地缓存,避免“我的”页显示为空/不一致
await fetchUserInfo();
} catch (e) {
console.warn('写入默认角色失败(不影响注册成功):', e)
}
// 获得50元奖励
await updateShopDealerUserByUserId({
userId: user?.userId,
money: '50',
})
// 保存明细
await addShopDealerCapital({
userId: user?.userId,
flowType: 50,
money: '50',
toUserId: user?.refereeId,
comments: '新人注册奖励'
})
Taro.showToast({
title: `注册成功`,
@@ -232,17 +264,12 @@ const AddUserAddress = () => {
});
setTimeout(() => {
Taro.navigateBack();
// “我的”是 tabBar 页面,注册完成后直接切到“我的”
Taro.switchTab({ url: '/pages/user/user' });
}, 1000);
} catch (error) {
console.error('验证邀请人失败:', error);
Taro.showToast({
title: '注册失败,请重试',
icon: 'error'
})
} finally {
setSubmitting(false)
}
}
@@ -411,9 +438,9 @@ const AddUserAddress = () => {
>
<View className={'bg-gray-100 h-3'}></View>
<CellGroup style={{padding: '4px 0'}}>
<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>
<Input placeholder="邀请人ID" disabled={true}/>
</Form.Item>
{/*<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>*/}
{/* <Input placeholder="邀请人ID" disabled={false}/>*/}
{/*</Form.Item>*/}
<Form.Item name="phone" label="手机号" initialValue={FormData?.phone} required>
<View className="flex items-center justify-between">
<Input
@@ -451,9 +478,8 @@ const AddUserAddress = () => {
{/* 底部浮动按钮 */}
<FixedButton
icon={<Edit/>}
text={submitting ? '注册中...' : '立即注册'}
text={'立即注册'}
onClick={handleFixedButtonClick}
disabled={submitting}
/>
</>

View File

@@ -1,156 +0,0 @@
import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
import {Loading, CellGroup, Input, Form} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {
getShopDealerBank,
listShopDealerBank,
updateShopDealerBank,
addShopDealerBank
} from "@/api/shop/shopDealerBank";
import FixedButton from "@/components/FixedButton";
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
import {myUserVerify} from "@/api/system/userVerify";
const AddUserAddress = () => {
const {params} = useRouter();
const [loading, setLoading] = useState<boolean>(true)
const [FormData, setFormData] = useState<ShopDealerBank>()
const formRef = useRef<any>(null)
// 判断是编辑还是新增模式
const isEditMode = !!params.id
const bankId = params.id ? Number(params.id) : undefined
const reload = async () => {
// 如果是编辑模式,加载地址数据
if (isEditMode && bankId) {
try {
const bank = await getShopDealerBank(bankId)
setFormData(bank)
} catch (error) {
console.error('加载地址失败:', error)
Taro.showToast({
title: '加载地址失败',
icon: 'error'
});
}
}
}
// 提交表单
const submitSucceed = async (values: any) => {
console.log('.>>>>>>,....',values)
const verify = await myUserVerify({userId: Taro.getStorageSync('UserId')})
if(verify?.realName !== values.bankAccount){
Taro.showToast({
title: '收款人姓名与实名认证信息不一致!',
icon: 'none'
});
return false;
}
try {
// 准备提交的数据
const submitData = {
...values,
isDefault: true // 新增或编辑的地址都设为默认地址
};
console.log('提交数据:', submitData)
// 如果是编辑模式添加id
if (isEditMode && bankId) {
submitData.id = bankId;
}
// 先处理默认地址逻辑
const defaultAddress = await listShopDealerBank({isDefault: true});
if (defaultAddress && defaultAddress.length > 0) {
// 如果当前编辑的不是默认地址,或者是新增地址,需要取消其他默认地址
if (!isEditMode || (isEditMode && defaultAddress[0].id !== bankId)) {
await updateShopDealerBank({
...defaultAddress[0],
isDefault: false
});
}
}
// 执行新增或更新操作
if (isEditMode) {
await updateShopDealerBank(submitData);
} else {
await addShopDealerBank(submitData);
}
Taro.showToast({
title: `${isEditMode ? '更新' : '保存'}成功`,
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack();
}, 1000);
} catch (error) {
console.error('保存失败:', error);
Taro.showToast({
title: `${isEditMode ? '更新' : '保存'}失败`,
icon: 'error'
});
}
}
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
useEffect(() => {
// 动态设置页面标题
Taro.setNavigationBarTitle({
title: isEditMode ? '编辑银行卡' : '添加银行卡'
});
reload().then(() => {
setLoading(false)
})
}, [isEditMode]);
if (loading) {
return <Loading className={'px-2'}></Loading>
}
return (
<>
<Form
ref={formRef}
divider
initialValues={FormData}
labelPosition="left"
onFinish={(values) => submitSucceed(values)}
onFinishFailed={(errors) => submitFailed(errors)}
>
<CellGroup style={{padding: '4px 0'}}>
<Form.Item name="bankName" label="开户行名称" initialValue={FormData?.bankName} required>
<Input placeholder="开户行名称"/>
</Form.Item>
<Form.Item name="bankAccount" label="银行开户名" initialValue={FormData?.bankAccount} required>
<Input placeholder="银行开户名"/>
</Form.Item>
<Form.Item name="bankCard" label="银行卡号" initialValue={FormData?.bankCard} required>
<Input placeholder="银行卡号"/>
</Form.Item>
</CellGroup>
</Form>
{/* 底部浮动按钮 */}
<FixedButton text={isEditMode ? '更新地址' : '保存并使用'} onClick={() => formRef.current?.submit()}/>
</>
);
};
export default AddUserAddress;

View File

@@ -1,132 +0,0 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, Space, Empty, ConfigProvider} from '@nutui/nutui-react-taro'
import {CheckNormal, Checked} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
import {listShopDealerBank, removeShopDealerBank, updateShopDealerBank} from "@/api/shop/shopDealerBank";
import FixedButton from "@/components/FixedButton";
const DealerBank = () => {
const [list, setList] = useState<ShopDealerBank[]>([])
const [bank, setAddress] = useState<ShopDealerBank>()
const reload = () => {
listShopDealerBank({})
.then(data => {
setList(data || [])
// 默认地址
setAddress(data.find(item => item.isDefault))
})
.catch(() => {
Taro.showToast({
title: '获取地址失败',
icon: 'error'
});
})
}
const onDefault = async (item: ShopDealerBank) => {
if (bank) {
await updateShopDealerBank({
...bank,
isDefault: false
})
}
await updateShopDealerBank({
id: item.id,
isDefault: true
})
Taro.showToast({
title: '设置成功',
icon: 'success'
});
reload();
}
const onDel = async (id?: number) => {
await removeShopDealerBank(id)
Taro.showToast({
title: '删除成功',
icon: 'success'
});
reload();
}
const selectAddress = async (item: ShopDealerBank) => {
if (bank) {
await updateShopDealerBank({
...bank,
isDefault: false
})
}
await updateShopDealerBank({
id: item.id,
isDefault: true
})
setTimeout(() => {
Taro.navigateBack()
}, 500)
}
useDidShow(() => {
reload()
});
if (list.length == 0) {
return (
<ConfigProvider>
<div className={'h-full flex flex-col justify-center items-center'} style={{
height: 'calc(100vh - 300px)',
}}>
<Empty
style={{
backgroundColor: 'transparent'
}}
description="您还没有银行卡哦"
/>
<Space>
<Button onClick={() => Taro.navigateTo({url: '/dealer/bank/add'})}></Button>
</Space>
</div>
</ConfigProvider>
)
}
return (
<View className={'p-3'}>
{list.map((item, _) => (
<Cell.Group>
<Cell className={'flex flex-col gap-1'} extra={item.bankAccount} onClick={() => selectAddress(item)}>
<View>
<View className={'font-medium text-sm'}>{item.bankName}</View>
</View>
<View className={'text-xs'}>
{item.bankCard} {item.bankAccount}
</View>
</Cell>
<Cell
align="center"
title={
<View className={'flex items-center gap-1'} onClick={() => onDefault(item)}>
{item.isDefault ? <Checked className={'text-green-600'} size={16}/> : <CheckNormal size={16}/>}
<View className={'text-gray-400'}></View>
</View>
}
extra={
<>
<View className={'text-gray-400'} onClick={() => onDel(item.id)}>
</View>
</>
}
/>
</Cell.Group>
))}
{/* 底部浮动按钮 */}
<FixedButton text={'新增银行卡'} onClick={() => Taro.navigateTo({url: '/dealer/bank/add'})} />
</View>
);
};
export default DealerBank;

View File

@@ -1,79 +0,0 @@
import {useState, useEffect} from 'react'
import {View, Text} from '@tarojs/components'
import {Empty, Loading} from '@nutui/nutui-react-taro'
import {useRouter} from '@tarojs/taro'
import {getShopDealerCapital} from '@/api/shop/shopDealerCapital'
import type {ShopDealerCapital} from '@/api/shop/shopDealerCapital/model'
const DealerCapitalDetail = () => {
const {params} = useRouter();
const [loading, setLoading] = useState<boolean>(false)
const [item, setItem] = useState<ShopDealerCapital>()
// 获取订单数据
const reload = async () => {
const data = await getShopDealerCapital(Number(params.id))
setItem(data)
}
const getFlowType = (index?: number) => {
if (index === 10) return '电费收益'
if (index === 20) return '提现支出'
if (index === 30) return '转账支出'
if (index === 40) return '转账收入'
if (index === 50) return '新注册奖励'
return 'warning'
}
// 初始化加载数据
useEffect(() => {
reload().then(() => {
setLoading(true)
})
}, [])
return (
<View className="min-h-screen bg-gray-50">
<View className="p-4">
{loading && !item ? (
<View className="text-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : item ? (
<View key={item.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex flex-col justify-center items-center py-8">
<Text className="text-lg text-gray-300">
{getFlowType(item.flowType)}
</Text>
<View className="text-4xl mt-1 font-semibold flex justify-start">
<Text className={'subscript text-xl mt-1'}></Text>
<Text className={'text-4xl'}>{Number(item.money).toFixed(2)}</Text>
</View>
</View>
<View className="flex flex-col justify-between mb-1">
<Text className="text-sm my-1 text-gray-500">
{item.comments}
</Text>
{item.orderNo && (
<Text className="text-sm my-1 text-gray-500">
{item.orderNo}
</Text>
)}
<Text className="text-sm my-1 text-gray-500">
{item.createTime}
</Text>
</View>
</View>
) : (
<Empty description="账单不存在" style={{
backgroundColor: 'transparent'
}}/>
)}
</View>
</View>
)
}
export default DealerCapitalDetail

Some files were not shown because too many files have changed in this diff Show More