remove(feature): 删除经销商申请、用户地址管理和聊天消息功能模块

- 移除经销商申请相关页面配置和业务逻辑代码
- 删除用户地址管理功能的所有配置文件和实现组件
- 清理聊天消息发送功能的相关页面配置和业务代码
- 移除相关的API调用和数据模型引用
- 删除页面导航配置和相关的工具函数引用
This commit is contained in:
2026-03-18 00:15:13 +08:00
parent 3cf8f40926
commit c3b29d4d76
218 changed files with 33 additions and 33197 deletions

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '商品文章管理',
navigationBarTextStyle: 'black'
})

View File

@@ -1,271 +0,0 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, CellGroup, Empty, ConfigProvider, SearchBar, Tag, InfiniteLoading, Loading, PullToRefresh} from '@nutui/nutui-react-taro'
import {Edit, Del, Eye} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {CmsArticle} from "@/api/cms/cmsArticle/model";
import {pageCmsArticle, removeCmsArticle} from "@/api/cms/cmsArticle";
import FixedButton from "@/components/FixedButton";
import dayjs from "dayjs";
const ArticleArticleManage = () => {
const [list, setList] = useState<CmsArticle[]>([])
const [loading, setLoading] = useState(false)
// const [refreshing, setRefreshing] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [searchValue, setSearchValue] = useState('')
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
const reload = async (isRefresh = false) => {
if (isRefresh) {
setPage(1)
setList([])
setHasMore(true)
}
setLoading(true)
try {
const currentPage = isRefresh ? 1 : page
const res = await pageCmsArticle({
page: currentPage,
limit: 10,
keywords: searchValue
})
if (res && res.list) {
const newList = isRefresh ? res.list : [...list, ...res.list]
setList(newList)
setTotal(res.count || 0)
// 判断是否还有更多数据
setHasMore(res.list.length === 10) // 如果返回的数据等于limit说明可能还有更多
if (!isRefresh) {
setPage(currentPage + 1)
} else {
setPage(2) // 刷新后下一页是第2页
}
} else {
setHasMore(false)
setTotal(0)
}
} catch (error) {
console.error('获取文章失败:', error)
Taro.showToast({
title: '获取文章失败',
icon: 'error'
});
} finally {
setLoading(false)
}
}
// 搜索功能
const handleSearch = (value: string) => {
setSearchValue(value)
reload(true)
}
// 下拉刷新
const handleRefresh = async () => {
// setRefreshing(true)
await reload(true)
// setRefreshing(false)
}
// 删除文章
const handleDelete = async (id?: number) => {
Taro.showModal({
title: '确认删除',
content: '确定要删除这篇文章吗?',
success: async (res) => {
if (res.confirm) {
try {
await removeCmsArticle(id)
Taro.showToast({
title: '删除成功',
icon: 'success'
});
reload(true);
} catch (error) {
Taro.showToast({
title: '删除失败',
icon: 'error'
});
}
}
}
});
}
// 编辑文章
const handleEdit = (item: CmsArticle) => {
Taro.navigateTo({
url: `/shop/shopArticle/add?id=${item.articleId}`
});
}
// 查看文章详情
const handleView = (item: CmsArticle) => {
// 这里可以跳转到文章详情页面
Taro.navigateTo({
url: `/cms/detail/index?id=${item.articleId}`
})
}
// 获取状态标签
const getStatusTag = (status?: number) => {
switch (status) {
case 0:
return <Tag type="success"></Tag>
case 1:
return <Tag type="warning"></Tag>
case 2:
return <Tag type="danger"></Tag>
case 3:
return <Tag type="danger"></Tag>
default:
return <Tag></Tag>
}
}
// 加载更多
const loadMore = async () => {
if (!loading && hasMore) {
await reload(false) // 不刷新,追加数据
}
}
useDidShow(() => {
reload(true).then()
});
return (
<ConfigProvider>
{/* 搜索栏 */}
<View className="py-2">
<SearchBar
placeholder="搜索关键词"
value={searchValue}
onChange={setSearchValue}
onSearch={handleSearch}
/>
</View>
{/* 统计信息 */}
{total > 0 && (
<View className="px-4 py-2 text-sm text-gray-500">
{total}
</View>
)}
{/* 文章列表 */}
<PullToRefresh
onRefresh={handleRefresh}
headHeight={60}
>
<View className="px-4" style={{ height: 'calc(100vh - 160px)', overflowY: 'auto' }} id="article-scroll">
{list.length === 0 && !loading ? (
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 200px)'}}>
<Empty
description="暂无文章数据"
style={{backgroundColor: 'transparent'}}
/>
</View>
) : (
<InfiniteLoading
target="article-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>
}
>
{list.map((item, index) => (
<CellGroup key={item.articleId || index} className="mb-4">
<Cell>
<View className="flex flex-col gap-3 w-full">
{/* 文章标题和状态 */}
<View className="flex justify-between items-start">
<View className="flex-1 pr-2">
<View className="text-lg font-bold text-gray-900 line-clamp-2">
{item.title}
</View>
</View>
{getStatusTag(item.status)}
</View>
{/* 文章概述 */}
{item.overview && (
<View className="text-sm text-gray-600 line-clamp-2">
{item.overview}
</View>
)}
{/* 文章信息 */}
<View className="flex justify-between items-center text-xs text-gray-500">
<View className="flex items-center gap-4">
<View>: {item.actualViews || 0}</View>
{item.price && <View>: ¥{item.price}</View>}
<View>: {dayjs(item.createTime).format('MM-DD HH:mm')}</View>
</View>
</View>
{/* 操作按钮 */}
<View className="flex justify-end gap-2 pt-2 border-t border-gray-100">
<Button
size="small"
fill="outline"
icon={<Eye/>}
onClick={() => handleView(item)}
>
</Button>
<Button
size="small"
fill="outline"
icon={<Edit/>}
onClick={() => handleEdit(item)}
>
</Button>
<Button
size="small"
type="danger"
fill="outline"
icon={<Del/>}
onClick={() => handleDelete(item.articleId)}
>
</Button>
</View>
</View>
</Cell>
</CellGroup>
))}
</InfiniteLoading>
)}
</View>
</PullToRefresh>
{/* 底部浮动按钮 */}
<FixedButton
text="发布文章"
icon={<Edit />}
onClick={() => Taro.navigateTo({url: '/shop/shopArticle/add'})}
/>
</ConfigProvider>
);
};
export default ArticleArticleManage;

View File

@@ -1,244 +0,0 @@
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)
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)
// })
}
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'}>*/}
{/* <span className={'text-sm text-gray-500'}>积分</span>*/}
{/* <span className={'text-xl'}>{pointsCount}</span>*/}
{/*</div>*/}
</div>
</div>
</div>
</div>
)
}
export default UserCard;

View File

@@ -1,186 +0,0 @@
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

@@ -1,102 +0,0 @@
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

@@ -1,69 +0,0 @@
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,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '管理中心'
})

View File

@@ -1,35 +0,0 @@
import {useEffect} from 'react'
import {useUser} from "@/hooks/useUser";
import {Empty} from '@nutui/nutui-react-taro';
import {Text} from '@tarojs/components';
function Admin() {
const {
isAdmin
} = useUser();
useEffect(() => {
}, []);
if (!isAdmin()) {
return (
<Empty
description="您不是管理员"
imageSize={80}
style={{
backgroundColor: 'transparent',
height: 'calc(100vh - 200px)'
}}
>
</Empty>
);
}
return (
<>
<Text>...</Text>
</>
)
}
export default Admin

View File

@@ -1,8 +1,6 @@
export default {
pages: [
'pages/index/index',
'pages/cart/cart',
'pages/find/find',
'pages/user/user'
],
"subpackages": [
@@ -27,96 +25,14 @@ export default {
"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",
"userVerify/index",
"address/index",
"address/add",
"address/wxAddress",
"help/index",
"about/index",
"wallet/wallet",
"coupon/index",
"coupon/receive",
"coupon/detail",
"points/points",
"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",
"chat/message/detail"
]
},
{
"root": "dealer",
"pages": [
"index",
"apply/add",
"withdraw/index",
"orders/index",
"capital/index",
"team/index",
"qrcode/index",
"invite-stats/index",
"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": "admin",
"pages": [
"index",
"article/index",
"about/index"
]
},
{
@@ -156,12 +72,6 @@ export default {
selectedIconPath: "assets/tabbar/home-active.png",
text: "首页",
},
{
pagePath: "pages/find/find",
iconPath: "assets/tabbar/shop.png",
selectedIconPath: "assets/tabbar/shop-active.png",
text: "网点",
},
{
pagePath: "pages/user/user",
iconPath: "assets/tabbar/user.png",

View File

@@ -45,7 +45,7 @@ function Category() {
useShareAppMessage(() => {
return {
title: `${nav?.categoryName}_易赊宝`,
path: `/shop/category/index?id=${categoryId}`,
path: `/cms/category/index?id=${categoryId}`,
success: function () {
console.log('分享成功');
},

View File

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

View File

@@ -1,368 +0,0 @@
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 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '注册成为会员',
navigationBarTextStyle: 'black'
})

View File

@@ -1,489 +0,0 @@
import {useEffect, useState, useRef} from "react";
import {Loading, CellGroup, Input, Form, Avatar, Button, Space} from '@nutui/nutui-react-taro'
import {Edit} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import FixedButton from "@/components/FixedButton";
import {useUser} from "@/hooks/useUser";
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} 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 {
detail: {
avatarUrl: string;
};
}
interface InputEvent {
detail: {
value: string;
};
}
const AddUserAddress = () => {
const {user, loginUser, fetchUserInfo} = useUser()
const [loading, setLoading] = useState<boolean>(true)
const [FormData, setFormData] = useState<User>()
const formRef = useRef<any>(null)
const reload = async () => {
const inviteParams = getStoredInviteParams()
if (inviteParams?.inviter) {
setFormData({
...user,
refereeId: Number(inviteParams.inviter),
// 清空昵称,强制用户手动输入
nickname: '',
})
} else {
// 如果没有邀请参数,也要确保昵称为空
setFormData({
...user,
nickname: '',
})
}
}
const uploadAvatar = ({detail}: ChooseAvatarEvent) => {
// 先更新本地显示的头像(临时显示)
const tempFormData = {
...FormData,
avatar: `${detail.avatarUrl}`,
}
setFormData(tempFormData)
Taro.uploadFile({
url: 'https://server.websoft.top/api/oss/upload',
filePath: detail.avatarUrl,
name: 'file',
header: {
'content-type': 'application/json',
TenantId
},
success: async (res) => {
const data = JSON.parse(res.data);
if (data.code === 0) {
const finalAvatarUrl = `${data.data.thumbnail}`
try {
// 使用 useUser hook 的 updateUser 方法更新头像
await updateUser({
avatar: finalAvatarUrl
})
Taro.showToast({
title: '头像上传成功',
icon: 'success',
duration: 1500
})
} catch (error) {
console.error('更新用户头像失败:', error)
}
// 无论用户信息更新是否成功都要更新本地FormData
const finalFormData = {
...tempFormData,
avatar: finalAvatarUrl
}
setFormData(finalFormData)
// 同步更新表单字段
if (formRef.current) {
formRef.current.setFieldsValue({
avatar: finalAvatarUrl
})
}
} else {
// 上传失败,恢复原来的头像
setFormData({
...FormData,
avatar: user?.avatar || ''
})
Taro.showToast({
title: '上传失败',
icon: 'error'
})
}
},
fail: (error) => {
console.error('上传头像失败:', error)
Taro.showToast({
title: '上传失败',
icon: 'error'
})
// 恢复原来的头像
setFormData({
...FormData,
avatar: user?.avatar || ''
})
}
})
}
// 提交表单
const submitSucceed = async (values: User) => {
try {
// 验证必填字段
if (!values.phone && !FormData?.phone) {
Taro.showToast({
title: '请先获取手机号',
icon: 'error'
});
return;
}
// 验证昵称:必须填写且不能是默认的微信昵称
const nickname = values.realName || FormData?.nickname || '';
if (!nickname || nickname.trim() === '') {
Taro.showToast({
title: '请上传头像和填写昵称',
icon: 'none'
});
return;
}
// 检查是否为默认的微信昵称(常见的默认昵称)
const defaultNicknames = ['微信用户', 'WeChat User', '微信昵称'];
if (defaultNicknames.includes(nickname.trim())) {
Taro.showToast({
title: '请填写真实昵称,不能使用默认昵称',
icon: 'error'
});
return;
}
// 验证昵称长度
if (nickname.trim().length < 2) {
Taro.showToast({
title: '昵称至少需要2个字符',
icon: 'error'
});
return;
}
if (!values.avatar && !FormData?.avatar) {
Taro.showToast({
title: '请上传头像',
icon: 'error'
});
return;
}
console.log(values,FormData)
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,
nickname: values.realName || FormData?.nickname,
phone: values.phone || FormData?.phone,
avatar: values.avatar || FormData?.avatar,
refereeId: values.refereeId || FormData?.refereeId
});
await addShopDealerUser({
userId: user.userId,
realName: values.realName || FormData?.nickname,
mobile: values.phone || FormData?.phone,
refereeId: Number(values.refereeId) || Number(FormData?.refereeId)
})
// 通知其他页面(如“我的”页、分销中心页)刷新经销商信息
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)
}
Taro.showToast({
title: `注册成功`,
icon: 'success'
});
setTimeout(() => {
// “我的”是 tabBar 页面,注册完成后直接切到“我的”
Taro.switchTab({ url: '/pages/user/user' });
}, 1000);
} catch (error) {
console.error('验证邀请人失败:', error);
}
}
// 获取微信昵称
const getWxNickname = (nickname: string) => {
// 更新表单数据
const updatedFormData = {
...FormData,
nickname: nickname
}
setFormData(updatedFormData);
// 同步更新表单字段
if (formRef.current) {
formRef.current.setFieldsValue({
realName: nickname
})
}
}
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
const {code, encryptedData, iv} = detail
Taro.login({
success: (loginRes) => {
if (code) {
Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
authCode: loginRes.code,
code,
encryptedData,
iv,
notVerifyPhone: true,
refereeId: 0,
sceneType: 'save_referee',
tenantId: TenantId
},
header: {
'content-type': 'application/json',
TenantId
},
success: async function (res) {
if (res.data.code == 1) {
Taro.showToast({
title: res.data.message,
icon: 'error',
duration: 2000
})
return false;
}
// 登录成功
const token = res.data.data.access_token;
const userData = res.data.data.user;
console.log(userData, 'userData...')
// 使用useUser Hook的loginUser方法更新状态
loginUser(token, userData);
if (userData.phone) {
console.log('手机号已获取', userData.phone)
const updatedFormData = {
...FormData,
phone: userData.phone,
// 不自动填充微信昵称,保持用户已输入的昵称
nickname: FormData?.nickname || '',
// 只在没有头像时才使用微信头像
avatar: FormData?.avatar || userData.avatar
}
setFormData(updatedFormData)
// 更新表单字段值
if (formRef.current) {
formRef.current.setFieldsValue({
phone: userData.phone,
// 不覆盖用户已输入的昵称
realName: FormData?.nickname || '',
avatar: FormData?.avatar || userData.avatar
})
}
Taro.showToast({
title: '手机号获取成功',
icon: 'success',
duration: 1500
})
}
// 处理邀请关系
if (userData?.userId) {
try {
const inviteSuccess = await handleInviteRelation(userData.userId)
if (inviteSuccess) {
Taro.showToast({
title: '邀请关系建立成功',
icon: 'success',
duration: 2000
})
}
} catch (error) {
console.error('处理邀请关系失败:', error)
}
}
// 显示登录成功提示
// Taro.showToast({
// title: '注册成功',
// icon: 'success',
// duration: 1500
// })
// 不需要重新启动小程序状态已经通过useUser更新
// 可以选择性地刷新当前页面数据
// await reload();
}
})
} else {
console.log('登录失败!')
}
}
})
}
// 处理固定按钮点击事件
const handleFixedButtonClick = () => {
// 触发表单提交
formRef.current?.submit();
};
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, [user?.userId]); // 依赖用户ID当用户变化时重新加载
// 当FormData变化时同步更新表单字段值
useEffect(() => {
if (formRef.current && FormData) {
formRef.current.setFieldsValue({
refereeId: FormData.refereeId,
phone: FormData.phone,
avatar: FormData.avatar,
realName: FormData.nickname
});
}
}, [FormData]);
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)}
>
<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={false}/>*/}
{/*</Form.Item>*/}
<Form.Item name="phone" label="手机号" initialValue={FormData?.phone} required>
<View className="flex items-center justify-between">
<Input
placeholder="请填写手机号"
disabled={true}
maxLength={11}
value={FormData?.phone || ''}
/>
<Button style={{color: '#ffffff'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Space>
<Button size="small"></Button>
</Space>
</Button>
</View>
</Form.Item>
{
FormData?.phone && <Form.Item name="avatar" label="头像" initialValue={FormData?.avatar} required>
<Button open-type="chooseAvatar" style={{height: '58px'}} onChooseAvatar={uploadAvatar}>
<Avatar src={FormData?.avatar || user?.avatar} size="54"/>
</Button>
</Form.Item>
}
<Form.Item name="realName" label="昵称" initialValue="" required>
<Input
type="nickname"
className="info-content__input"
placeholder="请获取微信昵称"
value={FormData?.nickname || ''}
onInput={(e: InputEvent) => getWxNickname(e.detail.value)}
/>
</Form.Item>
</CellGroup>
</Form>
{/* 底部浮动按钮 */}
<FixedButton
icon={<Edit/>}
text={'立即注册'}
onClick={handleFixedButtonClick}
/>
</>
);
};
export default AddUserAddress;

View File

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

View File

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

View File

@@ -1,199 +0,0 @@
import React, {useCallback, useEffect, useState} from 'react'
import {View, Text, ScrollView} from '@tarojs/components'
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {pageShopDealerCapital} from '@/api/shop/shopDealerCapital'
import {useDealerUser} from '@/hooks/useDealerUser'
import type {ShopDealerCapital} from '@/api/shop/shopDealerCapital/model'
const PAGE_SIZE = 10
const DealerCapital: React.FC = () => {
const {dealerUser} = useDealerUser()
const [loading, setLoading] = useState(false)
const [refreshing, setRefreshing] = useState(false)
const [loadingMore, setLoadingMore] = useState(false)
const [currentPage, setCurrentPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [records, setRecords] = useState<ShopDealerCapital[]>([])
const getFlowTypeText = (flowType?: number) => {
switch (flowType) {
case 10:
return '佣金收入'
case 20:
return '提现支出'
case 30:
return '转账支出'
case 40:
return '转账收入'
default:
return '资金变动'
}
}
const getFlowTypeTag = (flowType?: number) => {
// 收入success支出danger其它default
if (flowType === 10 || flowType === 40) return 'success'
if (flowType === 20 || flowType === 30) return 'danger'
return 'default'
}
const formatMoney = (flowType?: number, money?: string) => {
const isIncome = flowType === 10 || flowType === 40
const isExpense = flowType === 20 || flowType === 30
const sign = isIncome ? '+' : isExpense ? '-' : ''
return `${sign}${money || '0.00'}`
}
const fetchRecords = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
if (!dealerUser?.userId) return
try {
if (isRefresh) {
setRefreshing(true)
} else if (page === 1) {
setLoading(true)
} else {
setLoadingMore(true)
}
const result = await pageShopDealerCapital({
page,
limit: PAGE_SIZE,
// 只显示与当前登录用户相关的收益明细
userId: dealerUser.userId
})
const list = result?.list || []
if (page === 1) {
setRecords(list)
} else {
setRecords(prev => [...prev, ...list])
}
setHasMore(list.length === PAGE_SIZE)
setCurrentPage(page)
} catch (error) {
console.error('获取收益明细失败:', error)
Taro.showToast({
title: '获取收益明细失败',
icon: 'error'
})
} finally {
setLoading(false)
setRefreshing(false)
setLoadingMore(false)
}
}, [dealerUser?.userId])
const handleRefresh = async () => {
await fetchRecords(1, true)
}
const handleLoadMore = async () => {
if (!loadingMore && hasMore) {
await fetchRecords(currentPage + 1)
}
}
useEffect(() => {
if (dealerUser?.userId) {
fetchRecords(1)
}
}, [fetchRecords, dealerUser?.userId])
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="min-h-screen bg-gray-50">
<PullToRefresh
onRefresh={handleRefresh}
disabled={refreshing}
pullingText="下拉刷新"
canReleaseText="释放刷新"
refreshingText="刷新中..."
completeText="刷新完成"
>
<ScrollView
scrollY
className="h-screen"
onScrollToLower={handleLoadMore}
lowerThreshold={50}
>
<View className="p-4">
{loading && records.length === 0 ? (
<View className="text-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : records.length > 0 ? (
<>
{records.map((item) => (
<View key={item.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-1">
<Text className="font-semibold text-gray-800">
{item.describe || '收益明细'}
</Text>
<Tag type={getFlowTypeTag(item.flowType)}>
{getFlowTypeText(item.flowType)}
</Tag>
</View>
<View className="flex justify-between items-center mb-1">
<Text className="text-sm text-gray-400">
</Text>
<Text
className={`text-sm font-semibold ${
item.flowType === 10 || item.flowType === 40 ? 'text-green-600' :
item.flowType === 20 || item.flowType === 30 ? 'text-red-500' :
'text-gray-700'
}`}
>
{formatMoney(item.flowType, item.money)}
</Text>
</View>
<View className="flex justify-between items-center">
<Text className="text-sm text-gray-400">
{/*用户:{item.userId ?? '-'}*/}
</Text>
<Text className="text-sm text-gray-400">
{item.createTime || '-'}
</Text>
</View>
</View>
))}
{loadingMore && (
<View className="text-center py-4">
<Loading/>
<Text className="text-gray-500 mt-1 text-sm">...</Text>
</View>
)}
{!hasMore && records.length > 0 && (
<View className="text-center py-4">
<Text className="text-gray-400 text-sm"></Text>
</View>
)}
</>
) : (
<Empty description="暂无收益明细"/>
)}
</View>
</ScrollView>
</PullToRefresh>
</View>
)
}
export default DealerCapital

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '易赊宝分享中心'
})

View File

View File

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

View File

@@ -1,157 +0,0 @@
import React from 'react'
import { View, Text } from '@tarojs/components'
import { Button, Cell, CellGroup, Tag } from '@nutui/nutui-react-taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import Taro from '@tarojs/taro'
const DealerInfo: React.FC = () => {
const {
dealerUser,
loading,
error,
refresh,
} = useDealerUser()
// 跳转到申请页面
const navigateToApply = () => {
Taro.navigateTo({
url: '/pages/dealer/apply/add'
})
}
if (error) {
return (
<View className="p-4">
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
<Text className="text-red-600">{error}</Text>
</View>
<Button type="primary" onClick={refresh}>
</Button>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
{/* 页面标题 */}
<View className="bg-white px-4 py-3 border-b border-gray-100">
<Text className="text-lg font-bold text-center">
</Text>
</View>
{!dealerUser ? (
// 非经销商状态
<View className="bg-white mx-4 mt-4 rounded-lg p-6">
<View className="text-center py-8">
<Text className="text-gray-500 mb-4"></Text>
<Text className="text-sm text-gray-400 mb-6">
</Text>
<Button
type="primary"
size="large"
onClick={navigateToApply}
>
</Button>
</View>
</View>
) : (
// 经销商信息展示
<View>
{/* 状态卡片 */}
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
<View className="flex items-center justify-between mb-4">
<Text className="text-lg font-semibold"></Text>
<Tag>
{dealerUser.realName}
</Tag>
</View>
{/* 基本信息 */}
<CellGroup>
<Cell
title="经销商ID"
extra={dealerUser.userId || '-'}
/>
<Cell
title="refereeId"
extra={dealerUser.refereeId || '-'}
/>
<Cell
title="成为经销商时间"
extra={
dealerUser.money
}
/>
</CellGroup>
{/* 操作按钮 */}
<View className="mt-6 gap-2">
<Button
type="primary"
size="large"
loading={loading}
>
</Button>
</View>
</View>
{/* 经销商权益 */}
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
<Text className="font-semibold mb-3"></Text>
<View className="gap-2">
<Text className="text-sm text-gray-600">
</Text>
<Text className="text-sm text-gray-600">
广
</Text>
<Text className="text-sm text-gray-600">
</Text>
<Text className="text-sm text-gray-600">
</Text>
</View>
</View>
{/* 佣金统计 */}
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
<Text className="font-semibold mb-3"></Text>
<View className="grid grid-cols-3 gap-4">
<View className="text-center">
<Text className="text-lg font-bold text-blue-600">0</Text>
<Text className="text-sm text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-lg font-bold text-green-600">0</Text>
<Text className="text-sm text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-lg font-bold text-orange-600">0</Text>
<Text className="text-sm text-gray-500"></Text>
</View>
</View>
</View>
</View>
)}
{/* 刷新按钮 */}
<View className="text-center py-4">
<Text
className="text-blue-500 text-sm"
onClick={refresh}
>
</Text>
</View>
</View>
)
}
export default DealerInfo

View File

@@ -1,7 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '邀请统计',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
backgroundColor: '#f5f5f5',
enablePullDownRefresh: true
})

View File

@@ -1,336 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import {
Empty,
Tabs,
Loading,
PullToRefresh,
Card,
} from '@nutui/nutui-react-taro'
import {
User,
ArrowUp,
Calendar,
Share,
Target,
Gift
} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import {
getInviteStats,
getMyInviteRecords,
getInviteRanking
} from '@/api/invite'
import type {
InviteStats,
InviteRecord
} from '@/api/invite'
import { businessGradients } from '@/styles/gradients'
import {InviteRanking} from "@/api/invite/model";
const InviteStatsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>('stats')
const [loading, setLoading] = useState<boolean>(false)
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
const [inviteRecords, setInviteRecords] = useState<InviteRecord[]>([])
const [ranking, setRanking] = useState<InviteRanking[]>([])
const [dateRange, setDateRange] = useState<string>('month')
const { dealerUser } = useDealerUser()
// 获取邀请统计数据
const fetchInviteStats = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
const stats = await getInviteStats(dealerUser.userId)
stats && setInviteStats(stats)
} catch (error) {
console.error('获取邀请统计失败:', error)
Taro.showToast({
title: '获取统计数据失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId])
// 获取邀请记录
const fetchInviteRecords = useCallback(async () => {
if (!dealerUser?.userId) return
try {
const result = await getMyInviteRecords({
page: 1,
limit: 50,
inviterId: dealerUser.userId
})
setInviteRecords(result?.list || [])
} catch (error) {
console.error('获取邀请记录失败:', error)
}
}, [dealerUser?.userId])
// 获取邀请排行榜
const fetchRanking = useCallback(async () => {
try {
const result = await getInviteRanking({
limit: 20,
period: dateRange as 'day' | 'week' | 'month'
})
setRanking(result || [])
} catch (error) {
console.error('获取排行榜失败:', error)
}
}, [dateRange])
// 刷新数据
const handleRefresh = async () => {
await Promise.all([
fetchInviteStats(),
fetchInviteRecords(),
fetchRanking()
])
}
// 初始化数据
useEffect(() => {
if (dealerUser?.userId) {
fetchInviteStats().then()
fetchInviteRecords().then()
fetchRanking().then()
}
}, [fetchInviteStats, fetchInviteRecords, fetchRanking])
// 获取状态显示文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
'pending': '待注册',
'registered': '已注册',
'activated': '已激活'
}
return statusMap[status] || status
}
// 获取状态颜色
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'pending': 'text-orange-500',
'registered': 'text-blue-500',
'activated': 'text-green-500'
}
return colorMap[status] || 'text-gray-500'
}
// 渲染统计概览
const renderStatsOverview = () => (
<View className="px-4 space-y-4">
{/* 核心数据卡片 */}
<Card className="bg-white rounded-2xl shadow-sm">
<View className="p-4">
<Text className="text-lg font-semibold text-gray-800 mb-4"></Text>
{loading ? (
<View className="flex items-center justify-center py-8">
<Loading />
</View>
) : inviteStats ? (
<View className="grid grid-cols-2 gap-4">
<View className="text-center p-4 bg-blue-50 rounded-xl">
<ArrowUp size="24" className="text-blue-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-blue-600">
{inviteStats.totalInvites || 0}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
<View className="text-center p-4 bg-green-50 rounded-xl">
<User size="24" className="text-green-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-green-600">
{inviteStats.successfulRegistrations || 0}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
<View className="text-center p-4 bg-purple-50 rounded-xl">
<Target size="24" className="text-purple-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-purple-600">
{inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
<View className="text-center p-4 bg-orange-50 rounded-xl">
<Calendar size="24" className="text-orange-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-orange-600">
{inviteStats.todayInvites || 0}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
</View>
) : (
<View className="text-center py-8">
<Text className="text-gray-500"></Text>
</View>
)}
</View>
</Card>
{/* 邀请来源分析 */}
{inviteStats?.sourceStats && inviteStats.sourceStats.length > 0 && (
<Card className="bg-white rounded-2xl shadow-sm">
<View className="p-4">
<Text className="text-lg font-semibold text-gray-800 mb-4"></Text>
<View className="space-y-3">
{inviteStats.sourceStats.map((source, index) => (
<View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<View className="flex items-center">
<Share size="16" className="text-blue-500 mr-2" />
<Text className="font-medium text-gray-800">{source.source}</Text>
</View>
<View className="text-right">
<Text className="text-lg font-bold text-gray-800">{source.count}</Text>
<Text className="text-sm text-gray-500">
{source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}
</Text>
</View>
</View>
))}
</View>
</View>
</Card>
)}
</View>
)
// 渲染邀请记录
const renderInviteRecords = () => (
<View className="px-4">
{inviteRecords.length > 0 ? (
<View className="space-y-3">
{inviteRecords.map((record, index) => (
<Card key={record.id || index} className="bg-white rounded-xl shadow-sm">
<View className="p-4">
<View className="flex items-center justify-between mb-2">
<Text className="font-medium text-gray-800">
{record.inviteeName || `用户${record.inviteeId}`}
</Text>
<Text className={`text-sm font-medium ${getStatusColor(record.status || 'pending')}`}>
{getStatusText(record.status || 'pending')}
</Text>
</View>
<View className="flex items-center justify-between text-sm text-gray-500">
<Text>: {record.source || '未知'}</Text>
<Text>{record.inviteTime ? new Date(record.inviteTime).toLocaleDateString() : ''}</Text>
</View>
{record.registerTime && (
<Text className="text-xs text-green-600 mt-1">
: {new Date(record.registerTime).toLocaleString()}
</Text>
)}
</View>
</Card>
))}
</View>
) : (
<Empty description="暂无邀请记录" />
)}
</View>
)
// 渲染排行榜
const renderRanking = () => (
<View className="px-4">
<View className="mb-4">
<Tabs value={dateRange} onChange={() => setDateRange}>
<Tabs.TabPane title="日榜" value="day" />
<Tabs.TabPane title="周榜" value="week" />
<Tabs.TabPane title="月榜" value="month" />
</Tabs>
</View>
{ranking.length > 0 ? (
<View className="space-y-3">
{ranking.map((item, index) => (
<Card key={item.inviterId} className="bg-white rounded-xl shadow-sm">
<View className="p-4 flex items-center">
<View className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 mr-3">
{index < 3 ? (
<Gift size="16" className={index === 0 ? 'text-yellow-500' : index === 1 ? 'text-gray-400' : 'text-orange-400'} />
) : (
<Text className="text-sm font-bold text-gray-600">{index + 1}</Text>
)}
</View>
<View className="flex-1">
<Text className="font-medium text-gray-800">{item.inviterName}</Text>
<Text className="text-sm text-gray-500">
{item.inviteCount} · {item.conversionRate ? `${(item.conversionRate * 100).toFixed(1)}%` : '0%'}
</Text>
</View>
<Text className="text-lg font-bold text-blue-600">{item.successCount}</Text>
</View>
</Card>
))}
</View>
) : (
<Empty description="暂无排行数据" />
)}
</View>
)
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
{/* 头部 */}
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
background: businessGradients.dealer.header
}}>
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="relative z-10">
<Text className="text-2xl font-bold mb-2 text-white"></Text>
<Text className="text-white text-opacity-80">
广
</Text>
</View>
</View>
{/* 标签页 */}
<View className="px-4 mb-4">
<Tabs value={activeTab} onChange={() => setActiveTab}>
<Tabs.TabPane title="统计概览" value="stats" />
<Tabs.TabPane title="邀请记录" value="records" />
<Tabs.TabPane title="排行榜" value="ranking" />
</Tabs>
</View>
{/* 内容区域 */}
<PullToRefresh onRefresh={handleRefresh}>
<View className="pb-6">
{activeTab === 'stats' && renderStatsOverview()}
{activeTab === 'records' && renderInviteRecords()}
{activeTab === 'ranking' && renderRanking()}
</View>
</PullToRefresh>
</View>
)
}
export default InviteStatsPage

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '分销订单'
})

View File

@@ -1,205 +0,0 @@
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text, ScrollView} from '@tarojs/components'
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
import {useDealerUser} from '@/hooks/useDealerUser'
import type {ShopDealerOrder} from '@/api/shop/shopDealerOrder/model'
interface OrderWithDetails extends ShopDealerOrder {
orderNo?: string
customerName?: string
userCommission?: string
}
const DealerOrders: React.FC = () => {
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [loadingMore, setLoadingMore] = useState<boolean>(false)
const [orders, setOrders] = useState<OrderWithDetails[]>([])
const [currentPage, setCurrentPage] = useState<number>(1)
const [hasMore, setHasMore] = useState<boolean>(true)
const {dealerUser} = useDealerUser()
// 获取订单数据
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
// 需要当前登录用户ID用于 resourceId 参数)
if (!dealerUser || !dealerUser.userId) return
try {
if (isRefresh) {
setRefreshing(true)
} else if (page === 1) {
setLoading(true)
} else {
setLoadingMore(true)
}
const result = await pageShopDealerOrder({
page,
limit: 10,
// 后端需要 resourceId=当前登录用户ID 才能正确过滤分销订单
resourceId: dealerUser.userId
})
if (result?.list) {
const newOrders = result.list.map(order => ({
...order,
// 优先使用接口返回的订单号;没有则降级展示 orderId
orderNo: order.orderNo ?? (order.orderId != null ? String(order.orderId) : undefined),
customerName: `${order.nickname}${order.userId}`,
userCommission: order.firstMoney || '0.00'
}))
if (page === 1) {
setOrders(newOrders)
} else {
setOrders(prev => [...prev, ...newOrders])
}
setHasMore(newOrders.length === 10)
setCurrentPage(page)
}
} catch (error) {
console.error('获取分销订单失败:', error)
Taro.showToast({
title: '获取订单失败',
icon: 'error'
})
} finally {
setLoading(false)
setRefreshing(false)
setLoadingMore(false)
}
}, [dealerUser?.userId])
// 下拉刷新
const handleRefresh = async () => {
await fetchOrders(1, true)
}
// 加载更多
const handleLoadMore = async () => {
if (!loadingMore && hasMore) {
await fetchOrders(currentPage + 1)
}
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchOrders(1)
}
}, [fetchOrders])
const getStatusText = (isSettled?: number, isInvalid?: number, isUnfreeze?: number, orderStatus?: number) => {
if (orderStatus === 2 || orderStatus === 5 || orderStatus === 6) return '已取消'
if (isInvalid === 1) return '已失效'
if (isUnfreeze === 1) return '已解冻'
if (isSettled === 1) return '已结算'
return '待结算'
}
const getStatusColor = (isSettled?: number, isInvalid?: number, isUnfreeze?: number, orderStatus?: number) => {
if (orderStatus === 2 || orderStatus === 5 || orderStatus === 6) return 'default'
if (isInvalid === 1) return 'danger'
if (isUnfreeze === 1) return 'success'
if (isSettled === 1) return 'info'
return 'warning'
}
const handleGoCapital = () => {
Taro.navigateTo({url: '/dealer/capital/index'})
}
const renderOrderItem = (order: OrderWithDetails) => (
<View
key={order.id}
className="bg-white rounded-lg p-4 mb-3 shadow-sm"
onClick={handleGoCapital}
>
<View className="flex justify-between items-start mb-1">
<Text className="font-semibold text-gray-800">
{order.orderNo || '-'}
</Text>
<Tag type={getStatusColor(order.isSettled, order.isInvalid, order.isUnfreeze,order.orderStatus)}>
{getStatusText(order.isSettled, order.isInvalid, order.isUnfreeze,order.orderStatus)}
</Tag>
</View>
{/*<View className="flex justify-between items-center mb-1">*/}
{/* <Text className="text-sm text-gray-400">*/}
{/* 订单金额:¥{order.orderPrice || '0.00'}*/}
{/* </Text>*/}
{/*</View>*/}
<View className="flex justify-between items-center">
<Text className="text-sm text-gray-400">
{order.createTime}
</Text>
<Text className="text-sm text-gray-400">
¥{order.orderPrice || '0.00'}
</Text>
</View>
</View>
)
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="min-h-screen bg-gray-50">
<PullToRefresh
onRefresh={handleRefresh}
disabled={refreshing}
pullingText="下拉刷新"
canReleaseText="释放刷新"
refreshingText="刷新中..."
completeText="刷新完成"
>
<ScrollView
scrollY
className="h-screen"
onScrollToLower={handleLoadMore}
lowerThreshold={50}
>
<View className="p-4">
{loading && orders.length === 0 ? (
<View className="text-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : orders.length > 0 ? (
<>
{orders.map(renderOrderItem)}
{loadingMore && (
<View className="text-center py-4">
<Loading/>
<Text className="text-gray-500 mt-1 text-sm">...</Text>
</View>
)}
{!hasMore && orders.length > 0 && (
<View className="text-center py-4">
<Text className="text-gray-400 text-sm"></Text>
</View>
)}
</>
) : (
<Empty description="暂无分销订单"/>
)}
</View>
</ScrollView>
</PullToRefresh>
</View>
)
}
export default DealerOrders

View File

@@ -1,6 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '易赊宝分享中心',
// Enable "Share to friends" and "Share to Moments" (timeline) for this page.
enableShareAppMessage: true,
enableShareTimeline: true
})

View File

@@ -1,522 +0,0 @@
import React, {useState, useEffect} from 'react'
import {View, Text, Image} from '@tarojs/components'
import {Button, Loading} from '@nutui/nutui-react-taro'
import {Download, QrCode} from '@nutui/icons-react-taro'
import Taro, {useShareAppMessage} from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {generateInviteCode} from '@/api/invite'
// import type {InviteStats} from '@/api/invite'
import {businessGradients} from '@/styles/gradients'
const DealerQrcode: React.FC = () => {
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
const [codeLoading, setCodeLoading] = useState<boolean>(false)
const [saving, setSaving] = useState<boolean>(false)
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
const {dealerUser, loading: dealerLoading, error, refresh} = useDealerUser()
// Enable "转发给朋友" + "分享到朋友圈" items in the share panel/menu.
useEffect(() => {
// Some clients require explicit call to show both share entries.
Taro.showShareMenu({
withShareTicket: true,
showShareItems: ['shareAppMessage', 'shareTimeline']
}).catch(() => {})
}, [])
// 转发给朋友(分享小程序链接)
useShareAppMessage(() => {
const inviterRaw = dealerUser?.userId ?? Taro.getStorageSync('UserId')
const inviter = Number(inviterRaw)
const hasInviter = Number.isFinite(inviter) && inviter > 0
const user = Taro.getStorageSync('User') || {}
const nickname = (user && (user.nickname || user.realName || user.username)) || ''
const title = hasInviter ? `${nickname || '我'}邀请你加入易赊宝伙伴计划` : '易赊宝伙伴计划'
return {
title,
path: hasInviter
? `/pages/index/index?inviter=${inviter}&source=dealer_qrcode&t=${Date.now()}`
: `/pages/index/index`,
success: function () {
Taro.showToast({title: '分享成功', icon: 'success', duration: 2000})
},
fail: function () {
Taro.showToast({title: '分享失败', icon: 'none', duration: 2000})
}
}
})
// 生成小程序码
const generateMiniProgramCode = async () => {
if (!dealerUser?.userId) {
return
}
try {
setCodeLoading(true)
// 生成邀请小程序码
const codeUrl = await generateInviteCode(dealerUser.userId)
if (codeUrl) {
setMiniProgramCodeUrl(codeUrl)
} else {
throw new Error('返回的小程序码URL为空')
}
} catch (error: any) {
Taro.showToast({
title: error.message || '生成小程序码失败',
icon: 'error'
})
// 清空之前的二维码
setMiniProgramCodeUrl('')
} finally {
setCodeLoading(false)
}
}
// 获取邀请统计数据
// const fetchInviteStats = async () => {
// if (!dealerUser?.userId) return
//
// try {
// setStatsLoading(true)
// const stats = await getInviteStats(dealerUser.userId)
// stats && setInviteStats(stats)
// } catch (error) {
// // 静默处理错误,不影响用户体验
// } finally {
// setStatsLoading(false)
// }
// }
// 初始化生成小程序码和获取统计数据
useEffect(() => {
if (dealerUser?.userId) {
generateMiniProgramCode()
// fetchInviteStats()
}
}, [dealerUser?.userId])
const isAlbumAuthError = (errMsg?: string) => {
if (!errMsg) return false
// WeChat uses variants like: "saveImageToPhotosAlbum:fail auth deny",
// "saveImageToPhotosAlbum:fail auth denied", "authorize:fail auth deny"
return (
errMsg.includes('auth deny') ||
errMsg.includes('auth denied') ||
errMsg.includes('authorize') ||
errMsg.includes('scope.writePhotosAlbum')
)
}
const ensureWriteAlbumPermission = async (): Promise<boolean> => {
try {
const setting = await Taro.getSetting()
if (setting?.authSetting?.['scope.writePhotosAlbum']) return true
await Taro.authorize({scope: 'scope.writePhotosAlbum'})
return true
} catch (error: any) {
const modal = await Taro.showModal({
title: '提示',
content: '需要您授权保存图片到相册,请在设置中开启相册权限',
confirmText: '去设置'
})
if (modal.confirm) {
await Taro.openSetting()
}
return false
}
}
const downloadImageToLocalPath = async (url: string): Promise<string> => {
// saveImageToPhotosAlbum must receive a local temp path (e.g. `http://tmp/...` or `wxfile://...`).
// Some environments may return a non-existing temp path from getImageInfo, so we verify.
if (url.startsWith('http://tmp/') || url.startsWith('wxfile://')) {
return url
}
const token = Taro.getStorageSync('access_token')
const tenantId = Taro.getStorageSync('TenantId')
const header: Record<string, string> = {}
if (token) header.Authorization = token
if (tenantId) header.TenantId = tenantId
// 先下载到本地临时文件再保存到相册
const res = await Taro.downloadFile({url, header})
if (res.statusCode !== 200 || !res.tempFilePath) {
throw new Error(`图片下载失败(${res.statusCode || 'unknown'})`)
}
// Double-check file exists to avoid: saveImageToPhotosAlbum:fail no such file or directory
try {
await Taro.getFileInfo({filePath: res.tempFilePath})
} catch (_) {
throw new Error('图片临时文件不存在,请重试')
}
return res.tempFilePath
}
// 保存小程序码到相册
const saveMiniProgramCode = async () => {
if (!miniProgramCodeUrl) {
Taro.showToast({
title: '小程序码未生成',
icon: 'error'
})
return
}
try {
if (saving) return
setSaving(true)
Taro.showLoading({title: '保存中...'})
const hasPermission = await ensureWriteAlbumPermission()
if (!hasPermission) return
let filePath = await downloadImageToLocalPath(miniProgramCodeUrl)
try {
await Taro.saveImageToPhotosAlbum({filePath})
} catch (e: any) {
const msg = e?.errMsg || e?.message || ''
// Fallback: some devices/clients may fail to save directly from a temp path.
if (
msg.includes('no such file or directory') &&
(filePath.startsWith('http://tmp/') || filePath.startsWith('wxfile://'))
) {
const saved = (await Taro.saveFile({tempFilePath: filePath})) as unknown as { savedFilePath?: string }
if (saved?.savedFilePath) {
filePath = saved.savedFilePath
}
await Taro.saveImageToPhotosAlbum({filePath})
} else {
throw e
}
}
Taro.showToast({
title: '保存成功',
icon: 'success'
})
} catch (error: any) {
const errMsg = error?.errMsg || error?.message
if (errMsg?.includes('cancel')) {
Taro.showToast({title: '已取消', icon: 'none'})
return
}
if (isAlbumAuthError(errMsg)) {
const modal = await Taro.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
confirmText: '去设置'
})
if (modal.confirm) {
await Taro.openSetting()
}
} else {
// Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
await Taro.showModal({
title: '保存失败',
content: errMsg || '保存失败,请稍后重试',
showCancel: false
})
}
} finally {
Taro.hideLoading()
setSaving(false)
}
}
// 复制邀请信息
// const copyInviteInfo = () => {
// if (!dealerUser?.userId) {
// Taro.showToast({
// title: '用户信息未加载',
// icon: 'error'
// })
// return
// }
//
// const inviteText = `🎉 邀请您加入我的团队!
//
// 扫描小程序码或搜索"易赊宝"小程序,即可享受优质商品和服务!
//
// 💰 成为我的团队成员,一起赚取丰厚佣金
// 🎁 新用户专享优惠等你来拿
//
// 邀请码:${dealerUser.userId}
// 快来加入我们吧!`
//
// Taro.setClipboardData({
// data: inviteText,
// success: () => {
// Taro.showToast({
// title: '邀请信息已复制',
// icon: 'success'
// })
// }
// })
// }
// 分享小程序码
// const shareMiniProgramCode = () => {
// if (!dealerUser?.userId) {
// Taro.showToast({
// title: '用户信息未加载',
// icon: 'error'
// })
// return
// }
//
// // 小程序分享
// Taro.showShareMenu({
// withShareTicket: true,
// showShareItems: ['shareAppMessage']
// })
// }
if (dealerLoading) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
if (error) {
return (
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
<Text className="text-gray-800 font-semibold"></Text>
<Text className="text-gray-500 text-sm mt-2">{error}</Text>
<Button className="mt-6" type="primary" onClick={refresh}></Button>
</View>
)
}
// 未成为分销商时给出明确引导,避免一直停留在“加载中”
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
<Text className="text-gray-800 font-semibold"></Text>
<Text className="text-gray-500 text-sm mt-2 text-center"></Text>
<Button
className="mt-6"
type="primary"
onClick={() => Taro.navigateTo({url: '/dealer/apply/add'})}
>
</Button>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
{/* 头部卡片 */}
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
background: businessGradients.dealer.header
}}>
{/* 装饰背景 */}
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="relative z-10 flex flex-col">
<Text className="text-2xl font-bold mb-2 text-white"></Text>
<Text className="text-white text-opacity-80">
</Text>
</View>
</View>
<View className="px-4">
{/* 小程序码展示区 */}
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
<View className="text-center">
{codeLoading ? (
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : miniProgramCodeUrl ? (
<View className="w-48 h-48 mx-auto mb-4 bg-white rounded-xl shadow-sm p-4">
<Image
src={miniProgramCodeUrl}
className="w-full h-full"
mode="aspectFit"
onError={() => {
Taro.showModal({
title: '二维码加载失败',
content: '请检查网络连接或联系管理员',
showCancel: true,
confirmText: '重新生成',
success: (res) => {
if (res.confirm) {
generateMiniProgramCode();
}
}
});
}}
/>
</View>
) : (
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
<QrCode size="48" className="text-gray-400 mb-2"/>
<Text className="text-gray-500"></Text>
<Button
size="small"
type="primary"
className="mt-2"
onClick={generateMiniProgramCode}
>
</Button>
</View>
)}
<View className="text-lg font-semibold text-gray-800 mb-2">
</View>
<View className="text-sm text-gray-500 mb-4">
| |
</View>
</View>
</View>
{/* 操作按钮 */}
<View className={'gap-2'}>
<View className={'my-2'}>
<Button
type="primary"
size="large"
block
icon={<Download/>}
onClick={saveMiniProgramCode}
disabled={!miniProgramCodeUrl || codeLoading || saving}
>
</Button>
</View>
</View>
{/* 推广说明 */}
<View className="bg-white rounded-2xl p-4 mt-6 hidden">
<Text className="font-semibold text-gray-800 mb-3">广</Text>
<View className="space-y-2">
<View className="flex items-start">
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
<Text className="text-sm text-gray-600">
</Text>
</View>
<View className="flex items-start">
<View className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
<Text className="text-sm text-gray-600">
</Text>
</View>
<View className="flex items-start">
<View className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
<Text className="text-sm text-gray-600">
</Text>
</View>
</View>
</View>
{/* 邀请统计数据 */}
{/*<View className="bg-white rounded-2xl p-4 mt-4 mb-6">*/}
{/* <Text className="font-semibold text-gray-800 mb-3">我的邀请数据</Text>*/}
{/* {statsLoading ? (*/}
{/* <View className="flex items-center justify-center py-8">*/}
{/* <Loading/>*/}
{/* <Text className="text-gray-500 mt-2">加载中...</Text>*/}
{/* </View>*/}
{/* ) : inviteStats ? (*/}
{/* <View className="space-y-4">*/}
{/* <View className="grid grid-cols-2 gap-4">*/}
{/* <View className="text-center">*/}
{/* <Text className="text-2xl font-bold text-blue-500">*/}
{/* {inviteStats.totalInvites || 0}*/}
{/* </Text>*/}
{/* <Text className="text-sm text-gray-500">总邀请数</Text>*/}
{/* </View>*/}
{/* <View className="text-center">*/}
{/* <Text className="text-2xl font-bold text-green-500">*/}
{/* {inviteStats.successfulRegistrations || 0}*/}
{/* </Text>*/}
{/* <Text className="text-sm text-gray-500">成功注册</Text>*/}
{/* </View>*/}
{/* </View>*/}
{/* <View className="grid grid-cols-2 gap-4">*/}
{/* <View className="text-center">*/}
{/* <Text className="text-2xl font-bold text-purple-500">*/}
{/* {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
{/* </Text>*/}
{/* <Text className="text-sm text-gray-500">转化率</Text>*/}
{/* </View>*/}
{/* <View className="text-center">*/}
{/* <Text className="text-2xl font-bold text-orange-500">*/}
{/* {inviteStats.todayInvites || 0}*/}
{/* </Text>*/}
{/* <Text className="text-sm text-gray-500">今日邀请</Text>*/}
{/* </View>*/}
{/* </View>*/}
{/* /!* 邀请来源统计 *!/*/}
{/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/}
{/* <View className="mt-4">*/}
{/* <Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>*/}
{/* <View className="space-y-2">*/}
{/* {inviteStats.sourceStats.map((source, index) => (*/}
{/* <View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">*/}
{/* <View className="flex items-center">*/}
{/* <View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>*/}
{/* <Text className="text-sm text-gray-700">{source.source}</Text>*/}
{/* </View>*/}
{/* <View className="text-right">*/}
{/* <Text className="text-sm font-medium text-gray-800">{source.count}</Text>*/}
{/* <Text className="text-xs text-gray-500">*/}
{/* {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
{/* </Text>*/}
{/* </View>*/}
{/* </View>*/}
{/* ))}*/}
{/* </View>*/}
{/* </View>*/}
{/* )}*/}
{/* </View>*/}
{/* ) : (*/}
{/* <View className="text-center py-8">*/}
{/* <View className="text-gray-500">暂无邀请数据</View>*/}
{/* <Button*/}
{/* size="small"*/}
{/* type="primary"*/}
{/* className="mt-2"*/}
{/* onClick={fetchInviteStats}*/}
{/* >*/}
{/* 刷新数据*/}
{/* </Button>*/}
{/* </View>*/}
{/* )}*/}
{/*</View>*/}
</View>
</View>
)
}
export default DealerQrcode

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '邀请推广'
})

View File

@@ -1,439 +0,0 @@
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {Phone, Edit, Message} from '@nutui/icons-react-taro'
import {Space, Empty, Avatar, Button} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {listShopDealerReferee} from '@/api/shop/shopDealerReferee'
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
import type {ShopDealerReferee} from '@/api/shop/shopDealerReferee/model'
import FixedButton from "@/components/FixedButton";
import navTo from "@/utils/common";
import {updateUser} from "@/api/system/user";
interface TeamMemberWithStats extends ShopDealerReferee {
name?: string
avatar?: string
nickname?: string;
alias?: string;
phone?: string;
orderCount?: number
commission?: string
status?: 'active' | 'inactive'
subMembers?: number
joinTime?: string
dealerAvatar?: string;
dealerName?: string;
dealerPhone?: string;
}
// 层级信息接口
interface LevelInfo {
dealerId: number
dealerName?: string
level: number
}
const DealerTeam: React.FC = () => {
const [teamMembers, setTeamMembers] = useState<TeamMemberWithStats[]>([])
const {dealerUser} = useDealerUser()
const [dealerId, setDealerId] = useState<number>()
// 层级栈,用于支持返回上一层
const [levelStack, setLevelStack] = useState<LevelInfo[]>([])
const [loading, setLoading] = useState(false)
// 当前查看的用户名称
const [currentDealerName, setCurrentDealerName] = useState<string>('')
// 异步加载成员统计数据
const loadMemberStats = async (members: TeamMemberWithStats[]) => {
// 分批处理,避免过多并发请求
const batchSize = 3
for (let i = 0; i < members.length; i += batchSize) {
const batch = members.slice(i, i + batchSize)
const batchStats = await Promise.all(
batch.map(async (member) => {
try {
// 并行获取订单统计和下级成员数量
const [orderResult, subMembersResult] = await Promise.all([
pageShopDealerOrder({
page: 1,
userId: member.userId
}),
listShopDealerReferee({
dealerId: member.userId,
deleted: 0
})
])
let orderCount = 0
let commission = '0.00'
let status: 'active' | 'inactive' = 'inactive'
if (orderResult?.list) {
const orders = orderResult.list
orderCount = orders.length
commission = orders.reduce((sum, order) => {
const levelCommission = member.level === 1 ? order.firstMoney :
member.level === 2 ? order.secondMoney :
order.thirdMoney
return sum + parseFloat(levelCommission || '0')
}, 0).toFixed(2)
// 判断活跃状态30天内有订单为活跃
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
const hasRecentOrder = orders.some(order =>
new Date(order.createTime || '') > thirtyDaysAgo
)
status = hasRecentOrder ? 'active' : 'inactive'
}
return {
...member,
orderCount,
commission,
status,
subMembers: subMembersResult?.length || 0
}
} catch (error) {
console.error(`获取成员${member.userId}数据失败:`, error)
return {
...member,
orderCount: 0,
commission: '0.00',
status: 'inactive' as const,
subMembers: 0
}
}
})
)
// 更新这一批成员的数据
setTeamMembers(prevMembers => {
const updatedMembers = [...prevMembers]
batchStats.forEach(updatedMember => {
const index = updatedMembers.findIndex(m => m.userId === updatedMember.userId)
if (index !== -1) {
updatedMembers[index] = updatedMember
}
})
return updatedMembers
})
// 添加小延迟,避免请求过于密集
if (i + batchSize < members.length) {
await new Promise(resolve => setTimeout(resolve, 100))
}
}
}
// 获取团队数据
const fetchTeamData = useCallback(async () => {
if (!dealerUser?.userId && !dealerId) return
try {
setLoading(true)
console.log(dealerId, 'dealerId>>>>>>>>>')
// 获取团队成员关系
const refereeResult = await listShopDealerReferee({
dealerId: dealerId ? dealerId : dealerUser?.userId
})
if (refereeResult) {
console.log('团队成员原始数据:', refereeResult)
// 处理团队成员数据
const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({
...member,
name: `${member.userId}`,
orderCount: 0,
commission: '0.00',
status: 'active' as const,
subMembers: 0,
joinTime: member.createTime
}))
// 先显示基础数据,然后异步加载详细统计
setTeamMembers(processedMembers)
setLoading(false)
// 异步加载每个成员的详细统计数据
loadMemberStats(processedMembers)
}
} catch (error) {
console.error('获取团队数据失败:', error)
Taro.showToast({
title: '获取团队数据失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId, dealerId])
// 查看下级成员
const getNextUser = (item: TeamMemberWithStats) => {
// 检查层级限制最多只能查看2层levelStack.length >= 1 表示已经是第2层了
if (levelStack.length >= 1) {
return
}
// 如果没有下级成员,不允许点击
if (!item.subMembers || item.subMembers === 0) {
return
}
console.log('点击用户:', item.userId, item.name)
// 将当前层级信息推入栈中
const currentLevel: LevelInfo = {
dealerId: dealerId || dealerUser?.userId || 0,
dealerName: currentDealerName || (dealerId ? '上级' : dealerUser?.realName || '我'),
level: levelStack.length
}
setLevelStack(prev => [...prev, currentLevel])
// 切换到下级
setDealerId(item.userId)
setCurrentDealerName(item.nickname || item.dealerName || `用户${item.userId}`)
}
// 返回上一层
const goBack = () => {
if (levelStack.length === 0) {
// 如果栈为空,返回首页或上一页
Taro.navigateBack()
return
}
// 从栈中弹出上一层信息
const prevLevel = levelStack[levelStack.length - 1]
setLevelStack(prev => prev.slice(0, -1))
if (prevLevel.dealerId === (dealerUser?.userId || 0)) {
// 返回到根层级
setDealerId(undefined)
setCurrentDealerName('')
} else {
setDealerId(prevLevel.dealerId)
setCurrentDealerName(prevLevel.dealerName || '')
}
}
// 一键拨打
const makePhoneCall = (phone: string) => {
Taro.makePhoneCall({
phoneNumber: phone,
fail: () => {
Taro.showToast({
title: '拨打取消',
icon: 'error'
});
}
});
};
// 别名备注
const editAlias = (item: any, index: number) => {
Taro.showModal({
title: '备注',
// @ts-ignore
editable: true,
placeholderText: '真实姓名',
content: item.alias || '',
success: async (res: any) => {
if (res.confirm && res.content !== undefined) {
try {
// 更新跟进情况
await updateUser({
userId: item.userId,
alias: res.content.trim()
});
teamMembers[index].alias = res.content.trim()
setTeamMembers(teamMembers)
} catch (error) {
console.error('备注失败:', error);
Taro.showToast({
title: '备注失败,请重试',
icon: 'error'
});
}
}
}
});
};
// 发送消息
const sendMessage = (item: TeamMemberWithStats) => {
return navTo(`/user/chat/message/add?id=${item.userId}`, true)
}
// 监听数据变化,获取团队数据
useEffect(() => {
if (dealerUser?.userId || dealerId) {
fetchTeamData().then()
}
}, [fetchTeamData])
// 初始化当前用户名称
useEffect(() => {
if (!dealerId && dealerUser?.realName && !currentDealerName) {
setCurrentDealerName(dealerUser.realName)
}
}, [dealerUser, dealerId, currentDealerName])
const renderMemberItem = (member: TeamMemberWithStats, index: number) => {
// 判断是否可以点击:有下级成员且未达到层级限制
const canClick = member.subMembers && member.subMembers > 0 && levelStack.length < 1
// 判断是否显示手机号只有本级levelStack.length === 0才显示
const showPhone = levelStack.length === 0
// 判断数据是否还在加载中初始值都是0或'0.00'
const isStatsLoading = member.orderCount === 0 && member.commission === '0.00' && member.subMembers === 0
return (
<View
key={member.id}
className={`bg-white rounded-lg p-4 mb-3 shadow-sm ${
canClick ? 'cursor-pointer' : 'cursor-default opacity-75'
}`}
onClick={() => getNextUser(member)}
>
<View className="flex items-center mb-3">
<Avatar
size="40"
src={member.avatar}
className="mr-3"
/>
<View className="flex-1">
<View className="flex items-center justify-between mb-1">
<View className="flex items-center">
<Space>
{member.alias ? <Text className="font-semibold text-blue-700 mr-2">{member.alias}</Text> :
<Text className="font-semibold text-gray-800 mr-2">{member.nickname}</Text>}
{/*别名备注*/}
<Edit size={16} className={'text-blue-500 mr-2'} onClick={(e) => {
e.stopPropagation()
editAlias(member, index)
}}/>
{/*发送消息*/}
<Message size={16} className={'text-orange-500 mr-2'} onClick={(e) => {
e.stopPropagation()
sendMessage(member)
}}/>
</Space>
</View>
{/* 显示手机号(仅本级可见) */}
{showPhone && member.phone && (
<Text className="text-sm text-gray-500 hidden" onClick={(e) => {
e.stopPropagation();
makePhoneCall(member.phone || '');
}}>
{member.phone}
<Phone size={12} className="ml-1 text-green-500"/>
</Text>
)}
</View>
<Text className="text-xs text-gray-500">
{member.joinTime}
</Text>
</View>
</View>
<View className="grid grid-cols-3 gap-4 text-center">
<Space>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-sm font-semibold text-blue-600">
{isStatsLoading ? '-' : member.orderCount}
</Text>
</Space>
<Space>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-sm font-semibold text-green-600">
{isStatsLoading ? '-' : `¥${member.commission}`}
</Text>
</Space>
<Space>
<Text className="text-xs text-gray-500"></Text>
<Text className={`text-sm font-semibold ${
canClick ? 'text-purple-600' : 'text-gray-400'
}`}>
{isStatsLoading ? '-' : (member.subMembers || 0)}
</Text>
</Space>
</View>
</View>
)
}
const renderOverview = () => (
<View className="rounded-xl p-4">
<View
className={'bg-white rounded-lg py-2 px-4 mb-3 shadow-sm text-right text-sm font-medium flex justify-between items-center'}>
<Text className="text-lg font-semibold"></Text>
<Text className={'text-gray-500 '}>{teamMembers.length}</Text>
</View>
{teamMembers.map(renderMemberItem)}
</View>
)
// 渲染顶部导航栏
const renderHeader = () => {
if (levelStack.length === 0) return null
return (
<View className="bg-white p-4 mb-3 shadow-sm">
<View className="flex items-center justify-between">
<View className="flex items-center">
<Text className="text-lg font-semibold">
{currentDealerName}
</Text>
</View>
<Button
size="small"
type="primary"
onClick={goBack}
className="bg-blue-500"
>
</Button>
</View>
</View>
)
}
if (!dealerUser) {
return (
<Space className="flex items-center justify-center">
<Empty description="您还不是业务人员" style={{
backgroundColor: 'transparent'
}} actions={[{text: '立即申请', onClick: () => navTo(`/dealer/apply/add`, true)}]}
/>
</Space>
)
}
return (
<>
{renderHeader()}
{loading ? (
<View className="flex items-center justify-center mt-20">
<Text className="text-gray-500">...</Text>
</View>
) : teamMembers.length > 0 ? (
renderOverview()
) : (
<View className="flex items-center justify-center mt-20">
<Empty description="暂无成员" style={{
backgroundColor: 'transparent'
}}/>
</View>
)}
<FixedButton text={'立即邀请'} onClick={() => navTo(`/dealer/qrcode/index`, true)}/>
</>
)
}
export default DealerTeam;

View File

@@ -1,80 +0,0 @@
import React, { useState } from 'react'
import { View, Text } from '@tarojs/components'
import { Tabs, Button } from '@nutui/nutui-react-taro'
/**
* 提现功能调试组件
* 用于测试 Tabs 组件的点击和切换功能
*/
const WithdrawDebug: React.FC = () => {
const [activeTab, setActiveTab] = useState<string | number>('0')
const [clickCount, setClickCount] = useState(0)
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换:', { from: activeTab, to: value, type: typeof value })
setActiveTab(value)
setClickCount(prev => prev + 1)
}
// 手动切换测试
const manualSwitch = (tab: string | number) => {
console.log('手动切换到:', tab)
setActiveTab(tab)
setClickCount(prev => prev + 1)
}
return (
<View className="bg-gray-50 min-h-screen p-4">
<View className="bg-white rounded-lg p-4 mb-4">
<Text className="text-lg font-bold mb-2"></Text>
<Text className="block mb-1">Tab: {String(activeTab)}</Text>
<Text className="block mb-1">: {clickCount}</Text>
<Text className="block mb-1">Tab类型: {typeof activeTab}</Text>
</View>
<View className="bg-white rounded-lg p-4 mb-4">
<Text className="text-lg font-bold mb-2"></Text>
<View className="flex gap-2">
<Button size="small" onClick={() => manualSwitch('0')}>
</Button>
<Button size="small" onClick={() => manualSwitch('1')}>
</Button>
</View>
</View>
<View className="bg-white rounded-lg">
<Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.TabPane title="申请提现" value="0">
<View className="p-4">
<Text className="text-center text-gray-600"></Text>
<Text className="text-center text-sm text-gray-400 mt-2">
Tab值: {String(activeTab)}
</Text>
</View>
</Tabs.TabPane>
<Tabs.TabPane title="提现记录" value="1">
<View className="p-4">
<Text className="text-center text-gray-600"></Text>
<Text className="text-center text-sm text-gray-400 mt-2">
Tab值: {String(activeTab)}
</Text>
</View>
</Tabs.TabPane>
</Tabs>
</View>
<View className="bg-white rounded-lg p-4 mt-4">
<Text className="text-lg font-bold mb-2"></Text>
<Text className="text-sm text-gray-500">
</Text>
</View>
</View>
)
}
export default WithdrawDebug

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '提现申请'
})

View File

@@ -1,642 +0,0 @@
import React, {useState, useRef, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {
Space,
Button,
Form,
Input,
CellGroup,
Tabs,
Tag,
Empty,
Loading,
PullToRefresh
} from '@nutui/nutui-react-taro'
import {Wallet} from '@nutui/icons-react-taro'
import {businessGradients} from '@/styles/gradients'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {myUserVerify} from '@/api/system/userVerify'
import {goTo} from '@/utils/navigation'
import {
pageShopDealerWithdraw,
addShopDealerWithdraw,
receiveShopDealerWithdraw,
receiveSuccessShopDealerWithdraw
} from '@/api/shop/shopDealerWithdraw'
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
accountDisplay?: string
// Backend may include these fields for WeChat "confirm receipt" flow after approval.
package_info?: string
packageInfo?: string
package?: string
}
const extractPackageInfo = (result: unknown): string | null => {
if (typeof result === 'string') return result
if (!result || typeof result !== 'object') return null
const r = result as any
return (
r.package_info ??
r.packageInfo ??
r.package ??
null
)
}
const canRequestMerchantTransferConfirm = (): boolean => {
try {
if (typeof (Taro as any).getEnv === 'function' && (Taro as any).ENV_TYPE) {
const env = (Taro as any).getEnv()
if (env !== (Taro as any).ENV_TYPE.WEAPP) return false
}
const api =
(globalThis as any).wx?.requestMerchantTransfer ||
(Taro as any).requestMerchantTransfer
return typeof api === 'function'
} catch {
return false
}
}
const requestMerchantTransferConfirm = (packageInfo: string): Promise<any> => {
if (!canRequestMerchantTransferConfirm()) {
return Promise.reject(new Error('请在微信小程序内完成收款确认'))
}
// Backend may wrap/format base64 with newlines; WeChat API requires a clean string.
const cleanPackageInfo = String(packageInfo).replace(/\s+/g, '')
const api =
(globalThis as any).wx?.requestMerchantTransfer ||
(Taro as any).requestMerchantTransfer
if (typeof api !== 'function') {
return Promise.reject(new Error('当前环境不支持商家转账收款确认(缺少 requestMerchantTransfer'))
}
return new Promise((resolve, reject) => {
api({
// WeChat API uses `package`, backend returns `package_info`.
package: cleanPackageInfo,
mchId: '1737910695',
appId: 'wxad831ba00ad6a026',
success: (res: any) => resolve(res),
fail: (err: any) => reject(err)
})
})
}
// Some backends may return money fields as number; keep internal usage always as string.
const normalizeMoneyString = (money: unknown) => {
if (money === null || money === undefined || money === '') return '0.00'
return typeof money === 'string' ? money : String(money)
}
const DealerWithdraw: React.FC = () => {
const [activeTab, setActiveTab] = useState<string | number>('0')
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [submitting, setSubmitting] = useState<boolean>(false)
const [claimingId, setClaimingId] = useState<number | null>(null)
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
const formRef = useRef<any>(null)
const {dealerUser} = useDealerUser()
const [verifyStatus, setVerifyStatus] = useState<'unknown' | 'verified' | 'unverified' | 'pending' | 'rejected'>('unknown')
const [verifyStatusText, setVerifyStatusText] = useState<string>('')
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换到:', value)
setActiveTab(value)
// 如果切换到提现记录页面,刷新数据
if (String(value) === '1') {
fetchWithdrawRecords()
}
}
// 获取可提现余额
const fetchBalance = useCallback(async () => {
console.log(dealerUser, 'dealerUser...')
try {
setAvailableAmount(normalizeMoneyString(dealerUser?.money))
} catch (error) {
console.error('获取余额失败:', error)
}
}, [dealerUser])
// 获取提现记录
const fetchWithdrawRecords = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
const result = await pageShopDealerWithdraw({
page: 1,
limit: 100,
userId: dealerUser.userId
})
if (result?.list) {
const processedRecords = result.list.map(record => ({
...record,
accountDisplay: getAccountDisplay(record)
}))
setWithdrawRecords(processedRecords)
}
} catch (error) {
console.error('获取提现记录失败:', error)
Taro.showToast({
title: '获取提现记录失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId])
// 格式化账户显示
const getAccountDisplay = (record: ShopDealerWithdraw) => {
if (record.payType === 10) {
return '微信钱包'
} else if (record.payType === 20 && record.alipayAccount) {
return `支付宝(${record.alipayAccount.slice(-4)})`
} else if (record.payType === 30 && record.bankCard) {
return `${record.bankName || '银行卡'}(尾号${record.bankCard.slice(-4)})`
}
return '未知账户'
}
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
await Promise.all([fetchBalance(), fetchWithdrawRecords()])
setRefreshing(false)
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchBalance().then()
fetchWithdrawRecords().then()
}
}, [fetchBalance, fetchWithdrawRecords])
// 判断实名认证状态:提现前必须完成实名认证(已通过)
const fetchVerifyStatus = useCallback(async () => {
// Fast path: some pages store this flag after login.
if (String(Taro.getStorageSync('Certification')) === '1') {
setVerifyStatus('verified')
setVerifyStatusText('已实名认证')
return
}
try {
const r = await myUserVerify({})
if (!r) {
setVerifyStatus('unverified')
setVerifyStatusText('未实名认证')
return
}
const s = Number((r as any).status)
const st = String((r as any).statusText || '')
// Common convention in this project: 0审核中/待审核, 1已通过, 2已驳回
if (s === 1) {
setVerifyStatus('verified')
setVerifyStatusText(st || '已实名认证')
return
}
if (s === 0) {
setVerifyStatus('pending')
setVerifyStatusText(st || '审核中')
return
}
if (s === 2) {
setVerifyStatus('rejected')
setVerifyStatusText(st || '已驳回')
return
}
setVerifyStatus('unverified')
setVerifyStatusText(st || '未实名认证')
} catch (e) {
console.warn('获取实名认证状态失败,将按未认证处理:', e)
setVerifyStatus('unverified')
setVerifyStatusText('未实名认证')
}
}, [])
useEffect(() => {
if (!dealerUser?.userId) return
fetchVerifyStatus().then()
}, [dealerUser?.userId, fetchVerifyStatus])
const getStatusText = (status?: number) => {
switch (status) {
case 40:
return '已到账'
case 20:
return '待领取'
case 10:
return '待审核'
case 30:
return '已驳回'
default:
return '未知'
}
}
const getStatusColor = (status?: number) => {
switch (status) {
case 40:
return 'success'
case 20:
return 'info'
case 10:
return 'warning'
case 30:
return 'danger'
default:
return 'default'
}
}
const handleSubmit = async (values: any) => {
if (!dealerUser?.userId) {
Taro.showToast({
title: '用户信息获取失败',
icon: 'error'
})
return
}
if (verifyStatus !== 'verified') {
Taro.showToast({
title: '请先完成实名认证',
icon: 'none'
})
return
}
// 验证提现金额
const amount = parseFloat(String(values.amount))
const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, ''))
if (isNaN(amount) || amount <= 0) {
Taro.showToast({
title: '请输入有效的提现金额',
icon: 'error'
})
return
}
if (amount < 100) {
// Taro.showToast({
// title: '最低提现金额为100元',
// icon: 'error'
// })
// return
}
if (amount > available) {
Taro.showToast({
title: '提现金额超过可用余额',
icon: 'error'
})
return
}
try {
setSubmitting(true)
const withdrawData: ShopDealerWithdraw = {
userId: dealerUser.userId,
money: values.amount,
// Only support WeChat wallet withdrawals.
payType: 10,
platform: 'MiniProgram'
}
// Security flow:
// 1) user submits => applyStatus=10 (待审核)
// 2) backend审核通过 => applyStatus=20 (待领取)
// 3) user goes to records to "领取" => applyStatus=40 (已到账)
await addShopDealerWithdraw(withdrawData)
Taro.showToast({title: '提现申请已提交,等待审核', icon: 'success'})
// 重置表单
formRef.current?.resetFields()
// 刷新数据
await handleRefresh()
// 切换到提现记录页面
setActiveTab('1')
} catch (error: any) {
console.error('提现申请失败:', error)
Taro.showToast({
title: error.message || '提现申请失败',
icon: 'error'
})
} finally {
setSubmitting(false)
}
}
const handleClaim = async (record: WithdrawRecordWithDetails) => {
if (!record?.id) {
Taro.showToast({title: '记录不存在', icon: 'error'})
return
}
if (record.applyStatus !== 20) {
Taro.showToast({title: '当前状态不可领取', icon: 'none'})
return
}
if (record.payType !== 10) {
Taro.showToast({title: '仅支持微信提现领取', icon: 'none'})
return
}
if (claimingId !== null) return
try {
setClaimingId(record.id)
if (!canRequestMerchantTransferConfirm()) {
throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作')
}
const receiveResult = await receiveShopDealerWithdraw(record.id)
const packageInfo = extractPackageInfo(receiveResult)
if (!packageInfo) {
throw new Error('后台未返回 package_info无法领取请联系管理员')
}
try {
await requestMerchantTransferConfirm(packageInfo)
} catch (e: any) {
const msg = String(e?.errMsg || e?.message || '')
if (/cancel/i.test(msg)) {
Taro.showToast({title: '已取消领取', icon: 'none'})
return
}
throw new Error(msg || '领取失败,请稍后重试')
}
try {
await receiveSuccessShopDealerWithdraw(record.id)
Taro.showToast({title: '领取成功', icon: 'success'})
} catch (e: any) {
console.warn('领取成功,但状态同步失败:', e)
Taro.showToast({title: '已收款,状态更新失败,请稍后刷新', icon: 'none'})
} finally {
await handleRefresh()
}
} catch (e: any) {
console.error('领取失败:', e)
Taro.showToast({title: e?.message || '领取失败', icon: 'error'})
} finally {
setClaimingId(null)
}
}
const quickAmounts = ['100', '300', '500', '1000']
const setQuickAmount = (amount: string) => {
formRef.current?.setFieldsValue({amount})
}
const setAllAmount = () => {
formRef.current?.setFieldsValue({amount: normalizeMoneyString(availableAmount).replace(/,/g, '')})
}
// 格式化金额
const formatMoney = (money?: unknown) => {
const n = parseFloat(normalizeMoneyString(money).replace(/,/g, ''))
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
}
const goVerify = () => {
goTo('/user/userVerify/index')
}
const renderWithdrawForm = () => (
<View>
{(verifyStatus === 'unverified' || verifyStatus === 'pending' || verifyStatus === 'rejected') && (
<View className="rounded-lg bg-white px-4 py-3 mb-4 mx-4">
<View className="flex items-center justify-between">
<View className="flex flex-col">
<Text className="text-sm text-red-500"></Text>
{verifyStatusText ? (
<Text className="text-xs text-gray-500 mt-1">{verifyStatusText}</Text>
) : null}
</View>
<Text className="text-sm text-blue-600" onClick={goVerify}>
</Text>
</View>
</View>
)}
{/* 余额卡片 */}
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
background: businessGradients.dealer.header
}}>
{/* 装饰背景 - 小程序兼容版本 */}
<View className="absolute top-0 right-0 w-24 h-24 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
right: '-12px',
top: '-12px'
}}></View>
<View className="flex items-center justify-between relative z-10">
<View className={'flex flex-col'}>
<Text className="text-2xl font-bold text-white">{formatMoney(dealerUser?.money)}</Text>
<Text className="text-white text-opacity-80 text-sm mb-1"></Text>
</View>
<View className="p-3 rounded-full" style={{
background: 'rgba(255, 255, 255, 0.2)'
}}>
<Wallet color="white" size="32"/>
</View>
</View>
<View className="mt-4 pt-4 relative z-10" style={{
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
}}>
<Text className="text-white text-opacity-80 text-xs">
</Text>
</View>
</View>
<Form
ref={formRef}
onFinish={handleSubmit}
labelPosition="top"
>
<CellGroup>
<Form.Item name="amount" label="提现金额" required>
<Input
placeholder="请输入提现金额"
type="number"
/>
</Form.Item>
{/* 快捷金额 */}
<View className="px-4 py-2">
<Text className="text-sm text-gray-600 mb-2"></Text>
<View className="flex flex-wrap gap-2">
{quickAmounts.map(amount => (
<Button
key={amount}
size="small"
fill="outline"
onClick={() => setQuickAmount(amount)}
>
{amount}
</Button>
))}
<Button
size="small"
fill="outline"
onClick={setAllAmount}
>
</Button>
</View>
</View>
<View className="px-4 py-2">
<Text className="text-sm text-red-500">
1.
2.
3.
</Text>
</View>
</CellGroup>
<View className="mt-6 px-4">
<Button
block
type="primary"
nativeType="submit"
loading={submitting}
disabled={submitting || verifyStatus !== 'verified'}
>
{submitting ? '提交中...' : '申请提现'}
</Button>
</View>
</Form>
</View>
)
const renderWithdrawRecords = () => {
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.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>
) : withdrawRecords.length > 0 ? (
withdrawRecords.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>
<Text className="font-semibold text-gray-800 mb-1">
¥{record.money}
</Text>
{/*<Text className="text-sm text-gray-500">*/}
{/* 提现账户:{record.accountDisplay}*/}
{/*</Text>*/}
</Space>
<Tag background="#999999" type={getStatusColor(record.applyStatus)} plain>
{getStatusText(record.applyStatus)}
</Tag>
</View>
{record.applyStatus === 20 && record.payType === 10 && (
<View className="flex mb-5 justify-center">
<Button
size="small"
type="primary"
loading={claimingId === record.id}
disabled={claimingId !== null}
onClick={() => handleClaim(record)}
>
</Button>
</View>
)}
<View className="flex justify-between items-center">
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.auditTime && (
<Text className="block mt-1">
{record.auditTime}
</Text>
)}
{record.rejectReason && (
<Text className="block mt-1 text-red-500">
{record.rejectReason}
</Text>
)}
</View>
</View>
</View>
))
) : (
<Empty description="暂无提现记录"/>
)}
</View>
</PullToRefresh>
)
}
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
<Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.TabPane title="申请提现" value="0">
{renderWithdrawForm()}
</Tabs.TabPane>
<Tabs.TabPane title="提现记录" value="1">
{renderWithdrawRecords()}
</Tabs.TabPane>
</Tabs>
</View>
)
}
export default DealerWithdraw

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '购物车',
navigationStyle: 'custom'
})

View File

@@ -1,31 +0,0 @@
// 购物车页面样式
.cart-page {
// 当购物车为空时,设置透明背景
&.empty {
page {
background-color: transparent !important;
}
.cart-empty-container {
background-color: transparent !important;
}
}
}
// 空购物车容器样式
.cart-empty-container {
background-color: transparent !important;
// 确保 Empty 组件及其子元素也是透明的
.nut-empty {
background-color: transparent !important;
}
.nut-empty__image {
background-color: transparent !important;
}
.nut-empty__description {
background-color: transparent !important;
}
}

View File

@@ -1,352 +0,0 @@
import {useEffect, useState} from "react";
import Taro, {useShareAppMessage, useDidShow} from '@tarojs/taro';
import {
NavBar,
Checkbox,
Image,
InputNumber,
Button,
Empty,
Divider,
ConfigProvider
} from '@nutui/nutui-react-taro';
import {Del} from '@nutui/icons-react-taro';
import {View} from '@tarojs/components';
import {CartItem, useCart} from "@/hooks/useCart";
import './cart.scss';
import { ensureLoggedIn } from '@/utils/auth'
function Cart() {
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
const [selectedItems, setSelectedItems] = useState<number[]>([]);
const [isAllSelected, setIsAllSelected] = useState(false);
const {
cartItems,
cartCount,
updateQuantity,
removeFromCart,
clearCart,
loadCartFromStorage
} = useCart();
// InputNumber 主题配置
const customTheme = {
nutuiInputnumberButtonWidth: '28px',
nutuiInputnumberButtonHeight: '28px',
nutuiInputnumberInputWidth: '40px',
nutuiInputnumberInputHeight: '28px',
nutuiInputnumberInputBorderRadius: '4px',
nutuiInputnumberButtonBorderRadius: '4px',
}
useShareAppMessage(() => {
return {
title: '购物车 - 易赊宝',
success: function () {
console.log('分享成功');
},
fail: function () {
console.log('分享失败');
}
};
});
// 页面显示时刷新购物车数据
useDidShow(() => {
loadCartFromStorage();
});
useEffect(() => {
Taro.getSystemInfo({
success: (res) => {
setStatusBarHeight(res.statusBarHeight || 0);
},
});
// 设置导航栏背景色
Taro.setNavigationBarColor({
backgroundColor: '#ffffff',
frontColor: 'black',
});
}, []);
// 根据购物车状态动态设置页面背景色
useEffect(() => {
if (cartItems.length === 0) {
// 购物车为空时设置透明背景
Taro.setBackgroundColor({
backgroundColor: 'transparent'
});
} else {
// 有商品时恢复默认背景
Taro.setBackgroundColor({
backgroundColor: '#f5f5f5'
});
}
}, [cartItems.length]);
// 处理单个商品选择
const handleItemSelect = (goodsId: number, checked: boolean) => {
if (checked) {
setSelectedItems([...selectedItems, goodsId]);
} else {
setSelectedItems(selectedItems.filter(id => id !== goodsId));
setIsAllSelected(false);
}
};
// 处理全选
const handleSelectAll = (checked: boolean) => {
setIsAllSelected(checked);
if (checked) {
setSelectedItems(cartItems.map((item: CartItem) => item.goodsId));
} else {
setSelectedItems([]);
}
};
// 更新商品数量
const handleQuantityChange = (goodsId: number, value: number) => {
updateQuantity(goodsId, value);
};
// 删除商品
const handleRemoveItem = (goodsId: number) => {
Taro.showModal({
title: '确认删除',
content: '确定要删除这个商品吗?',
success: (res) => {
if (res.confirm) {
removeFromCart(goodsId);
setSelectedItems(selectedItems.filter(id => id !== goodsId));
}
}
});
};
// 计算选中商品的总价
const getSelectedTotalPrice = () => {
return cartItems
.filter((item: CartItem) => selectedItems.includes(item.goodsId))
.reduce((total: number, item: CartItem) => total + (parseFloat(item.price) * item.quantity), 0)
.toFixed(2);
};
// 去结算
const handleCheckout = () => {
if (selectedItems.length === 0) {
Taro.showToast({
title: '请选择要结算的商品',
icon: 'none'
});
return;
}
// 获取选中的商品
const selectedCartItems = cartItems.filter((item: CartItem) =>
selectedItems.includes(item.goodsId)
);
// 将选中的商品信息存储到本地,供结算页面使用
Taro.setStorageSync('checkout_items', JSON.stringify(selectedCartItems));
// 未登录则引导去注册/登录;登录后回到购物车结算页
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
// 跳转到购物车结算页面
Taro.navigateTo({
url: '/shop/orderConfirmCart/index'
});
};
// 检查是否全选
useEffect(() => {
if (cartItems.length > 0 && selectedItems.length === cartItems.length) {
setIsAllSelected(true);
} else {
setIsAllSelected(false);
}
}, [selectedItems, cartItems]);
if (cartItems.length === 0) {
return (
<>
<NavBar
fixed={true}
style={{marginTop: `${statusBarHeight}px`}}
right={
cartItems.length > 0 && (
<Button
size="small"
type="primary"
fill="none"
onClick={() => {
Taro.showModal({
title: '确认清空',
content: '确定要清空购物车吗?',
success: (res) => {
if (res.confirm) {
clearCart();
setSelectedItems([]);
}
}
});
}}
>
</Button>
)
}
>
<span className="text-lg">({cartCount})</span>
</NavBar>
{/* 垂直居中的空状态容器 */}
<View
className="flex items-center justify-center"
style={{
height: `calc(100vh - ${statusBarHeight + 150}px)`,
paddingTop: `${statusBarHeight + 50}px`,
backgroundColor: 'transparent'
}}
>
<Empty
description="购物车空空如也"
actions={[{ text: '去逛逛' }]}
style={{
backgroundColor: 'transparent'
}}
onClick={() => Taro.switchTab({ url: '/pages/index/index' })}
/>
</View>
</>
)
}
return (
<>
<View style={{backgroundColor: '#f6f6f6', height: `${statusBarHeight}px`}}
className="fixed z-10 top-0 w-full"></View>
<NavBar
fixed={true}
style={{marginTop: `${statusBarHeight}px`}}
right={
cartItems.length > 0 && (
<Button
size="small"
type="primary"
fill="none"
onClick={() => {
Taro.showModal({
title: '确认清空',
content: '确定要清空购物车吗?',
success: (res) => {
if (res.confirm) {
clearCart();
setSelectedItems([]);
}
}
});
}}
>
</Button>
)
}
>
<span className="text-lg">({cartCount})</span>
</NavBar>
{/* 购物车内容 */}
<View className="pt-24">
{/* 商品列表 */}
<View className="bg-white">
{cartItems.map((item: CartItem, index: number) => (
<View key={item.goodsId}>
<View className="bg-white px-4 py-3 flex items-center gap-3">
{/* 选择框 */}
<Checkbox
checked={selectedItems.includes(item.goodsId)}
onChange={(checked) => handleItemSelect(item.goodsId, checked)}
/>
{/* 商品图片 */}
<Image
src={item.image}
width="80"
height="80"
lazyLoad={false}
radius="8"
className="flex-shrink-0"
/>
{/* 商品信息 */}
<View className="flex-1 min-w-0">
<View className="text-lg font-bold text-gray-900 truncate mb-1">
{item.name}
</View>
<View className="flex items-center justify-between">
<View className={'flex text-red-500 text-xl items-baseline'}>
<span className={'text-xs'}></span>
<span className={'font-bold text-lg'}>{item.price}</span>
</View>
<ConfigProvider theme={customTheme}>
<InputNumber
value={item.quantity}
min={1}
onChange={(value) => handleQuantityChange(item.goodsId, Number(value))}
/>
</ConfigProvider>
<Del className={'text-red-500'} size={14} onClick={() => handleRemoveItem(item.goodsId)}/>
</View>
</View>
</View>
{index < cartItems.length - 1 && <Divider/>}
</View>
))}
</View>
{/* 底部结算栏 */}
<View
className="fixed z-50 bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3 safe-area-bottom">
<View className="flex items-center justify-between">
<View className="flex items-center gap-3">
<Checkbox
checked={isAllSelected}
onChange={handleSelectAll}
>
</Checkbox>
<View className="text-sm text-gray-600">
{selectedItems.length}
</View>
</View>
<View className="flex items-center gap-4">
<View className="text-right">
<View className="text-xs text-gray-500">:</View>
<div className={'flex text-red-500 text-xl items-baseline'}>
<span className={'text-xs'}></span>
<span className={'font-bold text-lg'}>{getSelectedTotalPrice()}</span>
</div>
</View>
<Button
type="primary"
size="large"
disabled={selectedItems.length === 0}
onClick={handleCheckout}
className="px-6"
>
({selectedItems.length})
</Button>
</View>
</View>
</View>
{/* 底部安全区域占位 */}
<View className="h-20"></View>
</View>
</>
);
}
export default Cart;

View File

@@ -1,5 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '网点',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff'
})

View File

@@ -1,144 +0,0 @@
page {
background: #f7f7f7;
}
.sitePage {
min-height: 100vh;
background: linear-gradient(180deg, #fde8ea 0%, #f7f7f7 320rpx, #f7f7f7 100%);
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
}
.searchArea {
padding: 22rpx 24rpx 18rpx;
}
.searchBox {
height: 86rpx;
background: #fff;
border: 2rpx solid #b51616;
border-radius: 12rpx;
display: flex;
align-items: center;
overflow: hidden;
}
.searchInput {
flex: 1;
height: 86rpx;
padding: 0 20rpx;
font-size: 30rpx;
color: #222;
}
.searchPlaceholder {
color: #9e9e9e;
font-size: 30rpx;
}
.searchIconWrap {
width: 88rpx;
height: 86rpx;
display: flex;
align-items: center;
justify-content: center;
}
.siteList {
padding: 0 24rpx 24rpx;
}
.siteCard {
background: #fff;
border-radius: 18rpx;
padding: 22rpx 22rpx 18rpx;
margin-top: 18rpx;
box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.04);
}
.siteCardInner {
display: flex;
align-items: stretch;
}
.siteInfo {
flex: 1;
padding-right: 10rpx;
}
.siteRow {
display: flex;
align-items: flex-start;
padding: 10rpx 0;
}
.siteRowTop {
padding-top: 2rpx;
padding-bottom: 14rpx;
}
.siteLabel {
width: 170rpx;
flex: 0 0 170rpx;
color: #9a9a9a;
font-size: 30rpx;
line-height: 1.6;
}
.siteValue {
flex: 1;
color: #222;
font-size: 30rpx;
line-height: 1.6;
word-break: break-all;
}
.siteValueStrong {
font-weight: 700;
}
.siteDivider {
height: 2rpx;
background: #ededed;
}
.siteSide {
width: 120rpx;
flex: 0 0 160rpx;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
padding-left: 12rpx;
}
.navArrow {
width: 14rpx;
height: 14rpx;
border-top: 5rpx solid #e60012;
border-right: 5rpx solid #e60012;
border-radius: 4rpx;
transform: rotate(45deg);
margin-right: 6rpx;
}
.distanceText {
margin-top: 18rpx;
font-size: 28rpx;
color: #e60012;
font-weight: 700;
}
.emptyWrap {
padding: 40rpx 0;
display: flex;
justify-content: center;
}
.emptyText {
font-size: 28rpx;
color: #9a9a9a;
}
.bottomSafe {
height: 20rpx;
}

View File

@@ -1,258 +0,0 @@
import { useMemo, useRef, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import {Input, Text, View} from '@tarojs/components'
import { Empty, InfiniteLoading, Loading, PullToRefresh } from '@nutui/nutui-react-taro'
import {Search} from '@nutui/icons-react-taro'
import { pageShopStore } from '@/api/shop/shopStore'
import type { ShopStore } from '@/api/shop/shopStore/model'
import { getCurrentLngLat } from '@/utils/location'
import './find.scss'
const PAGE_SIZE = 10
type LngLat = { lng: number; lat: number }
type ShopStoreView = ShopStore & { __distanceMeter?: number }
const parseLngLat = (raw: string | undefined): LngLat | null => {
const text = (raw || '').trim()
if (!text) return null
const parts = text.split(/[,\s]+/).filter(Boolean)
if (parts.length < 2) return null
const a = Number(parts[0])
const b = Number(parts[1])
if (!Number.isFinite(a) || !Number.isFinite(b)) return null
// Accept both "lng,lat" and "lat,lng".
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180
if (looksLikeLngLat) return { lng: a, lat: b }
if (looksLikeLatLng) return { lng: b, lat: a }
return null
}
const distanceMeters = (a: LngLat, b: LngLat) => {
const toRad = (x: number) => (x * Math.PI) / 180
const R = 6371000
const dLat = toRad(b.lat - a.lat)
const dLng = toRad(b.lng - a.lng)
const lat1 = toRad(a.lat)
const lat2 = toRad(b.lat)
const sin1 = Math.sin(dLat / 2)
const sin2 = Math.sin(dLng / 2)
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)))
}
const formatDistance = (meter: number | undefined) => {
if (!Number.isFinite(meter as number)) return ''
const m = Math.max(0, Math.round(meter as number))
if (m < 1000) return `${m}`
const km = m / 1000
return `${km.toFixed(km >= 10 ? 0 : 1)}km`
}
const Find = () => {
const [keyword, setKeyword] = useState<string>('')
const [storeList, setStoreList] = useState<ShopStoreView[]>([])
const [loading, setLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [userLngLat, setUserLngLat] = useState<LngLat | null>(null)
const pageRef = useRef(1)
const latestListRef = useRef<ShopStoreView[]>([])
const loadingRef = useRef(false)
const coordsRef = useRef<LngLat | null>(null)
const viewList = useMemo<ShopStoreView[]>(() => {
const me = userLngLat
if (!me) return storeList
// Keep backend order; only attach distance for display.
return storeList.map((s) => {
const coords = parseLngLat(s.lngAndLat || s.location)
if (!coords) return s
return { ...s, __distanceMeter: distanceMeters(me, coords) }
})
}, [storeList, userLngLat])
const loadStores = async (isRefresh = true, keywordsOverride?: string) => {
if (loadingRef.current) return
loadingRef.current = true
setLoading(true)
if (isRefresh) {
pageRef.current = 1
latestListRef.current = []
setStoreList([])
setHasMore(true)
}
try {
if (!coordsRef.current) {
const me = await getCurrentLngLat('为您展示附近网点,需要获取定位信息。')
const lng = me ? Number(me.lng) : NaN
const lat = me ? Number(me.lat) : NaN
coordsRef.current = Number.isFinite(lng) && Number.isFinite(lat) ? { lng, lat } : null
setUserLngLat(coordsRef.current)
}
const currentPage = pageRef.current
const kw = (keywordsOverride ?? keyword).trim()
const res = await pageShopStore({
page: currentPage,
limit: PAGE_SIZE,
keywords: kw || undefined
})
const resList = res?.list || []
const nextList = isRefresh ? resList : [...latestListRef.current, ...resList]
latestListRef.current = nextList
setStoreList(nextList)
const count = typeof res?.count === 'number' ? res.count : nextList.length
setHasMore(nextList.length < count)
if (resList.length > 0) {
pageRef.current = currentPage + 1
} else {
setHasMore(false)
}
} catch (e) {
console.error('获取网点列表失败:', e)
Taro.showToast({ title: '获取网点失败', icon: 'none' })
setHasMore(false)
} finally {
loadingRef.current = false
setLoading(false)
}
}
useDidShow(() => {
loadStores(true).then()
})
const onNavigate = (item: ShopStore) => {
const coords = parseLngLat(item.lngAndLat || item.location)
if (!coords) {
Taro.showToast({ title: '网点暂无坐标,无法导航', icon: 'none' })
return
}
Taro.openLocation({
latitude: coords.lat,
longitude: coords.lng,
name: item.name || item.city || '网点',
address: item.address || ''
})
}
const onCall = (phone: string | undefined) => {
const p = (phone || '').trim()
if (!p) {
Taro.showToast({ title: '暂无联系电话', icon: 'none' })
return
}
Taro.makePhoneCall({ phoneNumber: p })
}
const onSearch = () => {
loadStores(true).then()
}
return (
<View className='sitePage'>
<View className='searchArea'>
<View className='searchBox'>
<Input
className='searchInput'
value={keyword}
placeholder='请输入城市名称查询'
placeholderClass='searchPlaceholder'
confirmType='search'
onInput={(e) => setKeyword(e.detail.value)}
onConfirm={onSearch}
/>
<View className='searchIconWrap' onClick={onSearch}>
<Search size={18} color='#b51616' />
</View>
</View>
</View>
<PullToRefresh onRefresh={() => loadStores(true)} headHeight={60}>
<View style={{ height: 'calc(100vh) - 160px', overflowY: 'auto' }} id='store-scroll'>
{viewList.length === 0 && !loading ? (
<View className='emptyWrap'>
<Empty description='暂无网点' style={{ backgroundColor: 'transparent' }} />
</View>
) : (
<View className='siteList'>
<InfiniteLoading
target='store-scroll'
hasMore={hasMore}
onLoadMore={() => loadStores(false)}
loadingText={
<View className='emptyWrap'>
<Loading />
<Text className='emptyText' style={{ marginLeft: '8px' }}>
...
</Text>
</View>
}
loadMoreText={
<View className='emptyWrap'>
<Text className='emptyText'>
{viewList.length === 0 ? '暂无网点' : '没有更多了'}
</Text>
</View>
}
>
{viewList.map((item, idx) => {
const name = item?.name || item?.city || item?.province || '网点'
const contact = item?.managerName || '--'
const distanceText = formatDistance(item?.__distanceMeter)
return (
<View key={String(item?.id ?? `${name}-${idx}`)} className='siteCard'>
<View className='siteCardInner'>
<View className='siteInfo'>
<View className='siteRow siteRowTop'>
<Text className='siteLabel'></Text>
<Text className='siteValue siteValueStrong'>{name}</Text>
</View>
<View className='siteDivider' />
<View className='siteRow'>
<Text className='siteLabel'></Text>
<Text className='siteValue'>{item?.address || '--'}</Text>
</View>
<View className='siteRow'>
<Text className='siteLabel'></Text>
<Text className='siteValue' onClick={() => onCall(item?.phone)}>
{item?.phone || '--'}
</Text>
</View>
<View className='siteRow'>
<Text className='siteLabel'></Text>
<Text className='siteValue'>{contact}</Text>
</View>
</View>
<View className='siteSide' onClick={() => onNavigate(item)}>
<View className='navArrow' />
<Text className='distanceText'>
{distanceText ? `${distanceText}` : '查看导航'}
</Text>
</View>
</View>
</View>
)
})}
</InfiniteLoading>
</View>
)}
</View>
</PullToRefresh>
<View className='bottomSafe' />
</View>
)
}
export default Find

View File

@@ -1,141 +0,0 @@
import {useEffect, useState} from "react";
import {Image} from '@nutui/nutui-react-taro'
import {Share} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components';
import Taro, {useShareAppMessage, useShareTimeline} from "@tarojs/taro";
import {ShopGoods} from "@/api/shop/shopGoods/model";
import {pageShopGoods} from "@/api/shop/shopGoods";
import './BestSellers.scss'
const BestSellers = () => {
const [list, setList] = useState<ShopGoods[]>([])
const [goods, setGoods] = useState<ShopGoods>()
const reload = () => {
pageShopGoods({}).then(res => {
setList(res?.list || []);
})
}
// 处理分享点击
const handleShare = (item: ShopGoods) => {
setGoods(item);
// 显示分享选项菜单
Taro.showActionSheet({
itemList: ['分享给好友', '分享到朋友圈'],
success: (res) => {
if (res.tapIndex === 0) {
// 分享给好友 - 触发转发
Taro.showShareMenu({
withShareTicket: true,
success: () => {
// 提示用户点击右上角分享
Taro.showToast({
title: '请点击右上角分享给好友',
icon: 'none',
duration: 2000
});
}
});
} else if (res.tapIndex === 1) {
// 分享到朋友圈
Taro.showToast({
title: '请点击右上角分享到朋友圈',
icon: 'none',
duration: 2000
});
}
},
fail: (err) => {
console.log('显示分享菜单失败', err);
}
});
}
useEffect(() => {
reload()
}, [])
// 分享给好友
useShareAppMessage(() => {
return {
title: goods?.name || '精选商品',
path: `/shop/goodsDetail/index?id=${goods?.goodsId}`,
imageUrl: goods?.image, // 分享图片
success: function (res: any) {
console.log('分享成功', res);
Taro.showToast({
title: '分享成功',
icon: 'success',
duration: 2000
});
},
fail: function (res: any) {
console.log('分享失败', res);
Taro.showToast({
title: '分享失败',
icon: 'none',
duration: 2000
});
}
};
});
// 分享到朋友圈
useShareTimeline(() => {
return {
title: `${goods?.name || '精选商品'} - 易赊宝`,
path: `/shop/goodsDetail/index?id=${goods?.goodsId}`,
imageUrl: goods?.image
};
});
return (
<>
<View className={'py-1'}>
<View className={'flex flex-col justify-between items-center rounded-lg px-2'}>
{list?.map((item, index) => {
return (
<View key={index} className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
<Image src={item.image} mode={'aspectFit'} lazyLoad={false}
radius="10px 10px 0 0" height="180"
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
<View className={'flex flex-col p-2 rounded-lg'}>
<View>
<View className={'car-no text-sm'}>{item.name}</View>
<View className={'flex justify-between text-xs py-1'}>
<Text className={'text-orange-500'}>{item.comments}</Text>
<Text className={'text-gray-400'}> {item.sales}</Text>
</View>
<View className={'flex justify-between items-center py-2'}>
<View className={'flex text-red-500 text-xl items-baseline'}>
<Text className={'text-xs'}></Text>
<Text className={'font-bold text-2xl'}>{item.price}</Text>
</View>
<View className={'buy-btn'}>
<View className={'cart-icon flex items-center'}>
<View
className={'flex flex-col justify-center items-center text-white px-3 gap-1 text-nowrap whitespace-nowrap cursor-pointer'}
onClick={() => handleShare(item)}
>
<Share size={20}/>
</View>
</View>
<Text className={'text-white pl-4 pr-5'}
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>
</Text>
</View>
</View>
</View>
</View>
</View>
)
})}
</View>
</View>
</>
)
}
export default BestSellers

View File

@@ -1,69 +0,0 @@
import {useEffect, useState} from "react";
import {Tabs, TabPane} from '@nutui/nutui-react-taro'
const list = [
{
title: '今天',
id: 1
},
{
title: '昨天',
id: 2
},
{
title: '过去7天',
id: 3
},
{
title: '过去30天',
id: 4
}
]
const Chart = () => {
const [tapIndex, setTapIndex] = useState<string | number>('0')
const reload = () => {
}
useEffect(() => {
reload()
}, [])
return (
<>
<Tabs
align={'left'}
tabStyle={{position: 'sticky', top: '0px'}}
value={tapIndex}
onChange={(paneKey) => {
setTapIndex(paneKey)
}}
>
{
list?.map((item, index) => {
return (
<TabPane key={index} title={item.title}/>
)
})
}
</Tabs>
{
list?.map((item, index) => {
console.log(item.title)
return (
<div key={index} className={'px-3'}>
{
tapIndex != index ? null :
<div className={'bg-white rounded-lg p-4 flex justify-center items-center text-center text-gray-300'} style={{height: '200px'}}>
线
</div>
}
</div>
)
})
}
</>
)
}
export default Chart

View File

@@ -1,83 +0,0 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'
import {Button} from '@nutui/nutui-react-taro'
import {Target, Scan, Truck} from '@nutui/icons-react-taro'
import {getUserInfo} from "@/api/layout";
import navTo from "@/utils/common";
const ExpirationTime = () => {
const [isAdmin, setIsAdmin] = useState<boolean>(false)
const [roleName, setRoleName] = useState<string>()
const onScanCode = () => {
Taro.scanCode({
onlyFromCamera: true,
scanType: ['qrCode'],
success: (res) => {
console.log(res, 'qrcode...')
Taro.navigateTo({url: '/hjm/query?id=' + res.result})
},
fail: (res) => {
console.log(res, '扫码失败')
Taro.showToast({
title: '扫码失败',
icon: 'none',
duration: 2000
})
}
})
}
const navToCarList = () => {
if (isAdmin) {
navTo('/hjm/list', true)
}
}
useEffect(() => {
getUserInfo().then((data) => {
if (data) {
if(data.certification){
setIsAdmin( true)
}
data.roles?.map((item, index) => {
if (index == 0) {
setRoleName(item.roleCode)
}
})
}
})
}, [])
return (
<div className={'mb-3 fixed top-36 z-20'} style={{width: '96%', marginLeft: '3%'}}>
<div className={'w-full flex justify-around items-center py-3 rounded-lg'}>
<>
<Button size={'large'}
style={{background: 'linear-gradient(to right, #f3f2f7, #805de1)', borderColor: '#f3f2f7'}}
icon={<Truck/>} onClick={navToCarList}></Button>
<Button size={'large'}
style={{background: 'linear-gradient(to right, #fffbe6, #ffc53d)', borderColor: '#f3f2f7'}}
icon={<Scan/>}
onClick={onScanCode}>
</Button>
</>
{
roleName == 'youzheng' && <Button size={'large'} style={{
background: 'linear-gradient(to right, #eaff8f, #7cb305)',
borderColor: '#f3f2f7'
}} icon={<Target/>} onClick={() => Taro.navigateTo({url: '/hjm/fence'})}></Button>
}
{
roleName == 'kuaidiyuan' && <Button size={'large'} style={{
background: 'linear-gradient(to right, #ffa39e, #ff4d4f)',
borderColor: '#f3f2f7'
}} icon={<Target/>} onClick={() => Taro.navigateTo({url: '/hjm/bx/bx-add'})}></Button>
}
</div>
</div>
)
}
export default ExpirationTime

View File

@@ -1,67 +0,0 @@
import {useEffect, useState} from "react";
import {Image} from '@nutui/nutui-react-taro'
import {Share} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {ShopGoods} from "@/api/shop/shopGoods/model";
import {pageShopGoods} from "@/api/shop/shopGoods";
import './GoodsList.scss'
const BestSellers = () => {
const [list, setList] = useState<ShopGoods[]>([])
const reload = () => {
pageShopGoods({}).then(res => {
setList(res?.list || []);
})
}
useEffect(() => {
reload()
}, [])
return (
<>
<div className={'py-3'}>
<div className={'flex flex-wrap justify-between items-start rounded-lg px-2'}>
{list?.map((item, index) => {
return (
<div key={index} className={'flex flex-col rounded-lg bg-white shadow-sm mb-5'} style={{
width: '48%'
}}>
<Image src={item.image} mode={'scaleToFill'} lazyLoad={false}
radius="10px 10px 0 0" height="180"
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
<div className={'flex flex-col p-2 rounded-lg'}>
<div>
<div className={'car-no text-sm'}>{item.name}</div>
<div className={'flex justify-between text-xs py-1'}>
<span className={'text-orange-500'}>{item.comments}</span>
<span className={'text-gray-400'}> {item.sales}</span>
</div>
<div className={'flex justify-between items-center py-2'}>
<div className={'flex text-red-500 text-xl items-baseline'}>
<span className={'text-xs'}></span>
<span className={'font-bold text-2xl'}>{item.price}</span>
</div>
<div className={'buy-btn'}>
<div className={'cart-icon'}>
<Share size={20} className={'mx-4 mt-2'}
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
</div>
<div className={'text-white pl-4 pr-5'}
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>
</div>
</div>
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
</>
)
}
export default BestSellers

View File

@@ -1,58 +0,0 @@
import {useEffect, useState} from 'react'
import {Grid} from '@nutui/nutui-react-taro'
import {Avatar, Divider} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components'
import {listCmsNavigation} from "@/api/cms/cmsNavigation";
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
import navTo from "@/utils/common";
const MyGrid = () => {
const [list, setList] = useState<CmsNavigation[]>([])
const reload = async () => {
// 读取首页菜单
const home = await listCmsNavigation({model: 'index'});
const homeId = home[0].navigationId;
if(homeId){
const menu = await listCmsNavigation({home: 0, parentId: homeId, hide: 0})
setList(menu)
}
}
useEffect(() => {
reload().then()
}, [])
if (list.length == 0) {
return <></>
}
// @ts-ignore
return (
<>
<View className={'p-4'}>
<View className={' bg-white rounded-2xl py-4'}>
<View className={'title font-medium px-4'}></View>
<Divider />
<Grid columns={3} square style={{
// @ts-ignore
'--nutui-grid-border-color': 'transparent',
}}>
{
list.map((item) => (
<Grid.Item key={item.navigationId} onClick={() => navTo(`${item.path}`,true)}>
<Avatar src={item.icon} className={'mb-2'} shape="square" style={{
backgroundColor: 'transparent',
}}/>
<Text className={'text-gray-600'} style={{
fontSize: '16px'
}}>{item.title}</Text>
</Grid.Item>
))
}
</Grid>
</View>
</View>
</>
)
}
export default MyGrid

View File

@@ -1,16 +0,0 @@
.header-bg{
background: linear-gradient(to bottom, #03605c, #18ae4f);
height: 335px;
width: 100%;
top: 0;
position: absolute;
z-index: 0;
}
.header-bg2{
background: linear-gradient(to bottom, #03605c, #18ae4f);
height: 200px;
width: 100%;
top: 0;
position: absolute;
z-index: 0;
}

View File

@@ -1,275 +0,0 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro';
import {Space} from '@nutui/nutui-react-taro'
import {TriangleDown} from '@nutui/icons-react-taro'
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
import {getWxOpenId} from "@/api/layout";
// import {TenantId} from "@/config/app";
import {getOrganization} from "@/api/system/organization";
import {myUserVerify} from "@/api/system/userVerify";
import {useShopInfo} from '@/hooks/useShopInfo';
import {useUser} from '@/hooks/useUser';
// import {handleInviteRelation} from "@/utils/invite";
import {View, Text} from '@tarojs/components'
import MySearch from "./MySearch";
import './Header.scss';
import navTo from "@/utils/common";
import UnifiedQRButton from "@/components/UnifiedQRButton";
import {getShopDealerRefereeByUserId} from "@/api/shop/shopDealerReferee";
const Header = (props: any) => {
// 使用新的useShopInfo Hook
const {
getWebsiteLogo,
getWebsiteName
} = useShopInfo();
// 使用useUser Hook管理用户状态
const {
user,
isLoggedIn,
fetchUserInfo
} = useUser();
const [statusBarHeight, setStatusBarHeight] = useState<number>()
const reload = async () => {
Taro.getSystemInfo({
success: (res) => {
setStatusBarHeight(res.statusBarHeight)
},
})
// 检查用户是否已登录并且有头像和昵称
if (isLoggedIn) {
const hasAvatar = user?.avatar || Taro.getStorageSync('Avatar');
const hasNickname = user?.nickname || Taro.getStorageSync('Nickname');
if (!hasAvatar || !hasNickname) {
Taro.showToast({
title: '您还没有上传头像和昵称',
icon: 'none'
})
setTimeout(() => {
Taro.navigateTo({
url: '/user/profile/profile'
})
}, 3000)
return false;
}
}
// 如果已登录,获取最新用户信息
if (isLoggedIn) {
try {
const data = await fetchUserInfo();
if (data) {
console.log('用户信息>>>', data.phone)
// 获取openId
if (!data.openid) {
Taro.login({
success: (res) => {
getWxOpenId({code: res.code}).then(() => {
})
}
})
}
// 是否已认证
if (data.certification) {
Taro.setStorageSync('Certification', '1')
}
// 机构ID
Taro.setStorageSync('OrganizationId', data.organizationId)
// 父级机构ID
if (Number(data.organizationId) > 0) {
getOrganization(Number(data.organizationId)).then(res => {
Taro.setStorageSync('OrganizationParentId', res.parentId)
})
}
// 管理员
const isKdy = data.roles?.findIndex(item => item.roleCode == 'admin')
if (isKdy != -1) {
Taro.setStorageSync('RoleName', '管理')
Taro.setStorageSync('RoleCode', 'admin')
return false;
}
// 注册用户
const isUser = data.roles?.findIndex(item => item.roleCode == 'user')
if (isUser != -1) {
Taro.setStorageSync('RoleName', '注册用户')
Taro.setStorageSync('RoleCode', 'user')
return false;
}
// 认证信息
myUserVerify({status: 1}).then(data => {
if (data?.realName) {
Taro.setStorageSync('RealName', data.realName)
}
})
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
// 查找上级推荐人
if(Taro.getStorageSync('UserId')){
const dealer = await getShopDealerRefereeByUserId(Taro.getStorageSync('UserId'))
if(dealer){
Taro.setStorageSync('DealerId', dealer.dealerId)
Taro.setStorageSync('Dealer', dealer)
}
}
}
/* 获取用户手机号 */
// const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
// const {code, encryptedData, iv} = detail
// Taro.login({
// success: (loginRes) => {
// if (code) {
// Taro.request({
// url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
// method: 'POST',
// data: {
// authCode: loginRes.code,
// code,
// encryptedData,
// iv,
// notVerifyPhone: true,
// refereeId: 0,
// sceneType: 'save_referee',
// tenantId: TenantId
// },
// header: {
// 'content-type': 'application/json',
// TenantId
// },
// success: async function (res) {
// if (res.data.code == 1) {
// Taro.showToast({
// title: res.data.message,
// icon: 'error',
// duration: 2000
// })
// return false;
// }
// // 登录成功
// const token = res.data.data.access_token;
// const userData = res.data.data.user;
//
// // 使用useUser Hook的loginUser方法更新状态
// loginUser(token, userData);
//
// // 处理邀请关系
// if (userData?.userId) {
// try {
// const inviteSuccess = await handleInviteRelation(userData.userId)
// if (inviteSuccess) {
// Taro.showToast({
// title: '邀请关系建立成功',
// icon: 'success',
// duration: 2000
// })
// }
// } catch (error) {
// console.error('处理邀请关系失败:', error)
// }
// }
//
// // 显示登录成功提示
// Taro.showToast({
// title: '登录成功',
// icon: 'success',
// duration: 1500
// })
//
// // 不需要重新启动小程序状态已经通过useUser更新
// // 可以选择性地刷新当前页面数据
// reload();
// }
// })
// } else {
// console.log('登录失败!')
// }
// }
// })
// }
useEffect(() => {
reload().then()
}, [])
// 监听用户信息变化,当用户信息更新后重新检查
useEffect(() => {
if (isLoggedIn && user) {
console.log('用户信息已更新:', user);
// 检查是否设置头像和昵称
if (user.nickname === '微信用户') {
Taro.showToast({
title: '请设置头像和昵称',
icon: 'none'
})
setTimeout(() => {
Taro.navigateTo({
url: '/user/profile/profile'
});
}, 2000)
}
}
}, [user, isLoggedIn])
return (
<>
<View className={'fixed top-0 header-bg'} style={{
height: !props.stickyStatus ? '180px' : '148px',
}}>
<MySearch/>
{/*{!props.stickyStatus && <MySearch done={reload}/>}*/}
</View>
<NavBar
style={{marginTop: `${statusBarHeight}px`, marginBottom: '0px', backgroundColor: 'transparent'}}
onBackClick={() => {
}}
left={
<Space>
{/*统一扫码入口 - 支持登录和核销*/}
<UnifiedQRButton
size="small"
onSuccess={(result) => {
console.log('统一扫码成功:', result);
// 根据扫码类型给出不同的提示
if (result.type === 'verification') {
// 核销成功,可以显示更多信息或跳转到详情页
Taro.showModal({
title: '核销成功',
content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}`
});
}
}}
onError={(error) => {
console.error('统一扫码失败:', error);
}}
/>
</Space>
}
>
{isLoggedIn ? (
<Space onClick={() => navTo(`/user/profile/profile`, true)}>
<Text className={'text-white'}>{getWebsiteName()}</Text>
</Space>
) : (
<View style={{display: 'flex', alignItems: 'center'}}>
<Avatar
size="22"
src={getWebsiteLogo()}
/>
<Text className={'text-xs'} style={{color: '#ffffff'}}>{getWebsiteName()}</Text>
<TriangleDown size={9} className={'text-white'}/>
</View>
)}
</NavBar>
</>
)
}
export default Header

View File

@@ -1,221 +0,0 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro';
import {Button, Space} from '@nutui/nutui-react-taro'
import {TriangleDown} from '@nutui/icons-react-taro'
import {Popup, Avatar, NavBar} from '@nutui/nutui-react-taro'
import {getWxOpenId} from "@/api/layout";
import {TenantId} from "@/config/app";
import {getOrganization} from "@/api/system/organization";
import {myUserVerify} from "@/api/system/userVerify";
import { useShopInfo } from '@/hooks/useShopInfo';
import { useUser } from '@/hooks/useUser';
import {handleInviteRelation} from "@/utils/invite";
import MySearch from "./MySearch";
import './Header.scss';
const Header = (props: any) => {
// 使用新的hooks
const {
loading: shopLoading,
getWebsiteName,
getWebsiteLogo
} = useShopInfo();
const {
user,
isLoggedIn,
loading: userLoading
} = useUser();
const [showBasic, setShowBasic] = useState(false)
const [statusBarHeight, setStatusBarHeight] = useState<number>()
const reload = async () => {
Taro.getSystemInfo({
success: (res) => {
setStatusBarHeight(res.statusBarHeight)
},
})
// 注意商店信息现在通过useShopInfo自动管理不需要手动获取
// 用户信息现在通过useUser自动管理不需要手动获取
// 如果需要获取openId可以在用户登录后处理
if (user && !user.openid) {
Taro.login({
success: (res) => {
getWxOpenId({code: res.code}).then(() => {
console.log('OpenId获取成功');
})
}
})
}
// 检查用户认证状态
if (user?.userId) {
// 获取组织信息
getOrganization(user.userId).then((data) => {
console.log('组织信息>>>', data)
}).catch(() => {
console.log('获取组织信息失败')
});
// 检查用户认证
myUserVerify({id: user.userId}).then((data) => {
console.log('认证信息>>>', data)
}).catch(() => {
console.log('获取认证信息失败')
});
}
}
// 获取手机号授权
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
const {code, encryptedData, iv} = detail
Taro.login({
success: function (loginRes) {
if (code) {
Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
authCode: loginRes.code,
code,
encryptedData,
iv,
notVerifyPhone: true,
refereeId: 0,
sceneType: 'save_referee',
tenantId: TenantId
},
success: async 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)
// 处理邀请关系
if (res.data.data.user?.userId) {
try {
const inviteSuccess = await handleInviteRelation(res.data.data.user.userId)
if (inviteSuccess) {
Taro.showToast({
title: '邀请关系建立成功',
icon: 'success',
duration: 2000
})
}
} catch (error) {
console.error('处理邀请关系失败:', error)
}
}
// 重新加载小程序
Taro.reLaunch({
url: '/pages/index/index'
})
}
})
} else {
console.log('登录失败!')
}
}
})
}
useEffect(() => {
reload().then()
}, [])
// 显示加载状态
if (shopLoading || userLoading) {
return (
<div className={'fixed top-0 header-bg'} style={{
height: !props.stickyStatus ? '180px' : '148px',
}}>
<div style={{padding: '20px', textAlign: 'center', color: '#fff'}}>
...
</div>
</div>
);
}
return (
<>
<div className={'fixed top-0 header-bg'} style={{
height: !props.stickyStatus ? '180px' : '148px',
}}>
<MySearch/>
</div>
<NavBar
style={{marginTop: `${statusBarHeight}px`, marginBottom: '0px', backgroundColor: 'transparent'}}
onBackClick={() => {
}}
left={
!isLoggedIn ? (
<div style={{display: 'flex', alignItems: 'center'}}>
<Button style={{color: '#000'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Space>
<Avatar
size="22"
src={getWebsiteLogo()}
/>
<span style={{color: '#000'}}>{getWebsiteName()}</span>
</Space>
</Button>
<TriangleDown size={9}/>
</div>
) : (
<div style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
<Avatar
size="22"
src={getWebsiteLogo()}
/>
<span className={'text-white'}>{getWebsiteName()}</span>
<TriangleDown className={'text-white'} size={9}/>
</div>
)}>
</NavBar>
<Popup
visible={showBasic}
position="bottom"
style={{width: '100%', height: '100%'}}
onClose={() => {
setShowBasic(false)
}}
>
<div style={{padding: '20px'}}>
<h3></h3>
<div>: {getWebsiteName()}</div>
<div>Logo: <img src={getWebsiteLogo()} alt="logo" style={{width: '50px', height: '50px'}} /></div>
<h3></h3>
<div>: {isLoggedIn ? '已登录' : '未登录'}</div>
{user && (
<>
<div>ID: {user.userId}</div>
<div>: {user.phone}</div>
<div>: {user.nickname}</div>
</>
)}
<button
onClick={() => setShowBasic(false)}
style={{marginTop: '20px', padding: '10px 20px'}}
>
</button>
</div>
</Popup>
</>
)
}
export default Header;

View File

@@ -1,68 +0,0 @@
import {useEffect, useState} from "react";
import {ArrowRight} from '@nutui/icons-react-taro'
import {CmsArticle} from "@/api/cms/cmsArticle/model";
import Taro from '@tarojs/taro'
import {useRouter} from '@tarojs/taro'
import {BaseUrl} from "@/config/app";
import {TEMPLATE_ID} from "@/utils/server";
/**
* 帮助中心
* @constructor
*/
const Help = () => {
const {params} = useRouter();
const [categoryId, setCategoryId] = useState<number>(3494)
const [list, setList] = useState<CmsArticle[]>([])
const reload = () => {
if (params.id) {
setCategoryId(Number(params.id))
}
Taro.request({
url: BaseUrl + '/cms/cms-article/page',
method: 'GET',
data: {
categoryId
},
header: {
'content-type': 'application/json',
TenantId: TEMPLATE_ID
},
success: function (res) {
const data = res.data.data;
if (data?.list) {
setList(data?.list)
}
}
})
}
useEffect(() => {
reload()
}, [])
return (
<div className={'px-3 mb-10'}>
<div className={'flex flex-col justify-between items-center bg-white rounded-lg p-4'}>
<div className={'title-bar flex justify-between items-center w-full mb-2'}>
<div className={'font-bold text-lg flex text-gray-800 justify-center items-center'}></div>
<a className={'text-gray-400 text-sm'} onClick={() => Taro.navigateTo({url: `/cms/article?id=${categoryId}`})}></a>
</div>
<div className={'bg-white min-h-36 w-full'}>
{
list.map((item, index) => {
return (
<div key={index} className={'flex justify-between items-center py-2'} onClick={() => Taro.navigateTo({url: `/cms/help?id=${item.articleId}`}) }>
<div className={'text-sm'}>{item.title}</div>
<ArrowRight color={'#cccccc'} size={18} />
</div>
)
})
}
</div>
</div>
</div>
)
}
export default Help

View File

@@ -1,152 +0,0 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
import {TenantId} from "@/config/app";
import './login.scss';
import {saveStorageByLoginUser} from "@/utils/server";
import {handleInviteRelation} from "@/utils/invite";
// 微信获取手机号回调参数类型
interface GetPhoneNumberDetail {
code?: string;
encryptedData?: string;
iv?: string;
errMsg: string;
}
interface GetPhoneNumberEvent {
detail: GetPhoneNumberDetail;
}
interface LoginProps {
done?: (user: any) => void;
[key: string]: any;
}
// 登录接口返回数据类型
interface LoginResponse {
data: {
data: {
access_token: string;
user: any;
};
};
}
const Login = (props: LoginProps) => {
const [isAgree, setIsAgree] = useState(false)
const [env, setEnv] = useState<string>()
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}: GetPhoneNumberEvent) => {
const {code, encryptedData, iv} = detail
Taro.login({
success: function (loginRes) {
if (code) {
Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
authCode: loginRes.code,
code,
encryptedData,
iv,
notVerifyPhone: true,
refereeId: 0,
sceneType: 'save_referee',
tenantId: TenantId
},
header: {
'content-type': 'application/json',
TenantId
},
success: async function (res: LoginResponse) {
saveStorageByLoginUser(res.data.data.access_token, res.data.data.user)
// 处理邀请关系
if (res.data.data.user?.userId) {
try {
const inviteSuccess = await handleInviteRelation(res.data.data.user.userId)
if (inviteSuccess) {
Taro.showToast({
title: '邀请关系建立成功',
icon: 'success',
duration: 2000
})
}
} catch (error) {
console.error('处理邀请关系失败:', error)
}
}
props.done?.(res.data.data.user);
}
})
} else {
console.log('登录失败!')
}
}
})
}
const reload = () => {
Taro.hideTabBar()
setEnv(Taro.getEnv())
}
useEffect(() => {
reload()
}, [])
return (
<>
<div style={{height: '80vh'}} className={'flex flex-col justify-center px-5'}>
<div className={'text-3xl text-center py-5 font-normal mb-10 '}></div>
{
env === 'WEAPP' && (
<>
<div className={'flex flex-col w-full text-white rounded-full justify-between items-center my-2'} style={{ background: 'linear-gradient(to right, #7e22ce, #9333ea)'}}>
<Button open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
</Button>
</div>
</>
)
}
{
env === 'WEB' && (
<>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input type="text" placeholder="手机号" maxLength={11}
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
</div>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input type="password" placeholder="密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
</div>
<div className={'flex justify-between my-2 text-left px-1'}>
<a href={'#'} className={'text-blue-600 text-sm'}
onClick={() => Taro.navigateTo({url: '/passport/forget'})}></a>
<a href={'#'} className={'text-blue-600 text-sm'}
onClick={() => Taro.navigateTo({url: '/passport/setting'})}></a>
</div>
<div className={'flex justify-center my-5'}>
<Button type="info" size={'large'} className={'w-full rounded-lg p-2'} disabled={!isAgree}></Button>
</div>
<div className={'w-full bottom-20 my-2 flex justify-center text-sm items-center text-center'}>
<a href={''} onClick={() => Taro.navigateTo({url: '/passport/register'})}
className={'text-blue-600'}></a>
</div>
<div className={'my-2 flex fixed bottom-20 text-sm items-center px-1'}>
<Radio style={{color: '#333333'}} checked={isAgree} onClick={() => setIsAgree(!isAgree)}></Radio>
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}></span><a
onClick={() => Taro.navigateTo({url: '/passport/agreement'})}
className={'text-blue-600'}></a>
</div>
</>
)
}
</div>
</>
)
}
export default Login

View File

@@ -1,73 +0,0 @@
import Taro from '@tarojs/taro'
import {useEffect, useState} from 'react'
import {View, Text} from '@tarojs/components'
import {Image} from '@nutui/nutui-react-taro'
import {Loading} from '@nutui/nutui-react-taro'
import {listCmsNavigation} from "@/api/cms/cmsNavigation"
import {CmsNavigation} from "@/api/cms/cmsNavigation/model"
const Page = () => {
const [loading, setLoading] = useState<boolean>(true)
const [navItems, setNavItems] = useState<CmsNavigation[]>([])
const reload = async () => {
// 读取首页菜单
const home = await listCmsNavigation({model: 'index'});
const homeId = home[0].navigationId;
if (homeId) {
// 读取首页导航条
const menus = await listCmsNavigation({parentId: homeId, hide: 0});
setNavItems(menus || [])
}
};
const onNav = (row: CmsNavigation) => {
console.log('nav = ', row)
console.log('path = ', `/${row.model}${row.path}`)
if (row.model == 'goods') {
return Taro.navigateTo({url: `/shop/category/index?id=${row.navigationId}`})
}
if (row.model == 'article') {
return Taro.navigateTo({url: `/cms/category/index?id=${row.navigationId}`})
}
return Taro.navigateTo({url: `${row.path}`})
}
useEffect(() => {
reload().then(() => {
setLoading(false)
});
}, [])
if (loading) {
return (
<Loading></Loading>
)
}
if (navItems.length === 0) {
return <View className={'hidden'}></View>;
}
return (
<View className={'p-2 z-50 mt-1 hidden'}>
<View className={'flex justify-between pb-2 p-2 bg-white rounded-xl shadow-sm'}>
{
navItems.map((item, index) => (
<View key={index} className={'text-center'} onClick={() => onNav(item)}>
<View className={'flex flex-col justify-center items-center p-1'}>
<Image src={item.icon} height={36} width={36} lazyLoad={false}/>
<View className={'mt-1'}>
<Text className={'text-gray-600'} style={{fontSize: '14px'}}>{item?.title}</Text>
</View>
</View>
</View>
))
}
</View>
</View>
)
}
export default Page

View File

@@ -1,70 +0,0 @@
import {Search} from '@nutui/icons-react-taro'
import {Button, Input} from '@nutui/nutui-react-taro'
import {useState} from "react";
import Taro from '@tarojs/taro';
function MySearch() {
const [keywords, setKeywords] = useState<string>('')
const onKeywords = (keywords: string) => {
setKeywords(keywords)
}
const onQuery = () => {
if(!keywords.trim()){
Taro.showToast({
title: '请输入关键字',
icon: 'none'
});
return false;
}
// 跳转到搜索页面
Taro.navigateTo({
url: `/shop/search/index?keywords=${encodeURIComponent(keywords.trim())}`
});
}
// 点击搜索框跳转到搜索页面
const onInputFocus = () => {
Taro.navigateTo({
url: '/shop/search/index'
});
}
return (
<div className={'z-50 left-0 w-full'}>
<div className={'px-2 hidden'}>
<div
style={{
display: 'flex',
alignItems: 'center',
background: '#ffffff',
padding: '0 5px',
borderRadius: '20px',
marginTop: '100px',
}}
>
<Search size={18} className={'ml-2 text-gray-400'}/>
<Input
placeholder="搜索商品"
value={keywords}
onChange={onKeywords}
onConfirm={onQuery}
onFocus={onInputFocus}
style={{ padding: '9px 8px'}}
/>
<div
className={'flex items-center'}
>
<Button type="success" style={{background: 'linear-gradient(to bottom, #1cd98a, #24ca94)'}} onClick={onQuery}>
</Button>
</div>
</div>
</div>
</div>
);
}
export default MySearch;

View File

@@ -1,47 +0,0 @@
import {useEffect, useState} from 'react'
import { Dialog } from '@nutui/nutui-react-taro'
import {getCmsNavigation} from "@/api/cms/cmsNavigation";
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
import {RichText} from '@tarojs/components'
const PopUpAd = () => {
const [visible, setVisible] = useState(false)
const [item, setItem] = useState<CmsNavigation>()
const reload = async () => {
const navigation = await getCmsNavigation(4426)
if(navigation && navigation.hide == 0){
setItem(navigation)
setVisible(true)
}
}
useEffect(() => {
reload().then()
}, [])
return (
<>
<Dialog
title={
<div className={'font-bold mb-3'}></div>
}
footer={null}
closeIcon
closeIconPosition="top-right"
style={{
// @ts-ignore
'--nutui-dialog-close-color': '#8c8c8c',
}}
onConfirm={() => setVisible(false)}
onCancel={() => setVisible(false)}
visible={visible}
onClose={() => {
setVisible(false)
}}
>
<RichText nodes={item?.design?.content}/>
</Dialog>
</>
)
}
export default PopUpAd

View File

@@ -1,29 +0,0 @@
import {useEffect, useState} from "react";
import {Input, Button} from '@nutui/nutui-react-taro'
import {copyText} from "@/utils/common";
import Taro from '@tarojs/taro'
const SiteUrl = (props: any) => {
const [siteUrl, setSiteUrl] = useState<string>('')
const reload = () => {
if(props.tenantId){
setSiteUrl(`https://${props.tenantId}.shoplnk.cn`)
}else {
setSiteUrl(`https://${Taro.getStorageSync('TenantId')}.shoplnk.cn`)
}
}
useEffect(() => {
reload()
}, [props])
return (
<div className={'px-3 mt-1 mb-4'}>
<div className={'flex justify-between items-center bg-gray-300 rounded-lg pr-2'}>
<Input type="text" value={siteUrl} disabled style={{backgroundColor: '#d1d5db', borderRadius: '8px'}}/>
<Button type={'info'} onClick={() => copyText(siteUrl)}></Button>
</div>
</div>
)
}
export default SiteUrl

View File

@@ -1,38 +0,0 @@
import { useRef, useEffect } from 'react'
import { View } from '@tarojs/components'
import { EChart } from "echarts-taro3-react";
import './index.scss'
export default function Index() {
const refBarChart = useRef<any>()
const defautOption = {
xAxis: {
type: "category",
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
},
yAxis: {
type: "value",
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: "line",
showBackground: true,
backgroundStyle: {
color: "rgba(220, 220, 220, 0.8)",
},
},
],
};
useEffect(() => {
if(refBarChart.current) {
refBarChart.current?.refresh(defautOption);
}
})
return (
<View className='index'>
<EChart ref={refBarChart} canvasId='line-canvas' />
</View>
)
}

View File

@@ -1,7 +0,0 @@
.index {
width: 100vw;
height: 100vh;
background-color: #F3F3F3;
background-repeat: no-repeat;
background-size: 100%;
}

View File

@@ -116,6 +116,18 @@ page {
justify-content: flex-start;
}
.actionItemButton {
padding: 0;
margin: 0;
background: transparent;
border: 0;
line-height: 1;
}
.actionItemButton::after {
border: 0;
}
.actionIcon {
width: 112rpx;
height: 112rpx;

View File

@@ -1,5 +1,5 @@
import Taro, {useShareAppMessage, useShareTimeline} from '@tarojs/taro'
import {Image, Text, View} from '@tarojs/components'
import {Button, Image, Text, View} from '@tarojs/components'
import {useEffect, useState} from 'react'
import Banner from './Banner'
import iconShop from '@/assets/tabbar/shop.png'
@@ -35,14 +35,18 @@ function Home() {
}
})
const onAction = (type: 'progress' | 'guide' | 'kefu') => {
const onAction = (type: 'progress' | 'guide') => {
if (type === 'progress') {
navTo('/credit/my-order/index', true)
return
}
const textMap = { guide: '业务指南', kefu: '在线客服' } as const
Taro.showToast({title: `${textMap[type]}(示例)`, icon: 'none'})
navTo('/user/help/index')
}
const onContactFallback = () => {
if (Taro.getEnv() === Taro.ENV_TYPE.WEAPP) return
Taro.showToast({title: '请在微信小程序内使用在线客服', icon: 'none'})
}
const FAQ_CATEGORY_ID = 4558
@@ -112,12 +116,17 @@ function Home() {
<Text className='actionLabel'></Text>
</View>
<View className='actionItem' onClick={() => onAction('kefu')}>
<Button
className='actionItem actionItemButton'
openType='contact'
hoverClass='none'
onClick={onContactFallback}
>
<View className='actionIcon'>
<Image className='actionIconImg' src={iconKefu} mode='aspectFit' />
</View>
<Text className='actionLabel'>线</Text>
</View>
</Button>
</View>
</View>
)}

View File

@@ -1,10 +0,0 @@
// 微信授权按钮的特殊样式
button[open-type="getPhoneNumber"] {
width: 100%;
padding: 8px 0 !important;
height: 80px;
color: #ffffff !important;
margin: 0 !important;
border: none !important;
border-radius: 50px !important;
}

View File

@@ -1,96 +0,0 @@
import {Cell} from '@nutui/nutui-react-taro'
import navTo from "@/utils/common";
import {View, Text} from '@tarojs/components'
import {ArrowRight, Reward, Setting} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
import {useDealerUser} from "@/hooks/useDealerUser";
import {useThemeStyles} from "@/hooks/useTheme";
import { useConfig } from "@/hooks/useConfig"; // 使用新的自定义Hook
import Taro from '@tarojs/taro'
const IsDealer = () => {
const themeStyles = useThemeStyles();
const { config } = useConfig(); // 使用新的Hook
const {isSuperAdmin} = useUser();
const {dealerUser, loading: dealerLoading} = useDealerUser()
/**
* 管理中心
*/
if (isSuperAdmin()) {
return (
<>
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
<Cell
className="nutui-cell-clickable"
style={themeStyles.primaryBackground}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Setting className={'text-white '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-white font-medium'}></Text>
</View>
}
extra={<ArrowRight color="#ffffff" size={18}/>}
onClick={() => navTo('/admin/index', true)}
/>
</View>
</>
)
}
/**
* 分销中心
*/
if (dealerUser) {
return (
<>
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
<Cell
className="nutui-cell-clickable"
style={themeStyles.primaryBackground}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}}
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '易赊宝分享中心'}</Text>
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/dealer/index', true)}
/>
</View>
</>
)
}
/**
* 普通用户
*/
return (
<>
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
<Cell
className="nutui-cell-clickable"
style={themeStyles.primaryBackground}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '易赊宝分享中心'}</Text>
<Text className={'text-white opacity-80 pl-3'}>{config?.vipComments || ''}</Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
if (dealerLoading) {
Taro.showToast({ title: '正在加载信息,请稍等...', icon: 'none' })
return
}
navTo('/dealer/apply/add', true)
}}
/>
</View>
</>
)
}
export default IsDealer

View File

@@ -12,7 +12,6 @@ import {checkAndHandleInviteRelation, getStoredInviteParams, hasPendingInvite} f
import UnifiedQRButton from "@/components/UnifiedQRButton";
import {useThemeStyles} from "@/hooks/useTheme";
import {getRootDomain} from "@/utils/domain";
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
import { saveStorageByLoginUser } from '@/utils/server'
const UserCard = forwardRef<any, any>((_, ref) => {
@@ -20,7 +19,6 @@ const UserCard = forwardRef<any, any>((_, ref) => {
const {loadUserFromStorage} = useUser();
const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>()
const [ticketTotal, setTicketTotal] = useState<number>(0)
const themeStyles = useThemeStyles();
const canShowScanButton = (() => {
@@ -41,7 +39,6 @@ const UserCard = forwardRef<any, any>((_, ref) => {
// 下拉刷新
const reloadStats = async (showToast = false) => {
await refresh()
reloadTicketTotal()
if (showToast) {
Taro.showToast({
title: '刷新成功',
@@ -93,9 +90,6 @@ const UserCard = forwardRef<any, any>((_, ref) => {
}))
useEffect(() => {
// 独立于用户信息授权:只要有登录 token就可以拉取水票总数
reloadTicketTotal()
// Taro.getSetting获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
success: (res) => {
@@ -112,23 +106,6 @@ const UserCard = forwardRef<any, any>((_, ref) => {
});
}, []);
const reloadTicketTotal = () => {
const token = Taro.getStorageSync('access_token')
const userIdRaw = Taro.getStorageSync('UserId')
const userId = Number(userIdRaw)
const hasUserId = Number.isFinite(userId) && userId > 0
if (!token && !hasUserId) {
setTicketTotal(0)
return
}
getMyGltUserTicketTotal(hasUserId ? userId : undefined)
.then((total) => setTicketTotal(typeof total === 'number' ? total : 0))
.catch((err) => {
console.error('个人中心水票总数加载失败:', err)
setTicketTotal(0)
})
}
const reload = () => {
Taro.getUserInfo({
success: (res) => {
@@ -140,9 +117,8 @@ const UserCard = forwardRef<any, any>((_, ref) => {
})
reloadUserInfo()
.then(() => {
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票
// 登录态已就绪后刷新卡片统计(余额/积分/券
refresh().then()
reloadTicketTotal()
})
.catch(() => {
console.log('未登录')
@@ -229,9 +205,8 @@ const UserCard = forwardRef<any, any>((_, ref) => {
saveStorageByLoginUser(res.data.data.access_token, res.data.data.user)
setUserInfo(res.data.data.user)
setIsLogin(true)
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票
// 登录态已就绪后刷新卡片统计(余额/积分/券
refresh().then()
reloadTicketTotal()
// 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定
if (hasPendingInvite()) {

View File

@@ -1,144 +0,0 @@
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, Tips, Ask} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
const UserCell = () => {
const {logoutUser, isCertified} = 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'} style={{ marginTop: '8px', position: 'relative', zIndex: 20 }}>
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}></Text>
</View>
}>
<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={() => navTo('/user/theme/index', 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

@@ -8,14 +8,12 @@ import {
Agenda,
// AfterSaleService,
Logout,
Shop,
Jdl,
Service
} from '@nutui/icons-react-taro'
import {useUser} from "@/hooks/useUser";
const UserCell = () => {
const {logoutUser, hasRole} = useUser();
const {logoutUser} = useUser();
const onLogout = () => {
Taro.showModal({
@@ -48,26 +46,6 @@ const UserCell = () => {
} as React.CSSProperties}
>
{hasRole('store') && (
<Grid.Item text="门店中心" onClick={() => navTo('/store/index', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shop color="#3b82f6" size="20"/>
</View>
</View>
</Grid.Item>
)}
{hasRole('rider') && (
<Grid.Item text="配送中心" onClick={() => navTo('/rider/index', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Jdl color="#3b82f6" size="20"/>
</View>
</View>
</Grid.Item>
)}
<Grid.Item text="我的需求" onClick={() => navTo('/credit/order/index', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">

View File

@@ -1,122 +0,0 @@
import navTo from "@/utils/common";
import {View, Text} from '@tarojs/components';
import {Badge} from '@nutui/nutui-react-taro';
import {ArrowRight, Wallet, Comment, Transit, Refund, Package} from '@nutui/icons-react-taro';
import {useOrderStats} from "@/hooks/useOrderStats";
function UserOrder() {
const { orderStats, refreshOrderStats } = useOrderStats();
// 处理长按刷新
const handleLongPress = () => {
refreshOrderStats();
};
return (
<>
<View className={'px-4 pb-2 z-30 relative'} style={{ marginTop: '8px' }}>
<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',
// paddingBottom: '3px'
// 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)}
onLongPress={handleLongPress}
>
<Text className={'text-xs text-gray-500'}></Text>
<ArrowRight color="#cccccc" size={12}/>
</View>
</View>
<View className={'flex justify-around pb-1 mt-4'}>
{/* 待付款 */}
{orderStats.pending > 0 ? (
<Badge value={orderStats.pending} max={99} fill={'outline'}>
<View className={'item flex justify-center flex-col items-center'}>
<Wallet size={26} className={'font-normal text-gray-500'}
onClick={() => navTo('/user/order/order?statusFilter=0', true)}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
</Badge>
) : (
<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>
)}
{/* 待发货 */}
{orderStats.paid > 0 ? (
<Badge value={orderStats.paid} max={99} fill={'outline'}>
<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>
</Badge>
) : (
<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>
)}
{/* 待收货 */}
{orderStats.shipped > 0 ? (
<Badge value={orderStats.shipped} max={99} fill={'outline'}>
<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>
</Badge>
) : (
<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>
)}
{/* 已完成 - 不显示badge */}
<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>
{/* 退货/售后 */}
{orderStats.refund > 0 ? (
<Badge value={orderStats.refund} max={99} fill={'outline'}>
<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>
</Badge>
) : (
<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,4 +1,4 @@
import {useEffect, useRef, useState} from 'react'
import {useEffect, useRef} from 'react'
import {PullToRefresh} from '@nutui/nutui-react-taro'
import UserCard from "./components/UserCard";
import UserFooter from "./components/UserFooter";
@@ -17,15 +17,11 @@ function User() {
...themeStyles.primaryBackground,
background: '#cf1313'
}
// TabBar 页在小程序里通常不会销毁;从“注册/申请”页返回时需要触发子组件重新初始化/拉取最新状态。
const [dealerViewKey, setDealerViewKey] = useState(0)
console.log(dealerViewKey)
// 下拉刷新处理
const handleRefresh = async () => {
if (userCardRef.current?.handleRefresh) {
await userCardRef.current.handleRefresh()
}
setDealerViewKey(v => v + 1)
}
useEffect(() => {
@@ -36,7 +32,6 @@ function User() {
userCardRef.current?.reloadStats?.()
// 个人资料(头像/昵称)可能在其它页面被修改,这里确保返回时立刻刷新
userCardRef.current?.reloadUserInfo?.()
setDealerViewKey(v => v + 1)
})
return (

View File

@@ -87,7 +87,6 @@ function isTabBarUrl(url: string) {
const pure = url.split('?')[0]
return (
pure === '/pages/index/index' ||
pure === '/pages/find/find' ||
pure === '/pages/user/user'
)
}

View File

@@ -36,7 +36,6 @@ const SmsLogin = () => {
const pure = url.split('?')[0]
return (
pure === '/pages/index/index' ||
pure === '/pages/find/find' ||
pure === '/pages/user/user'
)
}

View File

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

View File

View File

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

View File

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

View File

@@ -1,610 +0,0 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import {
Tabs,
TabPane,
Cell,
Space,
Button,
Dialog,
Radio,
RadioGroup,
Image,
Empty,
InfiniteLoading,
PullToRefresh,
Loading
} from '@nutui/nutui-react-taro'
import { View, Text } from '@tarojs/components'
import dayjs from 'dayjs'
import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder'
import type { GltTicketOrder, GltTicketOrderParam } from '@/api/glt/gltTicketOrder/model'
import { uploadFile } from '@/api/system/file'
export default function RiderOrders() {
const PAGE_SIZE = 10
const riderId = useMemo(() => {
const v = Number(Taro.getStorageSync('UserId'))
return Number.isFinite(v) && v > 0 ? v : undefined
}, [])
const pageRef = useRef(1)
const listRef = useRef<GltTicketOrder[]>([])
const [tabIndex, setTabIndex] = useState(0)
const [list, setList] = useState<GltTicketOrder[]>([])
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [deliverDialogVisible, setDeliverDialogVisible] = useState(false)
const [deliverSubmitting, setDeliverSubmitting] = useState(false)
const [deliverOrder, setDeliverOrder] = useState<GltTicketOrder | null>(null)
const [deliverImg, setDeliverImg] = useState<string | undefined>(undefined)
type DeliverConfirmMode = 'photoComplete' | 'waitCustomerConfirm'
const [deliverConfirmMode, setDeliverConfirmMode] = useState<DeliverConfirmMode>('photoComplete')
const riderTabs = useMemo(
() => [
{ index: 0, title: '全部' },
{ index: 1, title: '待配送', deliveryStatus: 10 },
{ index: 2, title: '配送中', deliveryStatus: 20 },
{ index: 3, title: '待确认', deliveryStatus: 30 },
{ index: 4, title: '已完成', deliveryStatus: 40 }
],
[]
)
const getOrderStatusText = (order: GltTicketOrder) => {
if (order.status === 1) return '已冻结'
const deliveryStatus = order.deliveryStatus
if (deliveryStatus === 40) return '已完成'
if (deliveryStatus === 30) return '待客户确认'
if (deliveryStatus === 20) return '配送中'
if (deliveryStatus === 10) return '待配送'
// 兼容:如果后端暂未下发 deliveryStatus就用时间字段推断
if (order.receiveConfirmTime) return '已完成'
if (order.sendEndTime) return '待客户确认'
if (order.sendStartTime) return '配送中'
if (order.riderId) return '待配送'
return '待派单'
}
const getOrderStatusColor = (order: GltTicketOrder) => {
const text = getOrderStatusText(order)
if (text === '已完成') return 'text-green-600'
if (text === '待客户确认') return 'text-purple-600'
if (text === '配送中') return 'text-blue-600'
if (text === '待配送') return 'text-amber-600'
if (text === '已冻结') return 'text-orange-600'
return 'text-gray-500'
}
const canStartDeliver = (order: GltTicketOrder) => {
if (!order.id) return false
if (order.status === 1) return false
if (!riderId || order.riderId !== riderId) return false
if (order.deliveryStatus && order.deliveryStatus !== 10) return false
return !order.sendStartTime && !order.sendEndTime
}
const canConfirmDelivered = (order: GltTicketOrder) => {
if (!order.id) return false
if (order.status === 1) return false
if (!riderId || order.riderId !== riderId) return false
if (order.receiveConfirmTime) return false
if (order.deliveryStatus === 40) return false
if (order.sendEndTime) return false
// 只允许在“配送中”阶段确认送达
if (typeof order.deliveryStatus === 'number') return order.deliveryStatus === 20
return !!order.sendStartTime
}
const canCompleteByPhoto = (order: GltTicketOrder) => {
if (!order.id) return false
if (order.status === 1) return false
if (!riderId || order.riderId !== riderId) return false
if (order.receiveConfirmTime) return false
if (order.deliveryStatus === 40) return false
// 已送达但未完成:允许补传照片并直接完成
return !!order.sendEndTime
}
const filterByTab = useCallback(
(orders: GltTicketOrder[]) => {
if (tabIndex === 0) return orders
const current = riderTabs.find(t => t.index === tabIndex)
const status = current?.deliveryStatus
if (!status) return orders
// 如果后端已实现 deliveryStatus 筛选,这里基本不会再过滤;否则用兼容逻辑兜底。
return orders.filter(o => {
const ds = o.deliveryStatus
if (typeof ds === 'number') return ds === status
if (status === 10) return !!o.riderId && !o.sendStartTime && !o.sendEndTime
if (status === 20) return !!o.sendStartTime && !o.sendEndTime
if (status === 30) return !!o.sendEndTime && !o.receiveConfirmTime
if (status === 40) return !!o.receiveConfirmTime
return true
})
},
[riderTabs, tabIndex]
)
const reload = useCallback(
async (resetPage = false) => {
if (!riderId) return
if (loading) return
setLoading(true)
setError(null)
const currentPage = resetPage ? 1 : pageRef.current
const currentTab = riderTabs.find(t => t.index === tabIndex)
const params: GltTicketOrderParam = {
page: currentPage,
limit: PAGE_SIZE,
riderId,
deliveryStatus: currentTab?.deliveryStatus
}
try {
const res = await pageGltTicketOrder(params as any)
const incomingAll = (res?.list || []) as GltTicketOrder[]
// 兼容:后端若暂未实现 riderId 过滤,前端兜底过滤掉非本人的订单
const incoming = incomingAll.filter(o => o?.deleted !== 1 && o?.riderId === riderId)
const prev = resetPage ? [] : listRef.current
const next = resetPage ? incoming : prev.concat(incoming)
listRef.current = next
setList(next)
const total = typeof res?.count === 'number' ? res.count : undefined
const filteredOut = incomingAll.length - incoming.length
if (typeof total === 'number' && filteredOut === 0) {
setHasMore(next.length < total)
} else {
setHasMore(incomingAll.length >= PAGE_SIZE)
}
pageRef.current = currentPage + 1
} catch (e) {
console.error('加载配送订单失败:', e)
setError('加载失败,请重试')
setHasMore(false)
} finally {
setLoading(false)
}
},
[PAGE_SIZE, loading, riderId, riderTabs, tabIndex]
)
const reloadMore = useCallback(async () => {
if (loading || !hasMore) return
await reload(false)
}, [hasMore, loading, reload])
const openDeliverDialog = (order: GltTicketOrder, opts?: { mode?: DeliverConfirmMode }) => {
setDeliverOrder(order)
setDeliverImg(order.sendEndImg)
setDeliverConfirmMode(opts?.mode || (order.sendEndImg ? 'photoComplete' : 'waitCustomerConfirm'))
setDeliverDialogVisible(true)
}
const handleChooseDeliverImg = async () => {
try {
const file = await uploadFile()
setDeliverImg(file?.url)
} catch (e) {
console.error('上传送达照片失败:', e)
Taro.showToast({ title: '上传失败,请重试', icon: 'none' })
}
}
const handleStartDeliver = async (order: GltTicketOrder) => {
if (!order?.id) return
if (!canStartDeliver(order)) return
try {
await updateGltTicketOrder({
id: order.id,
deliveryStatus: 20,
sendStartTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
})
Taro.showToast({ title: '已开始配送', icon: 'success' })
pageRef.current = 1
listRef.current = []
setList([])
setHasMore(true)
await reload(true)
} catch (e) {
console.error('开始配送失败:', e)
Taro.showToast({ title: '开始配送失败', icon: 'none' })
}
}
const handleConfirmDelivered = async () => {
if (!deliverOrder?.id) return
if (deliverSubmitting) return
if (deliverConfirmMode === 'photoComplete' && !deliverImg) {
Taro.showToast({ title: '请先拍照/上传送达照片', icon: 'none' })
return
}
setDeliverSubmitting(true)
try {
const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
// 送达时间:首次“确认送达”写入;补传照片时不要覆盖原送达时间
const deliveredAt = deliverOrder.sendEndTime || now
// - waitCustomerConfirm只标记“已送达”进入待客户确认
// - photoComplete拍照留档后可直接完成是否允许由后端策略决定
const payload: GltTicketOrder =
deliverConfirmMode === 'photoComplete'
? {
id: deliverOrder.id,
deliveryStatus: 40,
sendEndTime: deliveredAt,
sendEndImg: deliverImg,
receiveConfirmTime: now,
receiveConfirmType: 20
}
: {
id: deliverOrder.id,
deliveryStatus: 30,
sendEndTime: deliveredAt,
sendEndImg: deliverImg
}
await updateGltTicketOrder(payload)
Taro.showToast({ title: '已确认送达', icon: 'success' })
setDeliverDialogVisible(false)
setDeliverOrder(null)
setDeliverImg(undefined)
setDeliverConfirmMode('photoComplete')
pageRef.current = 1
listRef.current = []
setList([])
setHasMore(true)
await reload(true)
} catch (e) {
console.error('确认送达失败:', e)
Taro.showToast({ title: '确认送达失败', icon: 'none' })
} finally {
setDeliverSubmitting(false)
}
}
useEffect(() => {
listRef.current = list
}, [list])
useDidShow(() => {
pageRef.current = 1
listRef.current = []
setList([])
setHasMore(true)
void reload(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
})
useEffect(() => {
pageRef.current = 1
listRef.current = []
setList([])
setHasMore(true)
void reload(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tabIndex, riderId])
if (!riderId) {
return (
<View className="bg-gray-50 min-h-screen p-4">
<Text></Text>
</View>
)
}
const displayList = filterByTab(list)
return (
<View className="bg-gray-50 min-h-screen">
<View>
<Tabs
align="left"
className="fixed left-0"
style={{zIndex: 998, borderBottom: '1px solid #e5e5e5'}}
tabStyle={{backgroundColor: '#ffffff'}}
value={tabIndex}
onChange={(paneKey) => setTabIndex(Number(paneKey))}
>
{riderTabs.map(t => (
<TabPane key={t.index} title={loading && tabIndex === t.index ? `${t.title}...` : t.title}></TabPane>
))}
</Tabs>
<PullToRefresh
onRefresh={async () => {
pageRef.current = 1
listRef.current = []
setList([])
setHasMore(true)
await reload(true)
}}
headHeight={60}
>
<View
style={{ height: '84vh', width: '100%', padding: '0', overflowY: 'auto', overflowX: 'hidden' }}
id="rider-order-scroll"
>
{error ? (
<View className="flex flex-col items-center justify-center h-64">
<Text className="text-gray-500 mb-4">{error}</Text>
<Button size="small" type="primary" onClick={() => reload(true)}>
</Button>
</View>
) : (
<InfiniteLoading
target="rider-order-scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
loadingText={
<View className="flex justify-center items-center py-4">
<Loading />
<View className="ml-2">...</View>
</View>
}
loadMoreText={
displayList.length === 0 ? (
<Empty style={{ backgroundColor: 'transparent' }} description="暂无配送订单" />
) : (
<View className="h-24 text-center text-gray-500"></View>
)
}
>
{displayList.map(o => {
const timeText = o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'
const addr = o.address || (o.addressId ? `地址ID${o.addressId}` : '-')
const remark = o.buyerRemarks || o.comments || ''
const qty = Number(o.totalNum || 0)
const flow1Done = !!o.riderId
const flow2Done = !!o.sendStartTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 20)
const flow3Done = !!o.sendEndTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 30)
const flow4Done = !!o.receiveConfirmTime || o.deliveryStatus === 40
const phoneToCall = o.phone
const storePhone = o.storePhone
const pickupName = o.warehouseName || o.storeName
const pickupAddr = o.warehouseAddress || o.storeAddress
return (
<Cell key={String(o.id)} style={{ padding: '16px' }}>
<View className="w-full">
<View className="flex justify-between items-center">
<Text className="text-gray-800 font-bold text-sm">
{o.userTicketId ? `票号#${o.userTicketId}` : '送水订单'}
</Text>
<Text className={`${getOrderStatusColor(o)} text-sm font-medium`}>{getOrderStatusText(o)}</Text>
</View>
<View className="text-gray-400 text-xs mt-1">{timeText}</View>
<View className="mt-3 bg-white rounded-lg">
<View className="text-sm text-gray-700">
<Text className="text-gray-500"></Text>
<Text>{o.nickname || '-'} {o.phone ? `(${o.phone})` : ''}</Text>
</View>
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{addr}</Text>
</View>
{!!remark && (
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{remark}</Text>
</View>
)}
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{Number.isFinite(qty) ? qty : '-'}</Text>
<Text className="text-gray-500 ml-3"></Text>
<Text>{o.price || '-'}</Text>
</View>
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{o.sendTime ? dayjs(o.sendTime).format('YYYY-MM-DD HH:mm') : '-'}</Text>
</View>
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{pickupName || '-'}</Text>
</View>
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{pickupAddr || '-'}</Text>
</View>
{!!o.sendStartTime && (
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{dayjs(o.sendStartTime).format('YYYY-MM-DD HH:mm')}</Text>
</View>
)}
{!!o.sendEndTime && (
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{dayjs(o.sendEndTime).format('YYYY-MM-DD HH:mm')}</Text>
</View>
)}
{!!o.receiveConfirmTime && (
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{dayjs(o.receiveConfirmTime).format('YYYY-MM-DD HH:mm')}</Text>
</View>
)}
{o.sendEndImg ? (
<View className="text-sm text-gray-700 mt-2">
<Text className="text-gray-500"></Text>
<View className="mt-2">
<Image src={o.sendEndImg} width="100%" height="120" />
</View>
</View>
) : null}
</View>
{/* 配送流程 */}
<View className="mt-3 bg-gray-50 rounded-lg p-2 text-xs">
<Text className="text-gray-600"></Text>
<Text className={flow1Done ? 'text-green-600 font-medium' : 'text-gray-400'}>1 </Text>
<Text className="mx-1 text-gray-400">{'>'}</Text>
<Text className={flow2Done ? 'text-blue-600 font-medium' : 'text-gray-400'}>2 </Text>
<Text className="mx-1 text-gray-400">{'>'}</Text>
<Text className={flow3Done ? 'text-purple-600 font-medium' : 'text-gray-400'}>3 </Text>
<Text className="mx-1 text-gray-400">{'>'}</Text>
<Text className={flow4Done ? 'text-green-600 font-medium' : 'text-gray-400'}>4 </Text>
</View>
<View className="mt-3 flex justify-end">
<Space>
{!!phoneToCall && (
<Button
size="small"
onClick={e => {
e.stopPropagation()
Taro.makePhoneCall({ phoneNumber: phoneToCall })
}}
>
</Button>
)}
{!!addr && addr !== '-' && (
<Button
size="small"
onClick={e => {
e.stopPropagation()
void Taro.setClipboardData({ data: addr })
Taro.showToast({ title: '地址已复制', icon: 'none' })
}}
>
</Button>
)}
{!!storePhone && (
<Button
size="small"
onClick={e => {
e.stopPropagation()
Taro.makePhoneCall({ phoneNumber: storePhone })
}}
>
</Button>
)}
{canStartDeliver(o) && (
<Button
size="small"
onClick={e => {
e.stopPropagation()
void handleStartDeliver(o)
}}
>
</Button>
)}
{canConfirmDelivered(o) && (
<Button
size="small"
type="primary"
onClick={e => {
e.stopPropagation()
openDeliverDialog(o, { mode: 'waitCustomerConfirm' })
}}
>
</Button>
)}
{canCompleteByPhoto(o) && (
<Button
size="small"
type="primary"
onClick={e => {
e.stopPropagation()
openDeliverDialog(o, { mode: 'photoComplete' })
}}
>
</Button>
)}
</Space>
</View>
</View>
</Cell>
)
})}
</InfiniteLoading>
)}
</View>
</PullToRefresh>
</View>
<Dialog
title="确认送达"
visible={deliverDialogVisible}
confirmText={
deliverSubmitting
? '提交中...'
: deliverConfirmMode === 'photoComplete'
? '拍照完成'
: '确认送达'
}
cancelText="取消"
onConfirm={handleConfirmDelivered}
onCancel={() => {
if (deliverSubmitting) return
setDeliverDialogVisible(false)
setDeliverOrder(null)
setDeliverImg(undefined)
setDeliverConfirmMode('photoComplete')
}}
>
<View className="text-sm text-gray-700">
<View></View>
<View className="mt-3">
<RadioGroup value={deliverConfirmMode} onChange={v => setDeliverConfirmMode(v as DeliverConfirmMode)}>
<Radio value="photoComplete"></Radio>
<Radio value="waitCustomerConfirm"></Radio>
</RadioGroup>
</View>
<View className="mt-3">
<Button size="small" onClick={handleChooseDeliverImg}>
{deliverImg ? '重新拍照/上传' : '拍照/上传'}
</Button>
</View>
{deliverImg && (
<View className="mt-3">
<Image src={deliverImg} width="100%" height="120" />
<View className="mt-2 flex justify-end">
<Button size="small" onClick={() => setDeliverImg(undefined)}>
</Button>
</View>
</View>
)}
<View className="mt-3 text-xs text-gray-500">
</View>
</View>
</Dialog>
</View>
)
}

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '水票核销'
})

View File

@@ -1,280 +0,0 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { View, Text } from '@tarojs/components'
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
import { Button, Card, ConfigProvider } from '@nutui/nutui-react-taro'
import { Scan, Success, Failure, Tips } from '@nutui/icons-react-taro'
import { decryptQrData } from '@/api/shop/shopGift'
import { getGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket'
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'
import { isValidJSON } from '@/utils/jsonUtils'
import { useUser } from '@/hooks/useUser'
type TicketPayload = {
userTicketId: number
qty?: number
userId?: number
t?: number
}
type VerifyRecord = {
id: number
time: string
success: boolean
message: string
ticketName?: string
userInfo?: string
qty?: number
}
const RiderTicketVerificationPage: React.FC = () => {
const { hasRole, isAdmin } = useUser()
const router = useRouter()
const [loading, setLoading] = useState(false)
const [lastTicket, setLastTicket] = useState<GltUserTicket | null>(null)
const [lastQty, setLastQty] = useState<number>(1)
const [records, setRecords] = useState<VerifyRecord[]>([])
const autoScanOnceRef = useRef(false)
const canVerify = useMemo(() => {
return (
hasRole('rider') ||
hasRole('store') ||
hasRole('staff') ||
hasRole('admin') ||
isAdmin()
)
}, [hasRole, isAdmin])
const autoScanEnabled = useMemo(() => {
const p: any = router?.params || {}
return p.auto === '1' || p.auto === 'true'
}, [router])
const addRecord = (rec: Omit<VerifyRecord, 'id' | 'time'>) => {
const item: VerifyRecord = {
id: Date.now(),
time: new Date().toLocaleString(),
...rec
}
setRecords(prev => [item, ...prev].slice(0, 10))
}
const parsePayload = (raw: string): TicketPayload => {
const trimmed = raw.trim()
if (!isValidJSON(trimmed)) throw new Error('无效的水票核销信息')
const payload = JSON.parse(trimmed) as TicketPayload
const userTicketId = Number(payload.userTicketId)
const qty = Math.max(1, Number(payload.qty || 1))
if (!Number.isFinite(userTicketId) || userTicketId <= 0) {
throw new Error('水票核销信息无效')
}
return { ...payload, userTicketId, qty }
}
const extractPayloadFromScanResult = async (scanResult: string): Promise<TicketPayload> => {
const trimmed = scanResult.trim()
// 1) 加密二维码:{ businessType, token, data }
if (isValidJSON(trimmed)) {
const json = JSON.parse(trimmed) as any
if (json?.businessType && json?.token && json?.data) {
if (json.businessType !== 'ticket') {
throw new Error('请扫描水票核销码')
}
const decrypted = await decryptQrData({
token: String(json.token),
encryptedData: String(json.data)
})
return parsePayload(String(decrypted || ''))
}
// 2) 明文 payload内部调试/非加密二维码)
if (json?.userTicketId) {
return parsePayload(trimmed)
}
}
throw new Error('无效的水票核销码')
}
const verifyTicket = async (payload: TicketPayload) => {
const userTicketId = Number(payload.userTicketId)
const qty = Math.max(1, Number(payload.qty || 1))
const ticket = await getGltUserTicket(userTicketId)
if (!ticket) throw new Error('水票不存在')
if (ticket.status === 1) throw new Error('该水票已冻结')
const available = Number(ticket.availableQty || 0)
const used = Number(ticket.usedQty || 0)
if (available < qty) throw new Error('水票可用次数不足')
const lines: string[] = []
lines.push(`水票:${ticket.templateName || '水票'}`)
lines.push(`本次核销:${qty}`)
lines.push(`剩余可用:${available - qty}`)
if (ticket.phone) lines.push(`用户手机号:${ticket.phone}`)
if (ticket.nickname) lines.push(`用户昵称:${ticket.nickname}`)
const modalRes = await Taro.showModal({
title: '确认核销',
content: lines.join('\n')
})
if (!modalRes.confirm) return
await updateGltUserTicket({
...ticket,
availableQty: available - qty,
usedQty: used + qty
})
setLastTicket({
...ticket,
availableQty: available - qty,
usedQty: used + qty
})
setLastQty(qty)
addRecord({
success: true,
message: `核销成功(${qty}次)`,
ticketName: ticket.templateName || '水票',
userInfo: [ticket.nickname, ticket.phone].filter(Boolean).join(' / ') || undefined,
qty
})
Taro.showToast({ title: '核销成功', icon: 'success' })
}
const handleScan = async () => {
if (loading) return
if (!canVerify) {
Taro.showToast({ title: '您没有核销权限', icon: 'none' })
return
}
try {
setLoading(true)
const res = await Taro.scanCode({})
const scanResult = res?.result
if (!scanResult) throw new Error('未识别到二维码内容')
const payload = await extractPayloadFromScanResult(scanResult)
await verifyTicket(payload)
} catch (e: any) {
const msg = e?.message || '核销失败'
addRecord({ success: false, message: msg })
Taro.showToast({ title: msg, icon: 'none' })
} finally {
setLoading(false)
}
}
// If navigated in "auto" mode, open scan on first show when user has permission.
useDidShow(() => {
// Reset the flag when user manually re-enters the page via navigation again.
// (This runs on every show; only the first show with auto enabled will trigger scan.)
if (!autoScanEnabled) autoScanOnceRef.current = false
})
useEffect(() => {
if (!autoScanEnabled) return
if (autoScanOnceRef.current) return
if (!canVerify) return
autoScanOnceRef.current = true
// Defer to ensure page is fully mounted before opening camera.
setTimeout(() => {
handleScan().catch(() => {})
}, 80)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoScanEnabled, canVerify])
return (
<ConfigProvider>
<View className="min-h-screen bg-gray-50 p-4">
<Card>
<View className="flex items-center justify-between">
<View>
<Text className="text-base font-bold text-gray-800"></Text>
<View className="mt-1">
<Text className="text-xs text-gray-500">
</Text>
</View>
</View>
<Tips className="text-gray-400" size="18" />
</View>
<View className="mt-4">
<Button
type="primary"
block
loading={loading}
icon={<Scan />}
onClick={handleScan}
>
</Button>
</View>
{lastTicket && (
<View className="mt-4 bg-gray-50 rounded-lg p-3">
<View className="flex items-center justify-between">
<Text className="text-sm text-gray-700"></Text>
<Text className="text-xs text-gray-500">使 {lastQty} </Text>
</View>
<View className="mt-2">
<Text className="text-sm text-gray-900">
{lastTicket.templateName || '水票'} {lastTicket.availableQty ?? 0}
</Text>
</View>
</View>
)}
</Card>
<View className="mt-4">
<View className="mb-2">
<Text className="text-sm font-semibold text-gray-800"></Text>
<Text className="text-xs text-gray-500 ml-2">10</Text>
</View>
{records.length === 0 ? (
<View className="bg-white rounded-lg p-4">
<Text className="text-sm text-gray-500"></Text>
</View>
) : (
<View className="space-y-2">
{records.map(r => (
<View key={r.id} className="bg-white rounded-lg p-3 flex items-start justify-between">
<View className="flex-1 pr-3">
<View className="flex items-center">
{r.success ? (
<Success className="text-green-500 mr-2" size="16" />
) : (
<Failure className="text-red-500 mr-2" size="16" />
)}
<Text className="text-sm text-gray-900">{r.message}</Text>
</View>
<View className="mt-1">
<Text className="text-xs text-gray-500">
{r.time}
{r.ticketName ? ` · ${r.ticketName}` : ''}
{typeof r.qty === 'number' ? ` · ${r.qty}` : ''}
</Text>
</View>
{r.userInfo && (
<View className="mt-1">
<Text className="text-xs text-gray-500">{r.userInfo}</Text>
</View>
)}
</View>
</View>
))}
</View>
)}
</View>
</View>
</ConfigProvider>
)
}
export default RiderTicketVerificationPage

View File

@@ -1,31 +0,0 @@
import { useEffect, useState } from 'react'
import { Swiper } from '@nutui/nutui-react-taro'
import {CmsAd} from "@/api/cms/cmsAd/model";
import {Image} from '@nutui/nutui-react-taro'
import {getCmsAd} from "@/api/cms/cmsAd";
const MyPage = () => {
const [item, setItem] = useState<CmsAd>()
const reload = () => {
getCmsAd(439).then(data => {
setItem(data)
})
}
useEffect(() => {
reload()
}, [])
return (
<>
<Swiper defaultValue={0} height={item?.height} indicator style={{ height: item?.height + 'px', display: 'none' }}>
{item?.imageList?.map((item) => (
<Swiper.Item key={item}>
<Image width="100%" height="100%" src={item.url} mode={'scaleToFill'} lazyLoad={false} style={{ height: item.height + 'px' }} />
</Swiper.Item>
))}
</Swiper>
</>
)
}
export default MyPage

View File

@@ -1,54 +0,0 @@
import {Image} from '@nutui/nutui-react-taro'
import {Share} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
import './GoodsList.scss'
const GoodsList = (props: any) => {
return (
<>
<View className={'py-3'}>
<View className={'flex flex-col justify-between items-center rounded-lg px-2'}>
{props.data?.map((item: any, index: number) => {
return (
<View key={index} className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
<Image src={item.image} mode={'aspectFit'} lazyLoad={false}
radius="10px 10px 0 0" height="180"
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
<View className={'flex flex-col p-2 rounded-lg'}>
<View>
<View className={'car-no text-sm'}>{item.name}</View>
<View className={'flex justify-between text-xs py-1'}>
<Text className={'text-orange-500'}>{item.comments}</Text>
<Text className={'text-gray-400'}> {item.sales}</Text>
</View>
<View className={'flex justify-between items-center py-2'}>
<View className={'flex text-red-500 text-xl items-baseline'}>
<Text className={'text-xs'}></Text>
<Text className={'font-bold text-2xl'}>{item.price}</Text>
<Text className={'text-xs px-1'}></Text>
<Text className={'text-xs text-gray-400 line-through'}>{item.salePrice}</Text>
</View>
<View className={'buy-btn'}>
<View className={'cart-icon'}>
<Share size={20} className={'mx-4 mt-2'}
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
</View>
<View className={'text-white pl-4 pr-5'}
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>
</View>
</View>
</View>
</View>
</View>
</View>
)
})}
</View>
</View>
</>
)
}
export default GoodsList

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '商品分类',
navigationBarTextStyle: 'black'
})

View File

@@ -1,71 +0,0 @@
import Taro from '@tarojs/taro'
import GoodsList from './components/GoodsList'
import {useShareAppMessage} from "@tarojs/taro"
import {Loading} from '@nutui/nutui-react-taro'
import {useEffect, useState} from "react"
import {useRouter} from '@tarojs/taro'
import './index.scss'
import {pageShopGoods} from "@/api/shop/shopGoods"
import {ShopGoods} from "@/api/shop/shopGoods/model"
import {getCmsNavigation} from "@/api/cms/cmsNavigation";
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
function Category() {
const {params} = useRouter();
const [categoryId, setCategoryId] = useState<number>(0)
const [loading, setLoading] = useState<boolean>(true)
const [nav, setNav] = useState<CmsNavigation>()
const [list, setList] = useState<ShopGoods[]>([])
const reload = async () => {
// 1.加载远程数据
const id = Number(params.id)
const nav = await getCmsNavigation(id)
const shopGoods = await pageShopGoods({categoryId: id})
// 2.处理业务逻辑
setCategoryId(id)
setNav(nav)
setList(shopGoods?.list || [])
// 3.设置标题
Taro.setNavigationBarTitle({
title: `${nav?.categoryName}`
})
};
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, []);
useShareAppMessage(() => {
return {
title: `${nav?.categoryName}_易赊宝`,
path: `/shop/category/index?id=${categoryId}`,
success: function () {
console.log('分享成功');
},
fail: function () {
console.log('分享失败');
}
};
});
if (loading) {
return (
<Loading className={'px-2 text-center'}></Loading>
)
}
return (
<>
<div className={'flex flex-col'}>
<GoodsList data={list} nav={nav}/>
</div>
</>
)
}
export default Category

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '全部评论',
navigationBarTextStyle: 'black'
})

View File

@@ -1,5 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '商品详情',
navigationBarTextStyle: 'black',
navigationStyle: 'custom'
})

View File

@@ -1,25 +0,0 @@
.cart-icon{
background: linear-gradient(to bottom, #bbe094, #4ee265);
border-radius: 100px 0 0 100px;
height: 70px;
}
/* 去掉 RichText 中图片的间距 */
rich-text img {
margin: 0 !important;
padding: 0 !important;
display: block;
}
/* 在全局样式或组件样式文件中 */
.no-margin {
margin: 0 !important; /* 使用 !important 来确保覆盖默认样式 */
}
/* 文本截断样式 */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
}

View File

@@ -1,486 +0,0 @@
import {useEffect, useState} from "react";
import {Image, Badge, Popup, CellGroup, Cell} from "@nutui/nutui-react-taro";
import {ArrowLeft, Headphones, Share, Cart, ArrowRight} from "@nutui/icons-react-taro";
import Taro, {useShareAppMessage} from "@tarojs/taro";
import {RichText, View, Text} from '@tarojs/components'
import {ShopGoods} from "@/api/shop/shopGoods/model";
import {getShopGoods} from "@/api/shop/shopGoods";
import {listShopGoodsSpec} from "@/api/shop/shopGoodsSpec";
import {ShopGoodsSpec} from "@/api/shop/shopGoodsSpec/model";
import {listShopGoodsSku} from "@/api/shop/shopGoodsSku";
import {ShopGoodsSku} from "@/api/shop/shopGoodsSku/model";
import {Swiper} from '@nutui/nutui-react-taro'
import navTo, {wxParse} from "@/utils/common";
import SpecSelector from "@/components/SpecSelector";
import "./index.scss";
import {useCart} from "@/hooks/useCart";
import {useConfig} from "@/hooks/useConfig";
import {parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite";
import { ensureLoggedIn } from '@/utils/auth'
import {getGltTicketTemplateByGoodsId} from "@/api/glt/gltTicketTemplate";
import type {GltTicketTemplate} from "@/api/glt/gltTicketTemplate/model";
const GoodsDetail = () => {
const [statusBarHeight, setStatusBarHeight] = useState<number>(44);
const [windowWidth, setWindowWidth] = useState<number>(390)
const [goods, setGoods] = useState<ShopGoods | null>(null);
const [files, setFiles] = useState<any[]>([]);
const [specs, setSpecs] = useState<ShopGoodsSpec[]>([]);
const [skus, setSkus] = useState<ShopGoodsSku[]>([]);
const [showSpecSelector, setShowSpecSelector] = useState(false);
const [specAction, setSpecAction] = useState<'cart' | 'buy'>('cart');
const [showBottom, setShowBottom] = useState(false)
const [bottomItem, setBottomItem] = useState<any>({
title: '',
content: ''
})
// 水票套票模板:存在时该商品不允许加入购物车(购物车无法支付此类商品)
const [ticketTemplate, setTicketTemplate] = useState<GltTicketTemplate | null>(null)
const [ticketTemplateChecked, setTicketTemplateChecked] = useState(false)
// const [selectedSku, setSelectedSku] = useState<ShopGoodsSku | null>(null);
const [loading, setLoading] = useState(false);
const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.id;
// 使用购物车Hook
const {cartCount, addToCart} = useCart()
const {config} = useConfig()
// 如果从分享链接进入(携带 inviter/source/t且当前未登录则暂存邀请信息用于注册后绑定关系
useEffect(() => {
try {
const currentUserId = Taro.getStorageSync('UserId')
if (currentUserId) return
const inviteParams = parseInviteParams({query: router?.params})
if (inviteParams?.inviter) {
saveInviteParams(inviteParams)
trackInviteSource(inviteParams.source || 'share', parseInt(inviteParams.inviter))
}
} catch (e) {
// 邀请参数解析/存储失败不影响正常浏览商品
console.error('商品详情页处理邀请参数失败:', e)
}
// router 在 Taro 中可能不稳定;这里仅在 goodsId 变化时尝试处理一次即可
}, [goodsId])
// 处理加入购物车
const handleAddToCart = async () => {
if (!goods) return;
// 水票套票商品:不允许加入购物车(购物车无法支付)
// 优先使用已加载的 ticketTemplate若尚未加载则补一次查询
let tpl = ticketTemplate
let checked = ticketTemplateChecked
if (!tpl && goods?.goodsId) {
try {
tpl = await getGltTicketTemplateByGoodsId(Number(goods.goodsId))
setTicketTemplate(tpl)
setTicketTemplateChecked(true)
checked = true
} catch (_e) {
tpl = null
setTicketTemplateChecked(true)
checked = true
}
}
if (!checked || tpl) {
return
}
if (!ensureLoggedIn(`/shop/goodsDetail/index?id=${goods.goodsId}`)) return
// 如果有规格,显示规格选择器
if (specs.length > 0) {
setSpecAction('cart');
setShowSpecSelector(true);
return;
}
// 没有规格,直接加入购物车
addToCart({
goodsId: goods.goodsId!,
name: goods.name || '',
price: goods.price || '0',
image: goods.image || ''
});
};
// 处理立即购买
const handleBuyNow = () => {
if (!goods) return;
if (!ensureLoggedIn(`/shop/orderConfirm/index?goodsId=${goods.goodsId}`)) return
// 如果有规格,显示规格选择器
if (specs.length > 0) {
setSpecAction('buy');
setShowSpecSelector(true);
return;
}
// 没有规格,直接购买
navTo(`/shop/orderConfirm/index?goodsId=${goods?.goodsId}`, true);
};
// 规格选择确认回调
const handleSpecConfirm = async (sku: ShopGoodsSku, quantity: number, action?: 'cart' | 'buy') => {
// setSelectedSku(sku);
setShowSpecSelector(false);
if (action === 'cart') {
// 水票套票商品:不允许加入购物车(购物车无法支付)
let tpl = ticketTemplate
let checked = ticketTemplateChecked
if (!tpl && goods?.goodsId) {
try {
tpl = await getGltTicketTemplateByGoodsId(Number(goods.goodsId))
setTicketTemplate(tpl)
setTicketTemplateChecked(true)
checked = true
} catch (_e) {
tpl = null
setTicketTemplateChecked(true)
checked = true
}
}
if (!checked || tpl) {
return
}
// 加入购物车
addToCart({
goodsId: goods!.goodsId!,
skuId: sku.id,
name: goods!.name || '',
price: sku.price || goods!.price || '0',
image: goods!.image || '',
specInfo: sku.sku, // sku字段包含规格信息
}, quantity);
} else if (action === 'buy') {
// 立即购买
const orderData = {
goodsId: goods!.goodsId!,
skuId: sku.id,
quantity,
price: sku.price || goods!.price || '0'
};
navTo(`/shop/orderConfirm/index?orderData=${encodeURIComponent(JSON.stringify(orderData))}`, true);
} else {
// 默认情况如果action未定义默认为立即购买
const orderData = {
goodsId: goods!.goodsId!,
skuId: sku.id,
quantity,
price: sku.price || goods!.price || '0'
};
navTo(`/shop/orderConfirm/index?orderData=${encodeURIComponent(JSON.stringify(orderData))}`, true);
}
};
const openBottom = (title: string, content: string) => {
setBottomItem({
title,
content
})
setShowBottom(true)
}
useEffect(() => {
let alive = true
Taro.getSystemInfo({
success: (res) => {
if (!alive) return
setWindowWidth(res.windowWidth)
setStatusBarHeight(Number(res.statusBarHeight) + 5)
},
});
if (goodsId) {
setLoading(true);
// 切换商品时先重置套票模板,避免复用上一个商品状态
setTicketTemplate(null)
setTicketTemplateChecked(false)
// 加载商品详情
getShopGoods(Number(goodsId))
.then((res) => {
// 处理富文本内容,去掉图片间距
if (res.content) {
res.content = wxParse(res.content);
}
if (!alive) return
setGoods(res);
if (res.files) {
const arr = JSON.parse(res.files);
arr.length > 0 && setFiles(arr);
}
})
.catch((error) => {
console.error("Failed to fetch goods detail:", error);
})
.finally(() => {
if (!alive) return
setLoading(false);
});
// 查询商品是否绑定水票模板(失败/无数据时不影响正常浏览)
getGltTicketTemplateByGoodsId(Number(goodsId))
.then((tpl) => {
if (!alive) return
setTicketTemplate(tpl)
setTicketTemplateChecked(true)
})
.catch((_e) => {
if (!alive) return
setTicketTemplate(null)
setTicketTemplateChecked(true)
})
// 加载商品规格
listShopGoodsSpec({goodsId: Number(goodsId)} as any)
.then((data) => {
if (!alive) return
setSpecs(data || []);
})
.catch((error) => {
console.error("Failed to fetch goods specs:", error);
});
// 加载商品SKU
listShopGoodsSku({goodsId: Number(goodsId)} as any)
.then((data) => {
if (!alive) return
setSkus(data || []);
})
.catch((error) => {
console.error("Failed to fetch goods skus:", error);
});
}
return () => {
alive = false
}
}, [goodsId]);
// 分享给好友
useShareAppMessage(() => {
const inviter = Taro.getStorageSync('UserId')
const sharePath =
inviter
? `/shop/goodsDetail/index?id=${goodsId}&inviter=${inviter}&source=goods_share&t=${Date.now()}`
: `/shop/goodsDetail/index?id=${goodsId}`
return {
title: goods?.name || '精选商品',
path: sharePath,
imageUrl: goods?.image ? `${goods.image}?x-oss-process=image/resize,w_500,h_400,m_fill` : undefined, // 分享图片调整为5:4比例
success: function (res: any) {
console.log('分享成功', res);
Taro.showToast({
title: '分享成功',
icon: 'success',
duration: 2000
});
},
fail: function (res: any) {
console.log('分享失败', res);
Taro.showToast({
title: '分享失败',
icon: 'none',
duration: 2000
});
}
};
});
if (!goods || loading) {
return <View>...</View>;
}
const showAddToCart = ticketTemplateChecked && !ticketTemplate
return (
<View className={"py-0"}>
<View
className={
"fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-70"
}
style={{
borderRadius: "100%",
width: "32px",
height: "32px",
top: statusBarHeight + 'px',
left: "10px",
}}
onClick={() => Taro.navigateBack()}
>
<ArrowLeft size={14}/>
</View>
<View className={
"fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-90"
}
style={{
borderRadius: "100%",
width: "32px",
height: "32px",
top: statusBarHeight + 'px',
right: "110px",
}}
onClick={() => Taro.switchTab({url: `/pages/cart/cart`})}>
<Badge value={cartCount} top="-2" right="2">
<View style={{display: 'flex', alignItems: 'center'}}>
<Cart size={16}/>
</View>
</Badge>
</View>
{
files.length > 0 && (
<Swiper defaultValue={0} indicator height={windowWidth}>
{files.map((item) => (
<Swiper.Item key={item}>
<Image width={windowWidth} height={windowWidth} src={item.url} mode={'scaleToFill'} lazyLoad={false}/>
</Swiper.Item>
))}
</Swiper>
)
}
{
files.length == 0 && (
<Image
src={goods.image}
mode={"scaleToFill"}
radius="10px 10px 0 0"
height="300"
/>
)
}
<View
className={"flex flex-col justify-between items-center"}
>
<View
className={
"flex flex-col bg-white w-full"
}
>
<View className={"flex flex-col p-3 rounded-lg"}>
<>
<View className={'flex justify-between'}>
<View className={'flex text-red-500 text-xl items-baseline'}>
<Text className={'text-xs'}></Text>
<Text className={'font-bold text-2xl'}>{goods.price}</Text>
<Text className={'text-xs px-1'}></Text>
<Text className={'text-xs text-gray-400 line-through'}>{goods.salePrice}</Text>
</View>
<span className={"text-gray-400 text-xs"}> {goods.sales}</span>
</View>
<View className={'flex justify-between items-center'}>
<View className={'goods-info'}>
<View className={"car-no text-lg"}>
{goods.name}
</View>
<View className={"flex justify-between text-xs py-1"}>
<span className={"text-orange-500"}>
{goods.comments}
</span>
</View>
</View>
<View>
<button
className={'flex flex-col justify-center items-center bg-white text-gray-500 px-1 gap-1 text-nowrap whitespace-nowrap'}
open-type="share">
<Share size={20}/>
<span className={'text-xs'}></span>
</button>
</View>
</View>
</>
</View>
</View>
<View className={'w-full'}>
{
config?.deliveryText && (
<CellGroup>
<Cell title={'配送'} extra={
<View className="flex items-center">
<Text className={'truncate max-w-56 inline-block'}>{config?.deliveryText || '14:30下单明天配送'}</Text>
<ArrowRight color="#cccccc" size={15}/>
</View>
} onClick={() => openBottom('配送', `${config?.deliveryText}`)}/>
<Cell title={'保障'} extra={
<View className="flex items-center">
<Text className={'truncate max-w-56 inline-block'}>{config?.guaranteeText || '支持7天无理由退货'}</Text>
<ArrowRight color="#cccccc" size={15}/>
</View>
} onClick={() => openBottom('保障', `${config?.guaranteeText}`)}/>
</CellGroup>
)
}
{config?.openComments == '1' && (
<CellGroup>
<Cell title={'用户评价 (0)'} extra={
<>
<Text></Text>
<ArrowRight color="#cccccc" size={15}/>
</>
} onClick={() => navTo(`/shop/comments/index`)}/>
<Cell className={'flex h-32 bg-white p-4'}>
</Cell>
</CellGroup>
)}
</View>
<View className={'w-full'}>
<RichText nodes={goods.content || '内容详情'}/>
<View className={'h-24'}></View>
</View>
</View>
{/*底部弹窗*/}
<Popup
visible={showBottom}
position="bottom"
onClose={() => {
setShowBottom(false)
}}
lockScroll
>
<View className={'flex flex-col p-4'}>
<Text className={'font-bold text-sm'}>{bottomItem.title}</Text>
<Text className={'text-gray-500 my-2'}>{bottomItem.content}</Text>
</View>
</Popup>
{/*底部购买按钮*/}
<View className={'fixed bg-white w-full bottom-0 left-0 pt-4 pb-6'}>
<View className={'btn-bar flex justify-between items-center'}>
<View className={'flex justify-center items-center mx-4'}>
<button open-type="contact" className={'flex items-center text-sm py-2'}>
<Headphones size={16} style={{marginRight: '4px'}}/>
</button>
</View>
<View className={'buy-btn mx-4'}>
{showAddToCart && (
<View className={'cart-add px-4 text-sm'}
onClick={() => handleAddToCart()}>
</View>
)}
<View className={`cart-buy text-sm ${showAddToCart ? 'pl-4 pr-5' : 'cart-buy-only px-4'}`}
onClick={() => handleBuyNow()}>
</View>
</View>
</View>
</View>
{/* 规格选择器 */}
{showSpecSelector && (
<SpecSelector
goods={goods!}
specs={specs}
skus={skus}
action={specAction}
onConfirm={handleSpecConfirm}
onClose={() => setShowSpecSelector(false)}
/>
)}
</View>
);
};
export default GoodsDetail;

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '订单确认',
navigationBarTextStyle: 'black'
})

View File

@@ -1,116 +0,0 @@
.order-confirm-page {
padding-bottom: 100px; // 留出底部固定按钮的空间
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
.error-text {
color: #999;
margin-bottom: 20px;
font-size: 14px;
}
}
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #fff;
border-top: 1px solid #eee;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
.total-price {
display: flex;
align-items: center;
}
.submit-btn {
width: 150px;
}
}
}
.address-bottom-line{
width: 100%;
border-radius: 12rpx 12rpx 0 0;
background: #fff;
padding: 26rpx 49rpx 0 34rpx;
position: relative;
&:before {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 5px;
background: repeating-linear-gradient(-45deg, #ff6c6c, #ff6c6c 20%, transparent 0, transparent 25%, #1989fa 0,
#1989fa 45%, transparent 0, transparent 50%);
background-size: 120px;
content: "";
}
}
// 优惠券弹窗样式
.coupon-popup {
height: 100%;
display: flex;
flex-direction: column;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
&__title {
font-size: 16px;
font-weight: 600;
color: #333;
}
&__content {
flex: 1;
overflow-y: auto;
}
&__loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
color: #999;
font-size: 14px;
}
&__current {
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #f0f0f0;
&-title {
font-size: 28rpx;
color: #666;
margin-bottom: 8px;
}
&-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8rpx 12rpx;
background: #fff;
border-radius: 6rpx;
font-size: 28rpx;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '订单确认',
navigationBarTextStyle: 'black'
})

View File

@@ -1,44 +0,0 @@
.order-confirm-page {
padding-bottom: 100px; // 留出底部固定按钮的空间
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #fff;
border-top: 1px solid #eee;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
.total-price {
display: flex;
align-items: center;
}
.submit-btn {
width: 150px;
}
}
}
.address-bottom-line{
width: 100%;
border-radius: 24rpx 24rpx 0 0;
background: #fff;
padding: 26rpx 49rpx 0 34rpx;
position: relative;
&:before {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 5px;
background: repeating-linear-gradient(-45deg, #ff6c6c, #ff6c6c 20%, transparent 0, transparent 25%, #1989fa 0,
#1989fa 45%, transparent 0, transparent 50%);
background-size: 120px;
content: "";
}
}

View File

@@ -1,209 +0,0 @@
import {useEffect, useState} from "react";
import {Image, Button, Cell, CellGroup, Input, Space} from '@nutui/nutui-react-taro'
import {Location, ArrowRight} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components';
import {listShopUserAddress} from "@/api/shop/shopUserAddress";
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
import './index.scss'
import {useCart, CartItem} from "@/hooks/useCart";
import Gap from "@/components/Gap";
import {Payment} from "@/api/system/payment/model";
import {PaymentHandler, PaymentType, buildCartOrder} from "@/utils/payment";
import { ensureLoggedIn } from '@/utils/auth'
const OrderConfirm = () => {
const [address, setAddress] = useState<ShopUserAddress>()
const [payment] = useState<Payment>()
const [checkoutItems, setCheckoutItems] = useState<CartItem[]>([]);
const {
cartItems,
removeFromCart
} = useCart();
const reload = async () => {
const addressList = await listShopUserAddress({isDefault: true});
if (addressList.length > 0) {
setAddress(addressList[0])
}
}
// 页面级兜底:防止未登录时进入结算页导致接口报错/仅提示“请先登录”
useEffect(() => {
// redirect 到当前结算页,登录成功后返回继续支付
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
}, [])
// 加载结算商品数据
const loadCheckoutItems = () => {
try {
const checkoutData = Taro.getStorageSync('checkout_items');
if (checkoutData) {
const items = JSON.parse(checkoutData) as CartItem[];
setCheckoutItems(items);
// 清除临时存储的数据
Taro.removeStorageSync('checkout_items');
} else {
// 如果没有选中商品数据,使用全部购物车商品
setCheckoutItems(cartItems);
}
} catch (error) {
console.error('加载结算商品失败:', error);
setCheckoutItems(cartItems);
}
};
/**
* 统一支付入口
*/
const onPay = async () => {
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
// 基础校验
if (!address) {
Taro.showToast({
title: '请选择收货地址',
icon: 'error'
});
return;
}
if (!checkoutItems || checkoutItems.length === 0) {
Taro.showToast({
title: '没有要结算的商品',
icon: 'error'
});
return;
}
// 构建订单数据
const orderData = buildCartOrder(
checkoutItems.map(item => ({
goodsId: item.goodsId,
quantity: item.quantity || 1
})),
address.id,
{
comments: '购物车下单',
deliveryType: 0
}
);
// 根据支付方式选择支付类型,默认微信支付
const paymentType = payment?.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
// 执行支付
await PaymentHandler.pay(orderData, paymentType, {
onSuccess: () => {
// 支付成功后,从购物车中移除已下单的商品
checkoutItems.forEach(item => {
removeFromCart(item.goodsId);
});
}
});
};
useEffect(() => {
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
reload().then();
loadCheckoutItems();
}, [cartItems]);
// 计算总价
const getTotalPrice = () => {
return checkoutItems.reduce((total, item) => {
return total + (parseFloat(item.price) * item.quantity);
}, 0).toFixed(2);
};
// 计算商品总数量
const getTotalQuantity = () => {
return checkoutItems.reduce((total, item) => total + item.quantity, 0);
};
return (
<div className={'order-confirm-page'}>
<CellGroup>
{
address && (
<Cell className={'address-bottom-line'} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
<Space>
<Location/>
<View className={'flex flex-col w-full justify-between items-start'}>
<Space className={'flex flex-row w-full font-medium'}>
<View className={'flex-wrap text-nowrap whitespace-nowrap'}></View>
<View style={{width: '64%'}}
className={'line-clamp-1 relative'}>{address.province} {address.city} {address.region} {address.address}
</View>
</Space>
<View className={'pt-1 pb-3 text-gray-500'}>{address.name} {address.phone}</View>
</View>
</Space>
</Cell>
)
}
{!address && (
<Cell className={''} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
<Space>
<Location/>
</Space>
</Cell>
)}
</CellGroup>
<CellGroup>
{checkoutItems.map((item) => (
<Cell key={item.goodsId}>
<Space>
<Image src={item.image} mode={'aspectFill'} style={{
width: '80px',
height: '80px',
}} lazyLoad={false}/>
<View className={'flex flex-col'}>
<View className={'font-medium w-full'}>{item.name}</View>
{/*<View className={'number text-gray-400 text-sm py-2'}>80g/袋</View>*/}
<Space className={'flex justify-start items-center'}>
<View className={'text-red-500'}>{item.price}</View>
<View className={'text-gray-500 text-sm'}>x {item.quantity}</View>
</Space>
</View>
</Space>
</Cell>
))}
</CellGroup>
<CellGroup>
<Cell title={`商品总价(共${getTotalQuantity()}件)`} extra={<View className={'font-medium'}>{'¥' + getTotalPrice()}</View>}/>
<Cell title={'优惠券'} extra={(
<View className={'flex justify-between items-center'}>
<View className={'text-red-500 text-sm mr-1'}>-0.00</View>
<ArrowRight className={'text-gray-400'} size={14}/>
</View>
)}/>
{/*<Cell title={'配送费'} extra={'¥' + 10}/>*/}
<Cell title={'订单备注'} extra={(
<Input placeholder={'选填,请先和商家协商一致'} style={{ padding: '0'}}/>
)}/>
</CellGroup>
<Gap height={50} />
<div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
<View className={'btn-bar flex justify-between items-center'}>
<div className={'flex justify-center items-center mx-4'}>
<span className={'total-price text-sm text-gray-500'}></span>
<span className={'text-red-500 text-xl font-bold'}>{getTotalPrice()}</span>
</div>
<div className={'buy-btn mx-4'}>
<Button type="success" size="large" onClick={onPay}></Button>
</div>
</View>
</div>
</div>
);
};
export default OrderConfirm;

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '订单详情',
navigationBarTextStyle: 'black'
})

View File

@@ -1,26 +0,0 @@
.order-detail-page {
padding-bottom: 80px; // 留出底部固定按钮的空间
.nut-cell-group__title {
padding: 10px 16px;
color: #999;
}
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: flex-end;
align-items: center;
padding: 10px 20px;
background-color: #fff;
border-top: 1px solid #eee;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
.nut-button {
margin-left: 10px;
}
}
}

View File

@@ -1,282 +0,0 @@
import {useEffect, useState} from "react";
import {Cell, CellGroup, Image, Space, Button, Dialog} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import {ShopOrder} from "@/api/shop/shopOrder/model";
import {getShopOrder, updateShopOrder, refundShopOrder} from "@/api/shop/shopOrder";
import {listShopOrderGoods} from "@/api/shop/shopOrderGoods";
import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
import dayjs from "dayjs";
import PaymentCountdown from "@/components/PaymentCountdown";
import './index.scss'
// 申请退款:支付成功后仅允许在指定时间窗内发起(前端展示层限制,后端仍应校验)
const isWithinRefundWindow = (payTime?: string, windowMinutes: number = 60): boolean => {
if (!payTime) return false;
const raw = String(payTime).trim();
const t = /^\d+$/.test(raw)
? dayjs(Number(raw) < 1e12 ? Number(raw) * 1000 : Number(raw)) // 兼容秒/毫秒时间戳
: dayjs(raw);
if (!t.isValid()) return false;
return dayjs().diff(t, 'minute') <= windowMinutes;
};
const OrderDetail = () => {
const [order, setOrder] = useState<ShopOrder | null>(null);
const [orderGoodsList, setOrderGoodsList] = useState<ShopOrderGoods[]>([]);
const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
const router = Taro.getCurrentInstance().router;
const orderId = router?.params?.orderId;
// 处理支付超时
const handlePaymentExpired = async () => {
if (!order) return;
if (!order.orderId) return;
try {
// 自动取消过期订单
await updateShopOrder({
// 只传最小字段,避免误取消/误走售后流程
orderId: order.orderId,
orderStatus: 2 // 已取消
});
// 更新本地状态
setOrder(prev => prev ? {...prev, orderStatus: 2} : null);
Taro.showToast({
title: '订单已自动取消',
icon: 'none',
duration: 2000
});
} catch (error) {
console.error('自动取消订单失败:', error);
}
};
// 申请退款
const handleApplyRefund = async () => {
if (order) {
try {
const confirm = await Taro.showModal({
title: '申请退款',
content: '确认要申请退款吗?',
confirmText: '确认',
cancelText: '取消'
})
if (!confirm?.confirm) return
Taro.showLoading({ title: '提交中...' })
// 退款相关操作使用退款接口PUT /api/shop/shop-order/refund
await refundShopOrder({
orderId: order.orderId,
refundMoney: order.payPrice || order.totalPrice,
orderStatus: 7
})
// 乐观更新本地状态
setOrder(prev => prev ? { ...prev, orderStatus: 7 } : null)
Taro.showToast({ title: '退款申请已提交', icon: 'success' })
} catch (error) {
console.error('申请退款失败:', error);
Taro.showToast({
title: '操作失败,请重试',
icon: 'none'
});
} finally {
try {
Taro.hideLoading()
} catch (_e) {
// ignore
}
}
}
};
// 确认收货(客户)
const handleConfirmReceive = async () => {
if (!order?.orderId) return
try {
setConfirmReceiveDialogVisible(false)
await updateShopOrder({
orderId: order.orderId,
deliveryStatus: order.deliveryStatus, // 10未发货 20已发货 30部分发货
orderStatus: 1 // 已完成
})
Taro.showToast({title: '确认收货成功', icon: 'success'})
setOrder(prev => (prev ? {...prev, orderStatus: 1} : prev))
} catch (e) {
console.error('确认收货失败:', e)
Taro.showToast({title: '确认收货失败', icon: 'none'})
setConfirmReceiveDialogVisible(true)
}
}
const getOrderStatusText = (order: ShopOrder) => {
// 优先检查订单状态
if (order.orderStatus === 2) return '已取消';
if (order.orderStatus === 3) return '取消中';
if (order.orderStatus === 4) return '退款申请中';
if (order.orderStatus === 5) return '退款被拒绝';
if (order.orderStatus === 6) return '退款成功';
if (order.orderStatus === 7) return '客户端申请退款';
// 检查支付状态 (payStatus为boolean类型)
if (!order.payStatus) return '待付款';
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return '待发货';
if (order.deliveryStatus === 20) {
// 若订单有配送员,则以配送员送达时间作为“可确认收货”的依据
if (order.riderId) {
if (order.sendEndTime && order.orderStatus !== 1) return '待确认收货';
return '配送中';
}
return '待收货';
}
if (order.deliveryStatus === 30) return '部分发货';
// 最后检查订单完成状态
if (order.orderStatus === 1) return '已完成';
if (order.orderStatus === 0) return '未使用';
return '未知状态';
};
const getPayTypeText = (payType?: number) => {
switch (payType) {
case 0:
return '余额支付';
case 1:
return '微信支付';
case 102:
return '微信Native';
case 2:
return '会员卡支付';
case 3:
return '支付宝';
case 4:
return '现金';
case 5:
return 'POS机';
default:
return '未知支付方式';
}
};
useEffect(() => {
if (orderId) {
console.log('shop-goods', orderId)
getShopOrder(Number(orderId)).then(async (res) => {
setOrder(res);
// 获取订单商品列表
const goodsRes = await listShopOrderGoods({orderId: Number(orderId)});
if (goodsRes && goodsRes.length > 0) {
setOrderGoodsList(goodsRes);
}
}).catch(error => {
console.error("Failed to fetch order detail:", error);
});
}
}, [orderId]);
if (!order) {
return <div>...</div>;
}
const currentUserId = Number(Taro.getStorageSync('UserId'))
const isOwner = !!currentUserId && currentUserId === order.userId
const canConfirmReceive =
isOwner &&
order.payStatus &&
order.orderStatus !== 1 &&
order.deliveryStatus === 20 &&
(!order.riderId || !!order.sendEndTime)
return (
<div className={'order-detail-page'}>
{/* 支付倒计时显示 - 详情页实时更新 */}
{!order.payStatus && order.orderStatus !== 2 && (
<div className="order-detail-countdown flex justify-center p-4 border-b border-gray-50">
<PaymentCountdown
expirationTime={order.expirationTime}
createTime={order.createTime}
payStatus={order.payStatus}
realTime={true}
showSeconds={true}
mode="badge"
onExpired={handlePaymentExpired}
/>
</div>
)}
<CellGroup>
{orderGoodsList.map((item, index) => (
<Cell key={index}>
<div className={'flex items-center'}>
<Image src={item.image || '/default-goods.png'} width="80" height="80" lazyLoad={false}/>
<div className={'ml-2'}>
<div className={'text-sm font-bold'}>{item.goodsName}</div>
{item.spec && <div className={'text-gray-500 text-xs'}>{item.spec}</div>}
<div className={'text-gray-500 text-xs'}>{item.totalNum}</div>
<div className={'text-red-500 text-lg'}>{item.price}</div>
</div>
</div>
</Cell>
))}
</CellGroup>
<CellGroup>
<Cell title="订单编号" description={order.orderNo}/>
<Cell title="下单时间" description={dayjs(order.createTime).format('YYYY-MM-DD HH:mm:ss')}/>
<Cell title="订单状态" description={getOrderStatusText(order)}/>
</CellGroup>
<CellGroup>
<Cell title="收货人" description={order.realName}/>
<Cell title="手机号" description={order.phone}/>
<Cell title="收货地址" description={order.address}/>
</CellGroup>
{order.payStatus && (
<CellGroup>
<Cell title="支付方式" description={getPayTypeText(order.payType)}/>
<Cell title="实付金额" description={`${order.payPrice}`}/>
</CellGroup>
)}
<View className={'h5-div fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-5 border-t border-gray-200'}>
<View className={'flex justify-end px-4'}>
<Space>
{!order.payStatus && <Button onClick={() => console.log('取消订单')}></Button>}
{!order.payStatus && <Button type="primary" onClick={() => console.log('立即支付')}></Button>}
{order.orderStatus === 1 && order.payStatus && isWithinRefundWindow(order.payTime, 60) && (
<Button onClick={handleApplyRefund}>退</Button>
)}
{canConfirmReceive && (
<Button type="primary" onClick={() => setConfirmReceiveDialogVisible(true)}>
</Button>
)}
</Space>
</View>
</View>
<Dialog
title="确认收货"
visible={confirmReceiveDialogVisible}
confirmText="确认收货"
cancelText="我再想想"
onConfirm={handleConfirmReceive}
onCancel={() => setConfirmReceiveDialogVisible(false)}
>
</Dialog>
</div>
);
};
export default OrderDetail;

View File

@@ -1,33 +0,0 @@
// 使用与首页相同的样式主要依赖Tailwind CSS类名
.buy-btn {
background: linear-gradient(to right, #1cd98a, #24ca94);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
.cart-icon {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.1);
border-radius: 20px 0 0 20px;
}
}
.car-no {
font-weight: 500;
color: #333;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}

View File

@@ -1,58 +0,0 @@
import { View } from '@tarojs/components'
import { Image } from '@nutui/nutui-react-taro'
import { Share } from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import { ShopGoods } from '@/api/shop/shopGoods/model'
import './GoodsItem.scss'
interface GoodsItemProps {
goods: ShopGoods
}
const GoodsItem = ({ goods }: GoodsItemProps) => {
// 跳转到商品详情
const goToDetail = () => {
Taro.navigateTo({
url: `/shop/goodsDetail/index?id=${goods.goodsId}`
})
}
return (
<View className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
<Image
src={goods.image || ''}
mode={'aspectFit'}
lazyLoad={false}
radius="10px 10px 0 0"
height="180"
onClick={goToDetail}
/>
<View className={'flex flex-col p-2 rounded-lg'}>
<View>
<View className={'car-no text-sm'}>{goods.name || goods.goodsName}</View>
<View className={'flex justify-between text-xs py-1'}>
<span className={'text-orange-500'}>{goods.comments || ''}</span>
<span className={'text-gray-400'}> {goods.sales || 0}</span>
</View>
<View className={'flex justify-between items-center py-2'}>
<View className={'flex text-red-500 text-xl items-baseline'}>
<span className={'text-xs'}></span>
<span className={'font-bold text-2xl'}>{goods.price || '0.00'}</span>
</View>
<View className={'buy-btn'}>
<View className={'cart-icon'}>
<Share size={20} className={'mx-4 mt-2'}
onClick={goToDetail}/>
</View>
<View className={'text-white pl-4 pr-5'}
onClick={goToDetail}>
</View>
</View>
</View>
</View>
</View>
</View>
)
}
export default GoodsItem

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '商品搜索'
})

View File

@@ -1,103 +0,0 @@
.search-page {
min-height: 100vh;
background: #f5f5f5;
// 搜索输入框样式
.search-input-wrapper {
flex: 1;
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 20px;
padding: 0 12px;
.search-icon {
color: #999;
margin-right: 8px;
}
.search-input {
flex: 1;
border: none;
background: transparent;
font-size: 14px;
input {
background: transparent !important;
}
}
}
.search-btn {
padding: 0 16px;
height: 36px;
border-radius: 18px;
font-size: 14px;
}
.search-content {
padding-top: calc(32px + env(safe-area-inset-top));
.search-history {
background: #fff;
margin-bottom: 8px;
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f5f5f5;
.history-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
.clear-btn {
font-size: 14px;
color: #999;
cursor: pointer;
}
}
.history-list {
padding: 16px;
display: flex;
flex-wrap: wrap;
gap: 12px;
.history-item {
padding: 8px 16px;
background: #f5f5f5;
border-radius: 16px;
color: #666;
cursor: pointer;
&:active {
background: #e5e5e5;
}
}
}
}
.search-results {
.result-header {
padding: 16px;
color: #666;
background: #fff;
border-bottom: 1px solid #f5f5f5;
margin-bottom: 8px;
}
.loading-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
background: #fff;
}
}
}
}

View File

@@ -1,237 +0,0 @@
import {SetStateAction, useEffect, useState} from 'react'
import {useRouter} from '@tarojs/taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import {Loading, Empty, InfiniteLoading, Input, Button} from '@nutui/nutui-react-taro'
import {Search} from '@nutui/icons-react-taro';
import {ShopGoods} from '@/api/shop/shopGoods/model'
import {pageShopGoods} from '@/api/shop/shopGoods'
import GoodsItem from './components/GoodsItem'
import './index.scss'
const SearchPage = () => {
const router = useRouter()
const [keywords, setKeywords] = useState<string>('')
const [goodsList, setGoodsList] = useState<ShopGoods[]>([])
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [total, setTotal] = useState(0)
const [searchHistory, setSearchHistory] = useState<string[]>([])
// 从路由参数获取搜索关键词
useEffect(() => {
const {keywords: routeKeywords} = router.params || {}
if (routeKeywords) {
setKeywords(decodeURIComponent(routeKeywords))
handleSearch(decodeURIComponent(routeKeywords), 1).then()
}
// 加载搜索历史
loadSearchHistory()
}, [])
// 加载搜索历史
const loadSearchHistory = () => {
try {
const history = Taro.getStorageSync('search_history') || []
setSearchHistory(history)
} catch (error) {
console.error('加载搜索历史失败:', error)
}
}
// 保存搜索历史
const saveSearchHistory = (keyword: string) => {
try {
let history = Taro.getStorageSync('search_history') || []
// 去重并添加到开头
history = history.filter((item: string) => item !== keyword)
history.unshift(keyword)
// 只保留最近10条
history = history.slice(0, 10)
Taro.setStorageSync('search_history', history)
setSearchHistory(history)
} catch (error) {
console.error('保存搜索历史失败:', error)
}
}
const handleKeywords = (keywords: SetStateAction<string>) => {
setKeywords(keywords)
handleSearch(typeof keywords === "string" ? keywords : '').then()
}
// 搜索商品
const handleSearch = async (searchKeywords: string, pageNum: number = 1) => {
if (!searchKeywords.trim()) {
Taro.showToast({
title: '请输入搜索关键词',
icon: 'none'
})
return
}
setLoading(true)
try {
const params = {
keywords: searchKeywords.trim(),
page: pageNum,
size: 10,
isShow: 1 // 只搜索上架商品
}
const result = await pageShopGoods(params)
if (pageNum === 1) {
setGoodsList(result?.list || [])
setTotal(result?.count || 0)
// 保存搜索历史
saveSearchHistory(searchKeywords.trim())
} else {
setGoodsList(prev => [...prev, ...(result?.list || [])])
}
setHasMore((result?.list?.length || 0) >= 10)
setPage(pageNum)
} catch (error) {
console.error('搜索失败:', error)
Taro.showToast({
title: '搜索失败,请重试',
icon: 'none'
})
} finally {
setLoading(false)
}
}
// 加载更多
const loadMore = () => {
if (!loading && hasMore && keywords.trim()) {
handleSearch(keywords, page + 1).then()
}
}
// 点击历史搜索
const onHistoryClick = (keyword: string) => {
setKeywords(keyword)
setPage(1)
handleSearch(keyword, 1)
}
// 清空搜索历史
const clearHistory = () => {
Taro.showModal({
title: '提示',
content: '确定要清空搜索历史吗?',
success: (res) => {
if (res.confirm) {
try {
Taro.removeStorageSync('search_history')
setSearchHistory([])
} catch (error) {
console.error('清空搜索历史失败:', error)
}
}
}
})
}
return (
<View className="search-page pt-3">
<div className={'px-2'}>
<div
style={{
display: 'flex',
alignItems: 'center',
background: '#ffffff',
padding: '0 5px',
borderRadius: '20px',
marginTop: '5px',
}}
>
<Search size={18} className={'ml-2 text-gray-400'}/>
<Input
placeholder="搜索商品"
value={keywords}
onChange={handleKeywords}
onConfirm={() => handleSearch(keywords)}
style={{padding: '9px 8px'}}
/>
<div
className={'flex items-center'}
>
<Button type="success" style={{background: 'linear-gradient(to bottom, #1cd98a, #24ca94)'}}
onClick={() => handleSearch(keywords)}>
</Button>
</div>
</div>
</div>
{/*<SearchBar style={{height: `${statusBarHeight}px`}} shape="round" placeholder="搜索商品" onChange={setKeywords} onSearch={handleSearch}/>*/}
{/* 搜索内容 */}
<View className="search-content">
{/* 搜索历史 */}
{!keywords && searchHistory.length > 0 && (
<View className="search-history">
<View className="history-header">
<View className="text-sm"></View>
<View className={'text-gray-400'} onClick={clearHistory}></View>
</View>
<View className="history-list">
{searchHistory.map((item, index) => (
<View
key={index}
className="history-item"
onClick={() => onHistoryClick(item)}
>
{item}
</View>
))}
</View>
</View>
)}
{/* 搜索结果 */}
{keywords && (
<View className="search-results">
{/* 结果统计 */}
<View className="result-header">
{total}
</View>
{/* 商品列表 */}
{loading && page === 1 ? (
<View className="loading-wrapper">
<Loading>...</Loading>
</View>
) : goodsList.length > 0 ? (
<div className={'py-3'}>
<div className={'flex flex-col justify-between items-center rounded-lg px-2'}>
<InfiniteLoading
hasMore={hasMore}
// @ts-ignore
onLoadMore={loadMore}
loadingText="加载中..."
loadMoreText="没有更多了"
>
{goodsList.map((item) => (
<GoodsItem key={item.goodsId} goods={item}/>
))}
</InfiniteLoading>
</div>
</div>
) : (
<Empty description="暂无相关商品"/>
)}
</View>
)}
</View>
</View>
)
}
export default SearchPage

View File

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

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