remove(feature): 删除经销商申请、用户地址管理和聊天消息功能模块
- 移除经销商申请相关页面配置和业务逻辑代码 - 删除用户地址管理功能的所有配置文件和实现组件 - 清理聊天消息发送功能的相关页面配置和业务代码 - 移除相关的API调用和数据模型引用 - 删除页面导航配置和相关的工具函数引用
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品文章管理',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '管理中心'
|
||||
})
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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('分享成功');
|
||||
},
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '领劵中心',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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;
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '注册成为会员',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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) 取不到就回退到旧的默认ID(1848)
|
||||
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;
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '收益明细'
|
||||
})
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
/* Intentionally empty: styling is done via utility classes. */
|
||||
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '易赊宝分享中心'
|
||||
})
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,7 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '邀请统计',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f5f5f5',
|
||||
enablePullDownRefresh: true
|
||||
})
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '分销订单'
|
||||
})
|
||||
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '易赊宝分享中心',
|
||||
// Enable "Share to friends" and "Share to Moments" (timeline) for this page.
|
||||
enableShareAppMessage: true,
|
||||
enableShareTimeline: true
|
||||
})
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '邀请推广'
|
||||
})
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '提现申请'
|
||||
})
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '购物车',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,5 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '网点',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.index {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #F3F3F3;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ const SmsLogin = () => {
|
||||
const pure = url.split('?')[0]
|
||||
return (
|
||||
pure === '/pages/index/index' ||
|
||||
pure === '/pages/find/find' ||
|
||||
pure === '/pages/user/user'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '配送中心'
|
||||
})
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
export default {
|
||||
navigationBarTitleText: '送水订单',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '水票核销'
|
||||
})
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品分类',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '全部评论',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品详情',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单确认',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单确认',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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: "";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单详情',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品搜索'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '门店中心'
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user