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

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

View File

@@ -1,5 +1,5 @@
import {useEffect, useState} from "react";
import Taro, {useShareAppMessage, useShareTimeline, useDidShow} from '@tarojs/taro';
import Taro, {useShareAppMessage, useDidShow} from '@tarojs/taro';
import {
NavBar,
Checkbox,
@@ -10,10 +10,11 @@ import {
Divider,
ConfigProvider
} from '@nutui/nutui-react-taro';
import {ArrowLeft, Del} from '@nutui/icons-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);
@@ -39,12 +40,6 @@ function Cart() {
nutuiInputnumberButtonBorderRadius: '4px',
}
useShareTimeline(() => {
return {
title: '购物车 - 易赊宝'
};
});
useShareAppMessage(() => {
return {
title: '购物车 - 易赊宝',
@@ -156,6 +151,9 @@ function Cart() {
// 将选中的商品信息存储到本地,供结算页面使用
Taro.setStorageSync('checkout_items', JSON.stringify(selectedCartItems));
// 未登录则引导去注册/登录;登录后回到购物车结算页
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
// 跳转到购物车结算页面
Taro.navigateTo({
url: '/shop/orderConfirmCart/index'
@@ -177,7 +175,6 @@ function Cart() {
<NavBar
fixed={true}
style={{marginTop: `${statusBarHeight}px`}}
left={<ArrowLeft onClick={() => Taro.navigateBack()}/>}
right={
cartItems.length > 0 && (
<Button
@@ -233,7 +230,6 @@ function Cart() {
<NavBar
fixed={true}
style={{marginTop: `${statusBarHeight}px`}}
left={<ArrowLeft onClick={() => Taro.navigateBack()}/>}
right={
cartItems.length > 0 && (
<Button

View File

@@ -0,0 +1,35 @@
import {Image, Cell} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
const ArticleList = (props: any) => {
return (
<>
<View className={'px-3'}>
{props.data.map((item: any, index: number) => {
return (
<Cell
title={
<View>
<View className="text-base font-medium mb-1">{item.title}</View>
{item.comments && (
<Text className="text-sm text-gray-500 leading-relaxed">
{item.comments}
</Text>
)}
</View>
}
extra={
<Image src={item.image} mode={'aspectFit'} lazyLoad={false} width={100} height="100"/>
}
key={index}
onClick={() => Taro.navigateTo({url: '/cms/detail/index?id=' + item.articleId})}
/>
)
})}
</View>
</>
)
}
export default ArticleList

View File

@@ -0,0 +1,59 @@
import {useEffect, useState} from "react";
import {Tabs, Loading} from '@nutui/nutui-react-taro'
import {pageCmsArticle} from "@/api/cms/cmsArticle";
import {CmsArticle} from "@/api/cms/cmsArticle/model";
import ArticleList from "./ArticleList";
const ArticleTabs = (props: any) => {
const [loading, setLoading] = useState<boolean>(true)
const [tab1value, setTab1value] = useState<string | number>('0')
const [list, setList] = useState<CmsArticle[]>([])
const reload = async (value) => {
const {data} = props
pageCmsArticle({
categoryId: data[value].navigationId,
page: 1,
status: 0,
limit: 10
}).then((res) => {
res && setList(res?.list || [])
})
.catch(err => {
console.log(err)
})
.finally(() => {
setTab1value(value)
setLoading(false)
})
}
useEffect(() => {
reload(0).then()
}, []);
if (loading) {
return (
<Loading className={'px-2'}></Loading>
)
}
return (
<>
<Tabs
value={tab1value}
onChange={(value) => {
reload(value).then()
}}
>
{props.data?.map((item, index) => {
return (
<Tabs.TabPane title={item.categoryName} key={index}/>
)
})}
</Tabs>
<ArticleList data={list}/>
</>
)
}
export default ArticleTabs

View File

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

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '文章列表',
navigationBarTextStyle: 'black'
})

View File

View File

@@ -0,0 +1,96 @@
import Taro from '@tarojs/taro'
import {useShareAppMessage} from "@tarojs/taro"
import {useEffect, useState} from "react"
import {useRouter} from '@tarojs/taro'
import {View} from '@tarojs/components'
import {getCmsNavigation, listCmsNavigation} from "@/api/cms/cmsNavigation";
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
import {pageCmsArticle} from "@/api/cms/cmsArticle";
import {CmsArticle} from "@/api/cms/cmsArticle/model";
import ArticleList from './components/ArticleList'
import ArticleTabs from "./components/ArticleTabs";
import './index.scss'
function Category() {
const {params} = useRouter();
const [categoryId, setCategoryId] = useState<number>(0)
const [category, setCategory] = useState<CmsNavigation[]>([])
const [loading, setLoading] = useState<boolean>(true)
const [nav, setNav] = useState<CmsNavigation>()
const [list, setList] = useState<CmsArticle[]>([])
const reload = async () => {
try {
setLoading(true)
// 1.加载远程数据
const id = Number(params.id || 4328)
const nav = await getCmsNavigation(id)
const categoryList = await listCmsNavigation({parentId: id})
const shopGoods = await pageCmsArticle({categoryId: id})
// 2.赋值
setCategoryId(id)
setNav(nav)
setList(shopGoods?.list || [])
setCategory(categoryList)
Taro.setNavigationBarTitle({
title: `${nav?.categoryName}`
})
} catch (error) {
console.error('文章分类加载失败:', error)
} finally {
setLoading(false)
}
};
useEffect(() => {
reload()
}, []);
useShareAppMessage(() => {
return {
title: `${nav?.categoryName}_易赊宝`,
path: `/shop/category/index?id=${categoryId}`,
success: function () {
console.log('分享成功');
},
fail: function () {
console.log('分享失败');
}
};
});
// 骨架屏组件
const ArticleSkeleton = () => (
<View className="px-3">
{[1, 2, 3, 4, 5].map(i => (
<View key={i} className="flex items-center py-4 border-b border-gray-100">
{/* 左侧文字骨架屏 */}
<View className="flex-1 pr-4">
{/* 标题骨架屏 */}
<View className="bg-gray-200 h-5 rounded animate-pulse mb-2" style={{width: '75%'}}/>
{/* 副标题骨架屏 */}
<View className="bg-gray-200 h-4 rounded animate-pulse" style={{width: '50%'}}/>
</View>
{/* 右侧图片骨架屏 */}
<View
className="bg-gray-200 rounded animate-pulse flex-shrink-0"
style={{width: '100px', height: '100px'}}
/>
</View>
))}
</View>
)
if (loading) {
return <ArticleSkeleton />
}
if(category.length > 0){
return <ArticleTabs data={category}/>
}
return <ArticleList data={list}/>
}
export default Category

View File

@@ -1,5 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '网点',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff'
navigationBarTitleText: '发现',
navigationStyle: 'custom',
})

View File

@@ -1,144 +1,4 @@
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: 160rpx;
flex: 0 0 160rpx;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
padding-left: 12rpx;
}
.navArrow {
width: 34rpx;
height: 34rpx;
border-top: 8rpx solid #e60012;
border-right: 8rpx solid #e60012;
border-radius: 4rpx;
transform: rotate(45deg);
margin-right: 8rpx;
}
.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;
background: linear-gradient(to bottom, #e9fff2, #f9fafb);
background-size: 100%;
}

View File

@@ -1,130 +1,82 @@
import {useMemo, useState} from 'react'
import Taro from '@tarojs/taro'
import {Input, Text, View} from '@tarojs/components'
import {Search} from '@nutui/icons-react-taro'
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro';
import {pageCmsArticle} from "@/api/cms/cmsArticle";
import {CmsArticle} from "@/api/cms/cmsArticle/model";
import {NavBar, Cell} from '@nutui/nutui-react-taro';
import {Text} from '@tarojs/components';
import {ArrowRight, ImageRectangle, Coupon, Follow} from '@nutui/icons-react-taro'
import './find.scss'
import navTo from "@/utils/common";
type SiteItem = {
id: string
cityName: string
address: string
phone: string
contact: string
distanceMeter: number
}
const MOCK_SITES: SiteItem[] = [
{
id: '1',
cityName: '北京朝阳区网点',
address: '地安门西大街(南门)',
phone: '15878179339',
contact: '刘先生',
distanceMeter: 100
},
{
id: '2',
cityName: '兰州某某区网点',
address: '地安门西大街(南门)',
phone: '15878179339',
contact: '黄先生',
distanceMeter: 150
},
{
id: '3',
cityName: '合肥市某某区网点',
address: '地安门西大街(南门)',
phone: '15878179339',
contact: '黄先生',
distanceMeter: 250
},
{
id: '4',
cityName: '南宁市某某区网点',
address: '广西壮族自治区南宁市良庆区五象新区五象大道403号富雅国际金融中心G1栋高层6006',
phone: '15878179339',
contact: '柳先生',
distanceMeter: 1250
}
]
/**
* 文章终极列表
* @constructor
*/
const Find = () => {
const [keyword, setKeyword] = useState<string>('')
const [statusBarHeight, setStatusBarHeight] = useState<number>()
const [loading, setLoading] = useState<boolean>(false)
const [list, setList] = useState<CmsArticle[]>()
const filtered = useMemo(() => {
const key = keyword.trim()
if (!key) return MOCK_SITES
return MOCK_SITES.filter((it) => it.cityName.includes(key))
}, [keyword])
const onNavigate = (item: SiteItem) => {
Taro.showToast({title: `导航至:${item.cityName}(示例)`, icon: 'none'})
const reload = async () => {
setLoading(true)
const article = await pageCmsArticle({categoryId: 4289, status: 0})
if (article) {
setList(article?.list)
setLoading(false)
}
}
const onSearch = () => {
Taro.showToast({title: '查询(示例)', icon: 'none'})
}
useEffect(() => {
Taro.getSystemInfo({
success: (res) => {
setStatusBarHeight(res.statusBarHeight)
},
})
reload().then(() => {
console.log('初始化完成')
})
}, [])
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}
<>
{loading && (<div></div>)}
<NavBar
fixed={false}
style={{marginTop: `${statusBarHeight}px`, backgroundColor: 'transparent'}}
onBackClick={() => {
}}
>
<span></span>
</NavBar>
{list && (
<>
<Cell title={
<div style={{display: 'inline-flex', alignItems: 'center'}}>
<ImageRectangle size={18}/>
<Text className={'pl-3'} style={{fontSize: '16px'}}></Text>
</div>
} extra={<ArrowRight color="#cccccc" size={18}/>}/>
<Cell
title={
<div style={{display: 'inline-flex', alignItems: 'center'}}>
<Coupon size={18}/>
<span className={'pl-3'} style={{fontSize: '16px'}}></span>
</div>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/shop/shopArticle/index', true)
}}
/>
<View className='searchIconWrap' onClick={onSearch}>
<Search size={18} color='#b51616' />
</View>
</View>
</View>
<View className='siteList'>
{filtered.map((item) => (
<View key={item.id} className='siteCard'>
<View className='siteCardInner'>
<View className='siteInfo'>
<View className='siteRow siteRowTop'>
<Text className='siteLabel'></Text>
<Text className='siteValue siteValueStrong'>{item.cityName}</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'>{item.phone}</Text>
</View>
<View className='siteRow'>
<Text className='siteLabel'></Text>
<Text className='siteValue'>{item.contact}</Text>
</View>
</View>
<View className='siteSide' onClick={() => onNavigate(item)}>
<View className='navArrow' />
<Text className='distanceText'>{item.distanceMeter}</Text>
</View>
</View>
</View>
))}
{filtered.length === 0 && (
<View className='emptyWrap'>
<Text className='emptyText'></Text>
</View>
)}
</View>
<View className='bottomSafe' />
</View>
<Cell title={
<div style={{display: 'inline-flex', alignItems: 'center'}}>
<Follow size={18}/>
<span className={'pl-3'} style={{fontSize: '16px'}}></span>
</div>
} extra={<ArrowRight color="#cccccc" size={18}/>}/>
</>
)}
</>
)
}
export default Find

View File

@@ -1,4 +0,0 @@
// 订单页面样式
.order-page {
// 订单相关样式
}

View File

@@ -3,16 +3,16 @@ 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 {useEffect} from "react";
import {useDealerUser} from "@/hooks/useDealerUser";
import {useThemeStyles} from "@/hooks/useTheme";
import { useConfig } from "@/hooks/useConfig"; // 使用新的自定义Hook
import Taro from '@tarojs/taro'
const UserCell = () => {
const IsDealer = () => {
const themeStyles = useThemeStyles();
const { config } = useConfig(); // 使用新的Hook
const {isSuperAdmin} = useUser();
const {dealerUser} = useDealerUser()
useEffect(() => {
}, [])
const {dealerUser, loading: dealerLoading} = useDealerUser()
/**
* 管理中心
@@ -20,12 +20,10 @@ const UserCell = () => {
if (isSuperAdmin()) {
return (
<>
<View className={'px-4'}>
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #e53e3e, #c53030)',
}}
style={themeStyles.primaryBackground}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Setting className={'text-white '} size={16}/>
@@ -46,17 +44,15 @@ const UserCell = () => {
if (dealerUser) {
return (
<>
<View className={'px-4'}>
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
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'}></Text>
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '易赊宝分享中心'}</Text>
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
</View>
}
@@ -71,6 +67,30 @@ const UserCell = () => {
/**
* 普通用户
*/
return null
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 UserCell
export default IsDealer

View File

@@ -1,26 +1,101 @@
import {Button} from '@nutui/nutui-react-taro'
import {Avatar, Tag} from '@nutui/nutui-react-taro'
import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components'
import {Scan} from '@nutui/icons-react-taro';
import {getWxOpenId} from '@/api/layout';
import {getUserInfo, getWxOpenId} from '@/api/layout';
import Taro from '@tarojs/taro';
import {useEffect} from "react";
import {useEffect, useState, forwardRef, useImperativeHandle} from "react";
import {User} from "@/api/system/user/model";
import navTo from "@/utils/common";
import {TenantId} from "@/config/app";
import {useUser} from "@/hooks/useUser";
import {useUserData} from "@/hooks/useUserData";
import {checkAndHandleInviteRelation, getStoredInviteParams, hasPendingInvite} from "@/utils/invite";
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'
function UserCard() {
const {
user,
isAdmin,
isLoggedIn,
loginUser,
fetchUserInfo,
getDisplayName,
getRoleName
} = useUser();
const UserCard = forwardRef<any, any>((_, ref) => {
const {data, refresh} = useUserData()
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 = (() => {
const v: any = (userInfo as any)?.isAdmin
return v === true || v === 1 || v === '1'
})()
const getDisplayName = () => {
if (!userInfo) return IsLogin ? '用户' : '点击登录'
return userInfo.nickname || (userInfo as any).realName || (userInfo as any).username || (IsLogin ? '用户' : '点击登录')
}
// 角色名称:优先取用户 roles 数组的第一个角色名称
const getRoleName = () => {
return userInfo?.roles?.[0]?.roleName || userInfo?.roleName || '注册用户'
}
// 下拉刷新
const reloadStats = async (showToast = false) => {
await refresh()
reloadTicketTotal()
if (showToast) {
Taro.showToast({
title: '刷新成功',
icon: 'success'
})
}
}
const syncUserToStorage = (u: User) => {
// Keep storage up-to-date for other places that read user info synchronously.
Taro.setStorageSync('User', u)
if (u?.userId) Taro.setStorageSync('UserId', u.userId)
if (u?.nickname) Taro.setStorageSync('WxNickName', u.nickname)
}
const reloadUserInfo = async () => {
try {
const u = await getUserInfo()
if (u) {
setUserInfo(u)
setIsLogin(true)
syncUserToStorage(u)
// Refresh this hook instance's state from storage (defensive).
await loadUserFromStorage()
// 获取openId不阻塞 UI 刷新)
if (!u.openid) {
Taro.login({
success: (res) => {
getWxOpenId({code: res.code}).catch(() => {})
}
})
}
}
} catch (e) {
// Not logged in / token expired: keep UI in "not login" state.
// Other error handling is done in request interceptor / callers.
}
}
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
handleRefresh: async () => {
await reloadUserInfo()
await reloadStats(true)
},
reloadStats,
reloadUserInfo
}))
useEffect(() => {
// 独立于用户信息授权:只要有登录 token就可以拉取水票总数
reloadTicketTotal()
// Taro.getSetting获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
success: (res) => {
@@ -37,27 +112,43 @@ function UserCard() {
});
}, []);
const reload = async () => {
// 如果已登录,获取最新用户信息
if (isLoggedIn) {
try {
const data = await fetchUserInfo();
if (data) {
// 获取openId
if (!data.openid) {
Taro.login({
success: (res) => {
getWxOpenId({code: res.code}).then(() => {
})
}
})
}
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
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) => {
const avatar = res.userInfo.avatarUrl;
setUserInfo({
avatar,
nickname: res.userInfo.nickName,
sexName: res.userInfo.gender == 1 ? '男' : '女'
})
reloadUserInfo()
.then(() => {
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
refresh().then()
reloadTicketTotal()
})
.catch(() => {
console.log('未登录')
})
}
});
};
const showAuthModal = () => {
@@ -75,7 +166,6 @@ function UserCard() {
});
};
const openSetting = () => {
// Taro.openSetting调起客户端小程序设置界面返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
Taro.openSetting({
@@ -97,19 +187,28 @@ function UserCard() {
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
const {code, encryptedData, iv} = detail
// 判断用户是否已登录
if(IsLogin){
return navTo(`/user/profile/profile`)
}
// 获取存储的邀请参数
const inviteParams = getStoredInviteParams()
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
Taro.login({
success: function (loginRes) {
success: function () {
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,
refereeId: refereeId, // 使用解析出的推荐人ID
sceneType: 'save_referee',
tenantId: TenantId
},
@@ -127,21 +226,19 @@ function UserCard() {
return false;
}
// 登录成功
const token = res.data.data.access_token;
const userData = res.data.data.user;
saveStorageByLoginUser(res.data.data.access_token, res.data.data.user)
setUserInfo(res.data.data.user)
setIsLogin(true)
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
refresh().then()
reloadTicketTotal()
// 使用useUser Hook的loginUser方法更新状态
loginUser(token, userData);
// 显示登录成功提示
Taro.showToast({
title: '登录成功',
icon: 'success',
duration: 1500
})
// 刷新页面数据
reload();
// 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定
if (hasPendingInvite()) {
checkAndHandleInviteRelation().catch((e) => {
console.error('个人中心登录后处理邀请关系失败:', e)
})
}
}
})
} else {
@@ -152,55 +249,116 @@ function UserCard() {
}
return (
<View className={'header-bg pt-20'}>
<View className={'p-4'}>
<View
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',
minHeight: '120px',
// borderRadius: '22px 22px 0 0',
}}
>
<View className={'user-card-header flex w-full justify-between items-center pt-4'}>
<View className={'flex items-center mx-4'}>
{
isLoggedIn ? (
<Avatar size="large" src={user?.avatar} shape="round"/>
) : (
<Button className={'text-black'} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Avatar size="large" src={user?.avatar} shape="round"/>
</Button>
)
}
<View className={'user-info flex flex-col px-2'}>
<View className={'py-1 text-black font-bold max-w-28'}>{getDisplayName()}</View>
{isLoggedIn ? (
<View className={'grade text-xs py-1'}>
<Tag type="success" round>
<Text className={'p-1'}>
{getRoleName()}
</Text>
</Tag>
<View className={'pt-14'}>
{/* 使用相对定位容器,让个人资料图片可以绝对定位在右上角 */}
<View className="relative z-20">
<View
className={'user-card w-full flex flex-col justify-around rounded-xl'}
>
<View className={'user-card-header flex w-full justify-between items-center pt-4'}>
<View className={'flex items-center mx-4'}>
{IsLogin && (
<View style={{color: '#ffffff', height: 'auto'}} onClick={() => navTo(`/user/profile/profile`)}>
<View className={'flex items-center gap-2'}>
<Avatar
size={'large'}
src={userInfo?.avatar || ''}
/>
<View className={'flex flex-col'}>
<Text style={{color: '#ffffff'}}>{getDisplayName() || '点击登录'}</Text>
{getRootDomain() && (
<View><Tag type="success">{getRoleName()}</Tag></View>
)}
</View>
</View>
</View>
) : ''}
)}
{!IsLogin && (
<Button style={{color: '#ffffff', height: 'auto'}} open-type="getPhoneNumber"
onGetPhoneNumber={handleGetPhoneNumber}>
<View className={'flex items-center gap-2'}>
<Avatar
size={'large'}
src={userInfo?.avatar || ''}
/>
<View className={'flex flex-col'}>
<Text style={{color: '#ffffff'}}>{getDisplayName() || '点击登录'}</Text>
</View>
</View>
</Button>
)}
</View>
{/*统一扫码入口 - 仅管理员可见*/}
{canShowScanButton && (
<Space style={{
marginTop: '30px',
marginRight: '10px'
}}>
<UnifiedQRButton
text="扫一扫"
size="small"
onSuccess={(result) => {
console.log('统一扫码成功:', result);
// 根据扫码类型给出不同的提示
if (result.type === 'verification') {
const businessType = result?.data?.businessType;
if (businessType === 'gift' && result?.data?.gift) {
const gift = result.data.gift;
Taro.showModal({
title: '核销成功',
content: `已成功核销:${gift.goodsName || gift.name || '礼品'},面值¥${gift.faceValue}`
});
return;
}
if (businessType === 'ticket' && result?.data?.ticket) {
const ticket = result.data.ticket;
const qty = result.data.qty || 1;
Taro.showModal({
title: '核销成功',
content: `已成功核销:${ticket.templateName || '水票'},本次使用${qty}次,剩余可用${ticket.availableQty ?? 0}`
});
return;
}
Taro.showModal({
title: '核销成功',
content: '已成功核销'
});
}
}}
onError={(error) => {
console.error('统一扫码失败:', error);
}}
/>
</Space>
)}
</View>
<View className={'gap-2 flex items-center'}>
{isAdmin() && <Scan className={'text-gray-900'} size={24} onClick={() => navTo('/user/store/verification', true)} />}
<View className={'mr-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
onClick={() => navTo('/user/profile/profile', true)}>
{'个人资料'}
<View className={'py-2'}>
<View className={'flex justify-around mt-1'}>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/ticket/index', true)}>
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}></Text>
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{ticketTotal}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/coupon/index', true)}>
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}></Text>
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/wallet/wallet', true)}>
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}></Text>
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.balance || '0.00'}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}>
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}></Text>
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.points || 0}</Text>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
)
}
})
export default UserCard;

View File

@@ -2,11 +2,11 @@ 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, LogisticsError, Location} from '@nutui/icons-react-taro'
import {ArrowRight, ShieldCheck, LogisticsError, Location, Tips, Ask} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
const UserCell = () => {
const {logoutUser} = useUser();
const {logoutUser, isCertified} = useUser();
const onLogout = () => {
Taro.showModal({
@@ -26,7 +26,7 @@ const UserCell = () => {
return (
<>
<View className={'px-4'}>
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 20 }}>
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
@@ -55,7 +55,7 @@ const UserCell = () => {
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Location size={16}/>
<Text className={'pl-3 text-sm'}></Text>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
@@ -64,51 +64,51 @@ const UserCell = () => {
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
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'}}>
@@ -122,13 +122,13 @@ const UserCell = () => {
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={() => navTo('/user/theme/index', true)}
/>
<Cell
className="nutui-cell-clickable"
title="退出登录"

View File

@@ -0,0 +1,216 @@
import {Grid, ConfigProvider} from '@nutui/nutui-react-taro'
import navTo from "@/utils/common";
import Taro from '@tarojs/taro'
import {View, Button} from '@tarojs/components'
import {
ShieldCheck,
Location,
Tips,
Ask,
// Dongdong,
People,
// AfterSaleService,
Logout,
Shop,
Jdl,
Service
} from '@nutui/icons-react-taro'
import {useUser} from "@/hooks/useUser";
const UserCell = () => {
const {logoutUser, hasRole} = 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="bg-white mx-4 mt-4 rounded-xl">
<View className="font-semibold text-gray-800 pt-4 pl-4"></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}
>
{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>
)}
{(hasRole('staff') || hasRole('admin')) && (
<Grid.Item text="门店订单" onClick={() => navTo('/user/store/orders/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">
<Shop color="#f59e0b" size="20"/>
</View>
</View>
</Grid.Item>
)}
<Grid.Item text="配送地址" onClick={() => navTo('/user/address/index', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Location color="#3b82f6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Ask className={'text-cyan-500'} size="20"/>
</View>
</View>
</Grid.Item>
{/* 修改联系我们为微信客服 */}
<Grid.Item text="联系我们">
<Button
open-type="contact"
className="w-full h-full flex flex-col items-center justify-center p-0 bg-transparent"
hover-class="none"
style={{border: 'none'}}
>
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Service color="#67C23A" size="20"/>
</View>
</Button>
</Grid.Item>
<Grid.Item text={'实名认证'} onClick={() => navTo('/user/userVerify/index', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<ShieldCheck color="#10b981" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'推广邀请'} onClick={() => navTo('/dealer/team/index', true)}>
<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={() => navTo('/dealer/qrcode/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">*/}
{/* <Dongdong color="#f59e0b" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/*</Grid.Item>*/}
{/*<Grid.Item text={'管理中心'} onClick={() => navTo('/admin/index', true)}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-red-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <AfterSaleService className={'text-red-500'} size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/*</Grid.Item>*/}
<Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Tips className={'text-amber-500'} size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'安全退出'} onClick={onLogout}>
<View className="text-center">
<View className="w-12 h-12 bg-pink-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Logout className={'text-pink-500'} size="20"/>
</View>
</View>
</Grid.Item>
</Grid>
</ConfigProvider>
</View>
{/*<View className="bg-white mx-4 mt-4 rounded-xl">*/}
{/* <View className="font-semibold text-gray-800 pt-4 pl-4">账号管理</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={() => navTo('/user/profile/profile', 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">*/}
{/* <ShoppingAdd color="#3b82f6" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text="切换主题" onClick={() => navTo('/user/theme/index', true)}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Location color="#3b82f6" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Tips className={'text-amber-500'} size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={'安全退出'} onClick={onLogout}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-pink-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Logout className={'text-pink-500'} size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* </Grid>*/}
{/* </ConfigProvider>*/}
{/*</View>*/}
</>
)
}
export default UserCell

View File

@@ -14,7 +14,7 @@ function UserOrder() {
return (
<>
<View className={'px-4 pb-2'}>
<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={{
@@ -26,7 +26,7 @@ function UserOrder() {
}}
>
<View className={'title-bar flex justify-between pt-2'}>
<Text className={'title font-medium px-4'}></Text>
<Text className={'title font-medium px-4'}></Text>
<View
className={'more flex items-center px-2'}
onClick={() => navTo('/user/order/order', true)}

View File

@@ -1,10 +1,9 @@
.header-bg{
/* Replace top background image with a gradient + subtle decoration */
background:
radial-gradient(circle at 18% 22%, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0) 55%),
radial-gradient(circle at 82% 30%, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0) 52%),
radial-gradient(circle at 60% 8%, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0) 45%),
linear-gradient(180deg, #ff0000 0%, #ffeee9 45%, #f9fafb 100%);
background-repeat: no-repeat;
background-size: cover;
//background: url('https://oss.wsdns.cn/20250621/edb5d4da976b4d97ba185cb7077d2858.jpg') no-repeat top center;
background: linear-gradient(to bottom, #03605c, #18ae4f);
background-size: 100%;
}
.my-bg{
background: url('https://oss.wsdns.cn/20250913/5ae575a50dbb4ccaab086c3679c5e2c3.png') no-repeat top center;
}

View File

@@ -1,41 +1,71 @@
import {useEffect, useRef, useState} from 'react'
import {PullToRefresh} from '@nutui/nutui-react-taro'
import UserCard from "./components/UserCard";
import UserOrder from "./components/UserOrder";
import UserCell from "./components/UserCell";
import UserFooter from "./components/UserFooter";
import {useUser} from "@/hooks/useUser";
import {View} from '@tarojs/components';
import './user.scss'
import IsDealer from "./components/IsDealer";
import {useThemeStyles} from "@/hooks/useTheme";
import UserGrid from "@/pages/user/components/UserGrid";
import { useDidShow } from '@tarojs/taro'
function User() {
const {
isAdmin
} = useUser();
/**
* 门店核销管理
*/
if (isAdmin()) {
return <>
<div className={'w-full'}>
<UserCard/>
<UserOrder/>
<IsDealer/>
<UserCell/>
<UserFooter/>
</div>
</>
const userCardRef = useRef<any>()
const themeStyles = useThemeStyles();
// TabBar 页在小程序里通常不会销毁;从“注册/申请”页返回时需要触发子组件重新初始化/拉取最新状态。
const [dealerViewKey, setDealerViewKey] = useState(0)
// 下拉刷新处理
const handleRefresh = async () => {
if (userCardRef.current?.handleRefresh) {
await userCardRef.current.handleRefresh()
}
setDealerViewKey(v => v + 1)
}
useEffect(() => {
}, []);
// 每次进入/切回个人中心都刷新一次统计(包含水票数量)
useDidShow(() => {
userCardRef.current?.reloadStats?.()
// 个人资料(头像/昵称)可能在其它页面被修改,这里确保返回时立刻刷新
userCardRef.current?.reloadUserInfo?.()
setDealerViewKey(v => v + 1)
})
return (
<>
<div className={'w-full'}>
<UserCard/>
{/*<UserOrder/>*/}
<IsDealer/>
<UserCell/>
<UserFooter/>
</div>
</>
<PullToRefresh
onRefresh={handleRefresh}
headHeight={60}
>
{/* 装饰性背景 */}
<View className={'h-64 w-full fixed top-0 z-0'} 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>
<UserCard ref={userCardRef}/>
<UserOrder/>
<IsDealer key={dealerViewKey}/>
<UserGrid/>
<UserFooter/>
</PullToRefresh>
)
}