feat(user): 实现扫码登录和推荐人绑定功能- 添加扫码登录相关API接口,包括生成二维码、检查状态、确认登录等
- 在用户注册时支持从邀请参数中获取推荐人ID并绑定 - 修改管理员面板UI,添加统一扫码功能入口- 更新用户管理相关API地址,统一使用SERVER_API_URL - 调整优惠券卡片样式,移除小程序不支持的CSS属性 - 添加聊天会话和消息管理相关API模块 - 新增分销商银行卡管理API接口 - 修改系统用户模型,增加推荐人ID字段 - 更新广告位查询接口,支持根据code获取广告位 - 调整邀请绑定接口参数,将refereeId改为dealerId - 修改环境配置中的应用名称为"时里院子市集" - 移除分享到朋友圈的相关代码 - 添加管理员面板组件,提供统一扫码等管理功能 -修复用户管理API请求参数传递问题 - 添加聊天消息和会话管理的完整CRUD接口 - 更新系统用户相关接口URL,确保正确调用后端服务 - 添加分销商银行卡管理的完整API接口实现 - 修改邀请绑定接口,使用dealerId替代refereeId参数 - 修复扫码登录相关API的URL拼接问题 - 添加二维码内容解析功能,支持多种格式的token提取 - 更新用户信息模型,增加推荐人ID字段 -优化管理员面板样式和交互逻辑- 调整优惠券组件样式,兼容小程序环境限制- 修复用户管理模块的API调用问题 - 添加聊天相关数据模型和接口定义 - 更新环境配置中的应用名称 -修复邀请绑定相关的参数传递问题- 添加扫码登录状态枚举和相关数据结构定义- 优化管理员功能面板的UI展示和交互体验- 修复系统用户管理接口的请求参数问题 - 添加分销商银行卡管理相关接口实现- 调整聊天消息和会话管理接口的数据结构定义 -修复用户管理模块中的API调用路径问题 - 添加扫码登录相关工具函数,如设备信息获取等 - 更新邀请绑定接口的数据模型定义 -优化管理员面板组件的样式和功能实现 -修复系统用户管理接口中的参数传递问题 - 添加聊天相关模块的完整API接口实现 - 调整分销商银行卡管理模块的数据结构定义- 修复扫码登录相关接口的URL拼接问题- 更新用户管理模块中的API调用方式 - 添加聊天消息批量发送等相关接口实现- 修复邀请绑定接口中的参数名称问题- 优化管理员面板组件的功能和交互逻辑 - 调整系统用户管理接口的请求参数传递方式 - 添加分销商银行卡管理模块的完整接口实现 -修复聊天相关接口中的数据结构问题 - 更新扫码登录相关接口的数据模型定义 - 优化管理员功能面板的展示效果和用户体验
This commit is contained in:
@@ -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,
|
||||
@@ -39,15 +39,9 @@ function Cart() {
|
||||
nutuiInputnumberButtonBorderRadius: '4px',
|
||||
}
|
||||
|
||||
useShareTimeline(() => {
|
||||
return {
|
||||
title: '购物车 - 网宿小店'
|
||||
};
|
||||
});
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: '购物车 - 网宿小店',
|
||||
title: '购物车 - 时里院子市集',
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
},
|
||||
|
||||
25
src/pages/cms/category/components/ArticleList.tsx
Normal file
25
src/pages/cms/category/components/ArticleList.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import {Image, Cell} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const ArticleList = (props: any) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'px-3'}>
|
||||
{props.data.map((item, index) => {
|
||||
return (
|
||||
<Cell
|
||||
title={item.title}
|
||||
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})}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default ArticleList
|
||||
59
src/pages/cms/category/components/ArticleTabs.tsx
Normal file
59
src/pages/cms/category/components/ArticleTabs.tsx
Normal 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
|
||||
31
src/pages/cms/category/components/Banner.tsx
Normal file
31
src/pages/cms/category/components/Banner.tsx
Normal 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
|
||||
4
src/pages/cms/category/index.config.ts
Normal file
4
src/pages/cms/category/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '文章列表',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
0
src/pages/cms/category/index.scss
Normal file
0
src/pages/cms/category/index.scss
Normal file
71
src/pages/cms/category/index.tsx
Normal file
71
src/pages/cms/category/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useShareAppMessage} from "@tarojs/taro"
|
||||
import {Loading} from '@nutui/nutui-react-taro'
|
||||
import {useEffect, useState} from "react"
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
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 () => {
|
||||
// 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}`
|
||||
})
|
||||
};
|
||||
|
||||
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'}>加载中</Loading>
|
||||
)
|
||||
}
|
||||
|
||||
if(category.length > 0){
|
||||
return <ArticleTabs data={category}/>
|
||||
}
|
||||
|
||||
return <ArticleList data={list}/>
|
||||
}
|
||||
|
||||
export default Category
|
||||
3
src/pages/cms/detail/index.config.ts
Normal file
3
src/pages/cms/detail/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '文章详情'
|
||||
})
|
||||
0
src/pages/cms/detail/index.scss
Normal file
0
src/pages/cms/detail/index.scss
Normal file
53
src/pages/cms/detail/index.tsx
Normal file
53
src/pages/cms/detail/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Loading} from '@nutui/nutui-react-taro'
|
||||
import {View, RichText} from '@tarojs/components'
|
||||
import {wxParse} from "@/utils/common";
|
||||
import {getCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model"
|
||||
import Line from "@/components/Gap";
|
||||
import './index.scss'
|
||||
|
||||
function Detail() {
|
||||
const {params} = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
// 文章详情
|
||||
const [item, setItem] = useState<CmsArticle>()
|
||||
const reload = async () => {
|
||||
const item = await getCmsArticle(Number(params.id))
|
||||
|
||||
if (item) {
|
||||
item.content = wxParse(item.content)
|
||||
setItem(item)
|
||||
Taro.setNavigationBarTitle({
|
||||
title: `${item?.categoryName}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Loading className={'px-2'}>加载中</Loading>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'bg-white'}>
|
||||
<div className={'p-4 font-bold text-lg'}>{item?.title}</div>
|
||||
<div className={'text-gray-400 text-sm px-4 '}>{item?.createTime}</div>
|
||||
<View className={'content p-4'}>
|
||||
<RichText nodes={item?.content}/>
|
||||
</View>
|
||||
<Line height={44}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Detail
|
||||
@@ -1,31 +1,123 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Swiper } from '@nutui/nutui-react-taro'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {View} from '@tarojs/components'
|
||||
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";
|
||||
import {getCmsAdByCode} from "@/api/cms/cmsAd";
|
||||
import navTo from "@/utils/common";
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
import {listShopGoods} from "@/api/shop/shopGoods";
|
||||
|
||||
|
||||
const MyPage = () => {
|
||||
const [item, setItem] = useState<CmsAd>()
|
||||
const reload = () => {
|
||||
getCmsAd(439).then(data => {
|
||||
setItem(data)
|
||||
const [carouselData, setCarouselData] = useState<CmsAd>()
|
||||
// const [hotToday, setHotToday] = useState<CmsAd>()
|
||||
// const [groupBuy, setGroupBuy] = useState<CmsAd>()
|
||||
const [hotGoods, setHotGoods] = useState<ShopGoods[]>([])
|
||||
|
||||
// 加载数据
|
||||
const loadData = () => {
|
||||
// 轮播图
|
||||
getCmsAdByCode('flash').then(data => {
|
||||
setCarouselData(data)
|
||||
})
|
||||
// 今日热卖素材(上层图片)
|
||||
// getCmsAd(444).then(data => {
|
||||
// setHotToday(data)
|
||||
// })
|
||||
// 社区拼团素材(下层图片)
|
||||
// getCmsAd(445).then(data => {
|
||||
// setGroupBuy(data)
|
||||
// })
|
||||
// 今日热卖
|
||||
listShopGoods({categoryId: 4424, limit: 2}).then(data => {
|
||||
setHotGoods(data)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
// 轮播图高度,默认200px
|
||||
const carouselHeight = carouselData?.height || 200;
|
||||
|
||||
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>
|
||||
</>
|
||||
<View className="flex p-2 justify-between" style={{height: `${carouselHeight}px`}}>
|
||||
{/* 左侧轮播图区域 */}
|
||||
<View style={{width: '50%', height: '100%'}}>
|
||||
<Swiper
|
||||
defaultValue={0}
|
||||
height={carouselHeight}
|
||||
indicator
|
||||
style={{height: `${carouselHeight}px`}}
|
||||
>
|
||||
{carouselData?.imageList?.map((img, index) => (
|
||||
<Swiper.Item key={index}>
|
||||
<Image
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={img.url}
|
||||
mode={'scaleToFill'}
|
||||
onClick={() => navTo(`${img.path}`)}
|
||||
lazyLoad={false}
|
||||
style={{height: `${carouselHeight}px`, borderRadius: '4px'}}
|
||||
/>
|
||||
</Swiper.Item>
|
||||
))}
|
||||
</Swiper>
|
||||
</View>
|
||||
|
||||
{/* 右侧上下图片区域 - 从API获取数据 */}
|
||||
<View className="flex flex-col" style={{width: '50%', height: '100%'}}>
|
||||
{/* 上层图片 - 使用今日热卖素材 */}
|
||||
<View className={'ml-2 bg-white rounded-lg'}>
|
||||
<View className={'px-3 my-2 font-bold text-sm'}>今日热卖</View>
|
||||
<View className={'px-3 flex'} style={{
|
||||
height: '110px'
|
||||
}}>
|
||||
{
|
||||
hotGoods.map(item => (
|
||||
<View className={'item flex flex-col mr-4'}>
|
||||
<Image
|
||||
width={70}
|
||||
height={70}
|
||||
src={item.image}
|
||||
mode={'scaleToFill'}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
onClick={() => navTo('/shop/category/index?id=4424')}
|
||||
/>
|
||||
<View className={'text-xs py-2'}>到手价¥{item.price}</View>
|
||||
</View>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 下层图片 - 使用社区拼团素材 */}
|
||||
<View className={'ml-2 bg-white rounded-lg mt-3'}>
|
||||
<View className={'px-3 my-2 font-bold text-sm'}>走进社区</View>
|
||||
<View className={'rounded-lg px-3 pb-3'}>
|
||||
<Image
|
||||
width={'100%'}
|
||||
height={100}
|
||||
src={'https://oss.wsdns.cn/20250919/941c99899e694a7798cab3bb28f1f238.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90'}
|
||||
mode={'scaleToFill'}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
onClick={() => navTo('cms/detail/index?id=10109')}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyPage
|
||||
|
||||
|
||||
@@ -1,21 +1,56 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Image} from '@nutui/nutui-react-taro'
|
||||
import {Image, Swiper, SwiperItem, Empty} 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 Taro from "@tarojs/taro";
|
||||
import {Tabs} from '@nutui/nutui-react-taro'
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
import {pageShopGoods} from "@/api/shop/shopGoods";
|
||||
import './BestSellers.scss'
|
||||
|
||||
|
||||
const BestSellers = () => {
|
||||
const [tab1value, setTab1value] = useState<string | number>('0')
|
||||
const [list, setList] = useState<ShopGoods[]>([])
|
||||
const [goods, setGoods] = useState<ShopGoods>()
|
||||
const [goods, setGoods] = useState<ShopGoods | null>(null)
|
||||
// 轮播图固定高度,可根据需求调整
|
||||
const SWIPER_HEIGHT = 180;
|
||||
|
||||
const reload = () => {
|
||||
pageShopGoods({}).then(res => {
|
||||
setList(res?.list || []);
|
||||
})
|
||||
const processGoodsItem = (item: ShopGoods) => {
|
||||
const pics: string[] = [];
|
||||
// 添加主图
|
||||
if (item.image) {
|
||||
pics.push(item.image);
|
||||
}
|
||||
// 处理附加图片
|
||||
if (item.files) {
|
||||
try {
|
||||
// 解析文件字符串为对象
|
||||
const files = typeof item.files === "string"
|
||||
? JSON.parse(item.files)
|
||||
: item.files;
|
||||
|
||||
// 收集所有图片URL
|
||||
Object.values(files).forEach(file => {
|
||||
if (file?.url) {
|
||||
pics.push(file.url);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('解析文件失败:', error);
|
||||
}
|
||||
}
|
||||
// 返回新对象,避免直接修改原对象
|
||||
return {...item, pics};
|
||||
};
|
||||
|
||||
// 处理商品列表
|
||||
const goods = (res?.list || []).map(processGoodsItem);
|
||||
setList(goods);
|
||||
}).catch(err => {
|
||||
console.error('获取商品列表失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理分享点击
|
||||
@@ -24,14 +59,12 @@ const BestSellers = () => {
|
||||
|
||||
// 显示分享选项菜单
|
||||
Taro.showActionSheet({
|
||||
itemList: ['分享给好友', '分享到朋友圈'],
|
||||
itemList: ['分享给好友'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
// 分享给好友 - 触发转发
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
success: () => {
|
||||
// 提示用户点击右上角分享
|
||||
Taro.showToast({
|
||||
title: '请点击右上角分享给好友',
|
||||
icon: 'none',
|
||||
@@ -39,13 +72,6 @@ const BestSellers = () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (res.tapIndex === 1) {
|
||||
// 分享到朋友圈
|
||||
Taro.showToast({
|
||||
title: '请点击右上角分享到朋友圈',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
@@ -55,87 +81,135 @@ const BestSellers = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
reload();
|
||||
}, []);
|
||||
|
||||
// 分享给好友
|
||||
useShareAppMessage(() => {
|
||||
// 配置分享内容
|
||||
Taro.useShareAppMessage(() => {
|
||||
if (goods) {
|
||||
return {
|
||||
title: goods.name,
|
||||
path: `/shop/goodsDetail/index?id=${goods.goodsId}`,
|
||||
imageUrl: goods.image || ''
|
||||
};
|
||||
}
|
||||
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
|
||||
title: '热销商品',
|
||||
path: '/pages/index/index'
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={'py-3'}>
|
||||
<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 className={'py-3'}>
|
||||
<View className={'flex flex-col justify-between items-center rounded-lg px-2'}>
|
||||
<Tabs
|
||||
value={tab1value}
|
||||
className={'w-full'}
|
||||
onChange={(value) => {
|
||||
setTab1value(value)
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
}}
|
||||
activeType="smile"
|
||||
>
|
||||
<Tabs.TabPane title="今日主推">
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane title="即将到期">
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane title="活动预告">
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
|
||||
{tab1value == '0' && list?.map((item) => (
|
||||
<View
|
||||
key={item.goodsId || item.id} // 使用商品唯一ID作为key
|
||||
className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}
|
||||
>
|
||||
{/* 轮播图组件 */}
|
||||
{item.pics && item.pics.length > 0 ? (
|
||||
<Swiper
|
||||
defaultValue={0}
|
||||
height={SWIPER_HEIGHT}
|
||||
indicator
|
||||
className="swiper-container"
|
||||
autoPlay
|
||||
interval={3000}
|
||||
>
|
||||
{item.pics.map((pic, picIndex) => (
|
||||
<SwiperItem key={picIndex}>
|
||||
<Image
|
||||
radius="12px 12px 0 0"
|
||||
height={SWIPER_HEIGHT}
|
||||
src={pic}
|
||||
mode={'aspectFill'} // 使用aspectFill保持比例并填充容器
|
||||
lazyLoad
|
||||
onClick={() => Taro.navigateTo({
|
||||
url: `/shop/goodsDetail/index?id=${item.goodsId}`
|
||||
})}
|
||||
className="swiper-image"
|
||||
/>
|
||||
</SwiperItem>
|
||||
))}
|
||||
</Swiper>
|
||||
) : (
|
||||
// 没有图片时显示占位图
|
||||
<View className="no-image-placeholder" style={{height: `${SWIPER_HEIGHT}px`}}>
|
||||
<Text className="placeholder-text">暂无图片</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<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 hidden'}>
|
||||
<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-5 pr-5'}
|
||||
onClick={() => Taro.navigateTo({
|
||||
url: `/shop/goodsDetail/index?id=${item.goodsId}`
|
||||
})}
|
||||
>
|
||||
购买
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{
|
||||
tab1value == '1' && <Empty description="暂无相关商品" style={{
|
||||
background: 'transparent',
|
||||
}}/>
|
||||
}
|
||||
|
||||
{
|
||||
tab1value == '2' && <Empty description="暂无相关商品" style={{
|
||||
background: 'transparent',
|
||||
}}/>
|
||||
}
|
||||
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default BestSellers
|
||||
|
||||
@@ -8,7 +8,7 @@ import {TenantId} from "@/config/app";
|
||||
import {getOrganization} from "@/api/system/organization";
|
||||
import {myUserVerify} from "@/api/system/userVerify";
|
||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
||||
import {handleInviteRelation} from "@/utils/invite";
|
||||
import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite";
|
||||
import {View,Text} from '@tarojs/components'
|
||||
import MySearch from "./MySearch";
|
||||
import './Header.scss';
|
||||
@@ -88,6 +88,22 @@ const Header = (props: any) => {
|
||||
/* 获取用户手机号 */
|
||||
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
|
||||
const {code, encryptedData, iv} = detail
|
||||
|
||||
// 防重复登录检查
|
||||
const loginKey = 'login_in_progress'
|
||||
const loginInProgress = Taro.getStorageSync(loginKey)
|
||||
|
||||
if (loginInProgress && Date.now() - loginInProgress < 5000) { // 5秒内防重
|
||||
return
|
||||
}
|
||||
|
||||
// 标记登录开始
|
||||
Taro.setStorageSync(loginKey, Date.now())
|
||||
|
||||
// 获取存储的邀请参数
|
||||
const inviteParams = getStoredInviteParams()
|
||||
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
|
||||
|
||||
Taro.login({
|
||||
success: function () {
|
||||
if (code) {
|
||||
@@ -99,7 +115,7 @@ const Header = (props: any) => {
|
||||
encryptedData,
|
||||
iv,
|
||||
notVerifyPhone: true,
|
||||
refereeId: 0,
|
||||
refereeId: refereeId, // 使用解析出的推荐人ID
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId
|
||||
},
|
||||
@@ -108,6 +124,9 @@ const Header = (props: any) => {
|
||||
TenantId
|
||||
},
|
||||
success: async function (res) {
|
||||
// 清除登录防重标记
|
||||
Taro.removeStorageSync('login_in_progress')
|
||||
|
||||
if (res.data.code == 1) {
|
||||
Taro.showToast({
|
||||
title: res.data.message,
|
||||
@@ -124,14 +143,7 @@ const Header = (props: any) => {
|
||||
// 处理邀请关系
|
||||
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
|
||||
})
|
||||
}
|
||||
await handleInviteRelation(res.data.data.user.userId)
|
||||
} catch (error) {
|
||||
console.error('处理邀请关系失败:', error)
|
||||
}
|
||||
@@ -141,6 +153,10 @@ const Header = (props: any) => {
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
},
|
||||
fail: function() {
|
||||
// 清除登录防重标记
|
||||
Taro.removeStorageSync('login_in_progress')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -157,9 +173,10 @@ const Header = (props: any) => {
|
||||
return (
|
||||
<>
|
||||
<View className={'fixed top-0 header-bg'} style={{
|
||||
height: !props.stickyStatus ? '180px' : '148px',
|
||||
height: !props.stickyStatus ? '180px' : 'auto',
|
||||
paddingBottom: '12px'
|
||||
}}>
|
||||
<MySearch/>
|
||||
<MySearch statusBarHeight={statusBarHeight} />
|
||||
{/*{!props.stickyStatus && <MySearch done={reload}/>}*/}
|
||||
</View>
|
||||
<NavBar
|
||||
@@ -190,6 +207,7 @@ const Header = (props: any) => {
|
||||
<TriangleDown className={'text-white'} size={9}/>
|
||||
</View>
|
||||
)}>
|
||||
{/*<QRLoginButton />*/}
|
||||
</NavBar>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ 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";
|
||||
import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite";
|
||||
|
||||
// 微信获取手机号回调参数类型
|
||||
interface GetPhoneNumberDetail {
|
||||
@@ -40,6 +40,11 @@ const Login = (props: LoginProps) => {
|
||||
/* 获取用户手机号 */
|
||||
const handleGetPhoneNumber = ({detail}: GetPhoneNumberEvent) => {
|
||||
const {code, encryptedData, iv} = detail
|
||||
|
||||
// 获取存储的邀请参数
|
||||
const inviteParams = getStoredInviteParams()
|
||||
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
|
||||
|
||||
Taro.login({
|
||||
success: function () {
|
||||
if (code) {
|
||||
@@ -51,7 +56,7 @@ const Login = (props: LoginProps) => {
|
||||
encryptedData,
|
||||
iv,
|
||||
notVerifyPhone: true,
|
||||
refereeId: 0,
|
||||
refereeId: refereeId, // 使用解析出的推荐人ID
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import {useState} from "react";
|
||||
import Taro from '@tarojs/taro';
|
||||
import { goTo } from '@/utils/navigation';
|
||||
|
||||
function MySearch() {
|
||||
function MySearch(props: any) {
|
||||
const [keywords, setKeywords] = useState<string>('')
|
||||
|
||||
const onKeywords = (keywords: string) => {
|
||||
@@ -39,7 +39,7 @@ function MySearch() {
|
||||
background: '#ffffff',
|
||||
padding: '0 5px',
|
||||
borderRadius: '20px',
|
||||
marginTop: '100px',
|
||||
marginTop: `${props.statusBarHeight + 50}px`,
|
||||
}}
|
||||
>
|
||||
<Search size={18} className={'ml-2 text-gray-400'}/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Header from './Header';
|
||||
import BestSellers from './BestSellers';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
||||
import {useShareAppMessage} from "@tarojs/taro"
|
||||
import {useEffect, useState} from "react";
|
||||
import {getShopInfo} from "@/api/layout";
|
||||
import {Sticky} from '@nutui/nutui-react-taro'
|
||||
@@ -16,22 +16,28 @@ function Home() {
|
||||
// 吸顶状态
|
||||
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||
|
||||
useShareTimeline(() => {
|
||||
return {
|
||||
title: '网宿小店 - 网宿软件',
|
||||
path: `/pages/index/index`
|
||||
};
|
||||
});
|
||||
|
||||
useShareAppMessage(() => {
|
||||
// 获取当前用户ID,用于生成邀请链接
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
|
||||
return {
|
||||
title: '网宿小店 - 网宿软件',
|
||||
path: `/pages/index/index`,
|
||||
path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
console.log('首页分享成功');
|
||||
Taro.showToast({
|
||||
title: '分享成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
},
|
||||
fail: function () {
|
||||
console.log('分享失败');
|
||||
console.log('首页分享失败');
|
||||
Taro.showToast({
|
||||
title: '分享失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -89,18 +95,32 @@ function Home() {
|
||||
|
||||
})
|
||||
|
||||
// 检查是否有待处理的邀请关系
|
||||
// 检查是否有待处理的邀请关系 - 异步处理,不阻塞页面加载
|
||||
if (hasPendingInvite()) {
|
||||
console.log('检测到待处理的邀请关系')
|
||||
// 延迟处理,确保用户信息已加载
|
||||
// 延迟处理,确保用户信息已加载,并设置超时保护
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const success = await checkAndHandleInviteRelation()
|
||||
// 设置超时保护,避免长时间等待
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('邀请关系处理超时')), 8000)
|
||||
);
|
||||
|
||||
const invitePromise = checkAndHandleInviteRelation();
|
||||
|
||||
const success = await Promise.race([invitePromise, timeoutPromise]);
|
||||
if (success) {
|
||||
console.log('首页邀请关系处理成功')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('首页邀请关系处理失败:', error)
|
||||
// 邀请关系处理失败不应该影响页面正常显示
|
||||
// 可以选择清除邀请参数,避免重复尝试
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (errorMessage?.includes('超时')) {
|
||||
console.log('邀请关系处理超时,清除邀请参数')
|
||||
// 可以选择清除邀请参数或稍后重试
|
||||
}
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
3
src/pages/menu/menu.config.ts
Normal file
3
src/pages/menu/menu.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '菜单'
|
||||
})
|
||||
81
src/pages/menu/menu.tsx
Normal file
81
src/pages/menu/menu.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
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 navTo from "@/utils/common";
|
||||
|
||||
/**
|
||||
* 文章终极列表
|
||||
* @constructor
|
||||
*/
|
||||
const Menu = () => {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [list, setList] = useState<CmsArticle[]>()
|
||||
|
||||
const reload = async () => {
|
||||
setLoading(true)
|
||||
const article = await pageCmsArticle({categoryId: 4289, status: 0})
|
||||
if (article) {
|
||||
setList(article?.list)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
setStatusBarHeight(res.statusBarHeight)
|
||||
},
|
||||
})
|
||||
reload().then(() => {
|
||||
console.log('初始化完成')
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{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)
|
||||
}}
|
||||
/>
|
||||
<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 Menu
|
||||
@@ -6,7 +6,7 @@ import {useUser} from '@/hooks/useUser'
|
||||
import {useEffect} from "react";
|
||||
import {useDealerUser} from "@/hooks/useDealerUser";
|
||||
|
||||
const UserCell = () => {
|
||||
const IsDealer = () => {
|
||||
const {isSuperAdmin} = useUser();
|
||||
const {dealerUser} = useDealerUser()
|
||||
|
||||
@@ -20,7 +20,7 @@ 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={{
|
||||
@@ -46,7 +46,7 @@ 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={{
|
||||
@@ -56,7 +56,7 @@ const UserCell = () => {
|
||||
<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'}>分销中心</Text>
|
||||
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
|
||||
</View>
|
||||
}
|
||||
@@ -73,7 +73,7 @@ const UserCell = () => {
|
||||
*/
|
||||
return (
|
||||
<>
|
||||
<View className={'px-4'}>
|
||||
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
style={{
|
||||
@@ -82,19 +82,15 @@ const UserCell = () => {
|
||||
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>
|
||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>开通VIP</Text>
|
||||
<Text className={'text-white opacity-80 pl-3'}>享优惠</Text>
|
||||
</View>
|
||||
}
|
||||
extra={
|
||||
<>
|
||||
<Text className={'text-white opacity-80 px-3'}>需医师资格证</Text>
|
||||
<ArrowRight color="#cccccc" size={18}/>
|
||||
</>
|
||||
}
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => navTo('/dealer/apply/add', true)}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default UserCell
|
||||
export default IsDealer
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import {Button} from '@nutui/nutui-react-taro'
|
||||
import {Avatar, Tag} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {Scan} from '@nutui/icons-react-taro';
|
||||
import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro'
|
||||
import {View, Text, Image} from '@tarojs/components'
|
||||
import {getUserInfo, getWxOpenId} from '@/api/layout';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {useEffect, useState} 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 {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import {useUserData} from "@/hooks/useUserData";
|
||||
import {getStoredInviteParams} from "@/utils/invite";
|
||||
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
||||
|
||||
function UserCard() {
|
||||
const {
|
||||
isAdmin
|
||||
} = useUser();
|
||||
const { data, refresh } = useUserData()
|
||||
const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
const {data, refresh} = useUserData()
|
||||
const {getDisplayName, getRoleName} = useUser();
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||
const [userInfo, setUserInfo] = useState<User>()
|
||||
const [couponCount, setCouponCount] = useState(0)
|
||||
const [pointsCount, setPointsCount] = useState(0)
|
||||
const [giftCount, setGiftCount] = useState(0)
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
@@ -33,6 +26,11 @@ function UserCard() {
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||
Taro.getSetting({
|
||||
@@ -50,33 +48,6 @@ function UserCard() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadUserStats = (userId: number) => {
|
||||
// 加载优惠券数量
|
||||
getMyAvailableCoupons()
|
||||
.then((coupons: any) => {
|
||||
setCouponCount(coupons?.length || 0)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Coupon count error:', error)
|
||||
})
|
||||
|
||||
// 加载积分数量
|
||||
console.log(userId)
|
||||
setPointsCount(0)
|
||||
// getUserPointsStats(userId)
|
||||
// .then((res: any) => {
|
||||
// setPointsCount(res.currentPoints || 0)
|
||||
// })
|
||||
// .catch((error: any) => {
|
||||
// console.error('Points stats error:', error)
|
||||
// })
|
||||
// 加载礼品劵数量
|
||||
setGiftCount(0)
|
||||
// pageUserGiftLog({userId, page: 1, limit: 1}).then(res => {
|
||||
// setGiftCount(res.count || 0)
|
||||
// })
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
@@ -92,11 +63,6 @@ function UserCard() {
|
||||
setIsLogin(true);
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
|
||||
// 加载用户统计数据
|
||||
if (data.userId) {
|
||||
loadUserStats(data.userId)
|
||||
}
|
||||
|
||||
// 获取openId
|
||||
if (!data.openid) {
|
||||
Taro.login({
|
||||
@@ -149,8 +115,13 @@ function UserCard() {
|
||||
};
|
||||
|
||||
/* 获取用户手机号 */
|
||||
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
|
||||
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) {
|
||||
@@ -162,7 +133,7 @@ function UserCard() {
|
||||
encryptedData,
|
||||
iv,
|
||||
notVerifyPhone: true,
|
||||
refereeId: 0,
|
||||
refereeId: refereeId, // 使用解析出的推荐人ID
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId
|
||||
},
|
||||
@@ -194,74 +165,114 @@ function UserCard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={'header-bg pt-20'}>
|
||||
<View className={'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',
|
||||
height: '170px',
|
||||
// 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'}>
|
||||
{
|
||||
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>
|
||||
)
|
||||
}
|
||||
<View className={'user-info flex flex-col px-2'}>
|
||||
<View className={'py-1 text-black font-bold'}>{getDisplayName()}</View>
|
||||
{IsLogin ? (
|
||||
<View className={'grade text-xs py-1'}>
|
||||
<Tag type="success" round>
|
||||
<Text className={'p-1'}>
|
||||
{getRoleName()}
|
||||
</Text>
|
||||
</Tag>
|
||||
</View>
|
||||
) : ''}
|
||||
{/* 使用相对定位容器,让个人资料图片可以绝对定位在右上角 */}
|
||||
<View className="relative z-20">
|
||||
<View
|
||||
className={'user-card w-full flex flex-col justify-around rounded-xl'}
|
||||
style={{
|
||||
background: 'linear-gradient(to bottom, #ffffff, #ffffff)',
|
||||
height: '170px',
|
||||
}}
|
||||
>
|
||||
<View className={'user-card-header flex w-full justify-between items-center pt-4'}>
|
||||
<View className={'flex items-center mx-4'}>
|
||||
{
|
||||
IsLogin ? (
|
||||
<Avatar size="large"
|
||||
src={userInfo?.avatar || 'https://oss.wsdns.cn/20250623/62f830b85edb4a7293b8948c25e6f987.jpeg'}
|
||||
shape="round"/>
|
||||
) : (
|
||||
<Button className={'text-black'} open-type="getPhoneNumber"
|
||||
onGetPhoneNumber={handleGetPhoneNumber}>
|
||||
<Avatar size="large"
|
||||
src={userInfo?.avatar || 'https://oss.wsdns.cn/20250623/62f830b85edb4a7293b8948c25e6f987.jpeg'}
|
||||
shape="round"/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<View className={'user-info flex flex-col px-2'}>
|
||||
<View className={'py-1 text-black font-bold'}>{getDisplayName()}</View>
|
||||
{IsLogin ? (
|
||||
<View className={'grade text-xs py-1'}>
|
||||
<Tag type="success" round>
|
||||
<Text className={'p-1'}>
|
||||
{getRoleName()}
|
||||
</Text>
|
||||
</Tag>
|
||||
</View>
|
||||
) : ''}
|
||||
</View>
|
||||
</View>
|
||||
<Space style={{
|
||||
marginTop: '30px',
|
||||
marginRight: '10px'
|
||||
}}>
|
||||
{/*统一扫码入口 - 支持登录和核销*/}
|
||||
<UnifiedQRButton
|
||||
text="扫码"
|
||||
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>
|
||||
</View>
|
||||
<View className={'flex justify-around mt-1'}>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/wallet/wallet', true)}>
|
||||
<Text className={'text-sm text-gray-500'}>余额</Text>
|
||||
<Text className={'text-xl'}>{data?.balance || '0.00'}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}>
|
||||
<Text className={'text-sm text-gray-500'}>积分</Text>
|
||||
<Text className={'text-xl'}>{data?.points || 0}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/coupon/index', true)}>
|
||||
<Text className={'text-sm text-gray-500'}>优惠券</Text>
|
||||
<Text className={'text-xl'}>{data?.coupons || 0}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/gift/index', true)}>
|
||||
<Text className={'text-sm text-gray-500'}>礼品卡</Text>
|
||||
<Text className={'text-xl'}>{data?.giftCards || 0}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{isAdmin() && <Scan 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>
|
||||
</View>
|
||||
<View className={'flex justify-around mt-1'}>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/wallet/wallet', true)}>
|
||||
<Text className={'text-sm text-gray-500'}>余额</Text>
|
||||
<Text className={'text-xl'}>{data?.balance || '0.00'}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}>
|
||||
<Text className={'text-sm text-gray-500'}>积分</Text>
|
||||
<Text className={'text-xl'}>{data?.points || 0}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/coupon/index', true)}>
|
||||
<Text className={'text-sm text-gray-500'}>优惠券</Text>
|
||||
<Text className={'text-xl'}>{data?.coupons || 0}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/gift/index', true)}>
|
||||
<Text className={'text-sm text-gray-500'}>礼品卡</Text>
|
||||
<Text className={'text-xl'}>{data?.giftCards || 0}</Text>
|
||||
</View>
|
||||
|
||||
{/* 个人资料图片,定位在右上角 */}
|
||||
<View
|
||||
className="absolute top-0 right-0 overflow-hidden z-30"
|
||||
style={{
|
||||
borderRadius: "0 0.75rem 0 0"
|
||||
}}
|
||||
onClick={() => navTo('/user/profile/profile', true)}
|
||||
>
|
||||
<Image
|
||||
src="https://oss.wsdns.cn/20250913/7c3de38b377344b89131aba40214f63f.png"
|
||||
style={{
|
||||
width: "200rpx"
|
||||
}}
|
||||
mode="widthFix"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default UserCard;
|
||||
|
||||
@@ -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'}}>
|
||||
|
||||
145
src/pages/user/components/UserGrid.tsx
Normal file
145
src/pages/user/components/UserGrid.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
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,
|
||||
ShoppingAdd,
|
||||
Service
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
|
||||
const UserCell = () => {
|
||||
const {logoutUser} = 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}
|
||||
>
|
||||
<Grid.Item text="企业采购" onClick={() => navTo('/user/poster/poster', 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="联系我们">
|
||||
<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/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/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/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={'关于我们'} 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
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
.header-bg{
|
||||
background: url('https://oss.wsdns.cn/20250621/edb5d4da976b4d97ba185cb7077d2858.jpg') no-repeat top center;
|
||||
//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;
|
||||
}
|
||||
|
||||
@@ -1,49 +1,45 @@
|
||||
import {useEffect} from 'react'
|
||||
import {useEffect, useRef} 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 {useUserData} from "@/hooks/useUserData";
|
||||
import {View} from '@tarojs/components';
|
||||
import './user.scss'
|
||||
import IsDealer from "./components/IsDealer";
|
||||
import UserCell from "@/pages/user/components/UserCell";
|
||||
import {useThemeStyles} from "@/hooks/useTheme";
|
||||
|
||||
function User() {
|
||||
const {
|
||||
isAdmin
|
||||
} = useUser();
|
||||
|
||||
const {refresh} = useUserData()
|
||||
const userCardRef = useRef<any>()
|
||||
const themeStyles = useThemeStyles();
|
||||
|
||||
// 下拉刷新处理
|
||||
const handleRefresh = async () => {
|
||||
await refresh()
|
||||
// 如果 UserCard 组件有自己的刷新方法,也可以调用
|
||||
if (userCardRef.current?.handleRefresh) {
|
||||
await userCardRef.current.handleRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 门店核销管理
|
||||
*/
|
||||
if (isAdmin()) {
|
||||
return <>
|
||||
<div className={'w-full'} style={{
|
||||
background: 'linear-gradient(to bottom, #e9fff2, #f9fafb)'
|
||||
}}>
|
||||
<UserCard/>
|
||||
<UserOrder/>
|
||||
<IsDealer/>
|
||||
<UserCell/>
|
||||
<UserFooter/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'w-full'} style={{
|
||||
background: 'linear-gradient(to bottom, #e9fff2, #f9fafb)'
|
||||
}}>
|
||||
<UserCard/>
|
||||
<UserOrder/>
|
||||
<IsDealer/>
|
||||
<UserCell/>
|
||||
<UserFooter/>
|
||||
</div>
|
||||
</>
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
headHeight={60}
|
||||
>
|
||||
<View className={'h-44 w-full fixed top-0 z-0'} style={themeStyles.primaryBackground}></View>
|
||||
<UserCard ref={userCardRef}/>
|
||||
<UserOrder/>
|
||||
<IsDealer/>
|
||||
<UserCell/>
|
||||
<UserFooter/>
|
||||
</PullToRefresh>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user