Browse Source

feat(admin): 完成门店核销功能

- 新增管理员页面和相关组件
- 实现用户认证和权限控制
- 添加用户订单、积分等功能
- 优化用户卡片和用户页面布局
- 实现礼品卡核销功能
master
科技小王子 1 week ago
parent
commit
591df84568
  1. 2
      config/env.ts
  2. 150
      src/admin/components/StoreCell.tsx
  3. 249
      src/admin/components/UserCard.tsx
  4. 186
      src/admin/components/UserCell.tsx
  5. 102
      src/admin/components/UserFooter.tsx
  6. 69
      src/admin/components/UserOrder.tsx
  7. 3
      src/admin/index.config.ts
  8. 25
      src/admin/index.tsx
  9. 13
      src/app.config.ts
  10. 39
      src/components/SimpleQRCodeModal.tsx
  11. 19
      src/pages/user/components/UserCard.tsx
  12. 30
      src/pages/user/user.tsx
  13. 100
      src/user/store/verification.tsx

2
config/env.ts

@ -2,7 +2,7 @@
export const ENV_CONFIG = {
// 开发环境
development: {
API_BASE_URL: 'http://127.0.0.1:9200/api',
API_BASE_URL: 'https://cms-api.websoft.top/api',
APP_NAME: '开发环境',
DEBUG: 'true',
},

150
src/admin/components/StoreCell.tsx

@ -0,0 +1,150 @@
import {Cell} from '@nutui/nutui-react-taro'
import navTo from "@/utils/common";
import Taro from '@tarojs/taro'
import {View, Text} from '@tarojs/components'
import {ArrowRight, ShieldCheck, LogisticsError, Location, Reward, Tips, Ask, Setting, Scan} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
const UserCell = () => {
const {logoutUser, isCertified, hasRole, isAdmin} = useUser();
const onLogout = () => {
Taro.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: function (res) {
if (res.confirm) {
// 使用 useUser hook 的 logoutUser 方法
logoutUser();
Taro.reLaunch({
url: '/pages/index/index'
})
}
}
})
}
return (
<>
<View className={'px-4'}>
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}></Text>
</View>
}>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Scan size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/wallet/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
style={{
display: 'none'
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<LogisticsError size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/wallet/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Location size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/address/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<ShieldCheck size={16} color={isCertified() ? '#52c41a' : '#666'}/>
<Text className={'pl-3 text-sm'}></Text>
{isCertified() && (
<Text className={'pl-2 text-xs text-green-500'}></Text>
)}
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/userVerify/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Ask size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/help/index')
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Tips size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/about/index')
}}
/>
</Cell.Group>
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}></Text>
</View>
}>
<Cell
className="nutui-cell-clickable"
title="账号安全"
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/user/profile/profile', true)}
/>
<Cell
className="nutui-cell-clickable"
title="退出登录"
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={onLogout}
/>
</Cell.Group>
</View>
</>
)
}
export default UserCell

249
src/admin/components/UserCard.tsx

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

186
src/admin/components/UserCell.tsx

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

102
src/admin/components/UserFooter.tsx

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

69
src/admin/components/UserOrder.tsx

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

3
src/admin/index.config.ts

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

25
src/admin/index.tsx

@ -0,0 +1,25 @@
import {useEffect} from 'react'
import {useUser} from "@/hooks/useUser";
import {Text} from '@tarojs/components';
function Admin() {
const {
isAdmin
} = useUser();
useEffect(() => {
}, []);
if (!isAdmin()) {
return (
<Text></Text>
);
}
return (
<>
<Text>...</Text>
</>
)
}
export default Admin

13
src/app.config.ts

@ -68,6 +68,7 @@ export default defineAppConfig({
{
"root": "admin",
"pages": [
"index",
"article/index",
]
}
@ -90,12 +91,12 @@ export default defineAppConfig({
selectedIconPath: "assets/tabbar/home-active.png",
text: "首页",
},
{
pagePath: "pages/find/find",
iconPath: "assets/tabbar/find.png",
selectedIconPath: "assets/tabbar/find-active.png",
text: "发现",
},
// {
// pagePath: "pages/find/find",
// iconPath: "assets/tabbar/find.png",
// selectedIconPath: "assets/tabbar/find-active.png",
// text: "发现",
// },
{
pagePath: "pages/cart/cart",
iconPath: "assets/tabbar/cart.png",

39
src/components/SimpleQRCodeModal.tsx

@ -24,6 +24,20 @@ const SimpleQRCodeModal: React.FC<SimpleQRCodeModalProps> = ({
faceValue
}) => {
// const copyToClipboard = () => {
// if (qrContent) {
// Taro.setClipboardData({
// data: qrContent,
// success: () => {
// Taro.showToast({
// title: '兑换码已复制',
// icon: 'success'
// })
// }
// })
// }
// }
return (
<Popup
@ -38,9 +52,9 @@ const SimpleQRCodeModal: React.FC<SimpleQRCodeModalProps> = ({
>
<View className="p-6">
{/* 标题 */}
<View className="text-center mb-4">
<View className="mb-4">
<Text className="text-lg font-bold"></Text>
<Text className="text-sm text-gray-500 mt-1">
<Text className="text-sm text-gray-400 mt-1 px-2">
</Text>
</View>
@ -64,13 +78,22 @@ const SimpleQRCodeModal: React.FC<SimpleQRCodeModalProps> = ({
{/* 二维码区域 */}
<View className="text-center mb-4">
<View className="p-4 bg-white border border-gray-200 rounded-lg">
<View className="bg-gray-100 rounded flex items-center justify-center mx-auto"
style={{width: '200px', height: '200px'}}>
<View className="text-center">
<Text className="text-gray-500 text-sm"></Text>
<Text className="text-xs text-gray-400 mt-1">ID: {qrContent ? qrContent.slice(-6) : '------'}</Text>
{qrContent ? (
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(qrContent)}`}
alt="二维码"
style={{width: '200px', height: '200px'}}
className="mx-auto"
/>
) : (
<View className="bg-gray-100 rounded flex items-center justify-center mx-auto"
style={{width: '200px', height: '200px'}}>
<View className="text-center">
<QrCode size="48" className="text-gray-400 mb-2"/>
<Text className="text-gray-500 text-sm">...</Text>
</View>
</View>
</View>
)}
</View>
</View>

19
src/pages/user/components/UserCard.tsx

@ -1,5 +1,6 @@
import {Button} from '@nutui/nutui-react-taro'
import {Avatar, Tag} from '@nutui/nutui-react-taro'
import {Scan} from '@nutui/icons-react-taro';
import {getUserInfo, getWxOpenId} from '@/api/layout';
import Taro from '@tarojs/taro';
import {useEffect, useState} from "react";
@ -10,6 +11,9 @@ import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
import {useUser} from "@/hooks/useUser";
function UserCard() {
const {
isAdmin
} = useUser();
const {getDisplayName, getRoleName} = useUser();
const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>()
@ -46,6 +50,7 @@ function UserCard() {
// 加载积分数量
console.log(userId)
setPointsCount(0)
// getUserPointsStats(userId)
// .then((res: any) => {
// setPointsCount(res.currentPoints || 0)
@ -61,7 +66,6 @@ function UserCard() {
}
const reload = () => {
setPointsCount(0)
Taro.getUserInfo({
success: (res) => {
const avatar = res.userInfo.avatarUrl;
@ -214,7 +218,8 @@ function UserCard() {
) : ''}
</div>
</div>
<div className={'mx-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
{isAdmin() && <Scan onClick={() => navTo('/user/store/verification', true)} />}
<div 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)}>
{'个人资料'}
</div>
@ -223,7 +228,11 @@ function UserCard() {
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/wallet/wallet', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>¥ {userInfo?.balance || '0.00'}</span>
<span className={'text-xl'}>{userInfo?.balance || '0.00'}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{pointsCount}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/coupon/index', true)}>
@ -235,10 +244,6 @@ function UserCard() {
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{giftCount}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{pointsCount}</span>
</div>
</div>
</div>
</div>

30
src/pages/user/user.tsx

@ -2,20 +2,42 @@ import {useEffect} from 'react'
import UserCard from "./components/UserCard";
import UserOrder from "./components/UserOrder";
import UserCell from "./components/UserCell";
import './user.scss'
import UserFooter from "./components/UserFooter";
import {useUser} from "@/hooks/useUser";
import './user.scss'
function User() {
const {
isAdmin
} = useUser();
useEffect(() => {
}, []);
/**
*
*/
if (isAdmin()) {
return <>
<div className={'w-full'} style={{
background: 'linear-gradient(to bottom, #e9fff2, #f9fafb)'
}}>
<UserCard/>
<UserOrder/>
<UserCell/>
<UserFooter/>
</div>
</>
}
return (
<>
<div className={'w-full'} style={{
background: 'linear-gradient(to bottom, #e9fff2, #f9fafb)'
}}>
<UserCard />
<UserOrder />
<UserCell />
<UserCard/>
<UserOrder/>
<UserCell/>
<UserFooter/>
</div>
</>

100
src/user/store/verification.tsx

@ -6,15 +6,15 @@ import Taro from '@tarojs/taro'
import dayjs from 'dayjs'
import {getShopGiftByCode, updateShopGift} from "@/api/shop/shopGift";
interface VerificationData {
type: string
giftId: number
giftCode: string
verificationCode: string
faceValue: string
timestamp: number
expireTime?: string
}
// interface VerificationData {
// type: string
// giftId: number
// giftCode: string
// verificationCode: string
// faceValue: string
// timestamp: number
// expireTime?: string
// }
interface GiftCardInfo {
id: number
@ -40,7 +40,8 @@ const StoreVerification: React.FC = () => {
success: (res) => {
console.log('扫码结果:', res.result)
setScanResult(res.result)
parseQRCode(res.result)
verificationQRCode(res.result)
// parseQRCode(res.result)
},
fail: (err) => {
console.error('扫码失败:', err)
@ -53,41 +54,57 @@ const StoreVerification: React.FC = () => {
}
// 解析二维码数据
const parseQRCode = (qrData: string) => {
try {
const data: VerificationData = JSON.parse(qrData)
// const parseQRCode = (qrData: string) => {
// try {
// const data: VerificationData = JSON.parse(qrData)
//
// if (data.type === 'gift_card_verification') {
// setVerificationCode(data.verificationCode)
// console.log(data.verificationCode,'...vaerrr')
// } else {
// throw new Error('无效的二维码格式')
// }
// } catch (error) {
// console.error('解析二维码失败:', error)
// Taro.showToast({
// title: '无效的二维码',
// icon: 'error'
// })
// }
// }
if (data.type === 'gift_card_verification') {
setVerificationCode(data.verificationCode)
// 模拟获取礼品卡信息
mockGetGiftInfo(data)
} else {
throw new Error('无效的二维码格式')
}
} catch (error) {
console.error('解析二维码失败:', error)
Taro.showToast({
title: '无效的二维码',
// 扫码核销操作
const verificationQRCode = async (code:string) => {
const gift = await getShopGiftByCode(code)
if(gift.status == 1){
return Taro.showToast({
title: '此礼品码已使用',
icon: 'error'
})
}
}
// 模拟获取礼品卡信息(实际应该调用API)
const mockGetGiftInfo = (data: VerificationData) => {
// 这里应该调用后端API验证礼品卡信息
const mockGiftInfo: GiftCardInfo = {
id: data.giftId,
name: '礼品卡',
goodsName: '星巴克咖啡券',
faceValue: data.faceValue,
type: 20,
status: 0,
expireTime: data.expireTime,
code: data.giftCode
if(gift.status == 2){
return Taro.showToast({
title: '此礼品码已失效',
icon: 'error'
})
}
setGiftInfo(mockGiftInfo)
if(gift.userId == 0){
return Taro.showToast({
title: '此礼品码未认领',
icon: 'error'
})
}
updateShopGift({
...gift,
status: 1,
takeTime: dayjs.unix(Date.now() / 1000).format('YYYY-MM-DD HH:mm:ss')
}).then(() => {
Taro.showToast({
title: '核销成功',
icon: 'success'
})
})
}
// 手动输入核销码验证
@ -106,7 +123,8 @@ const StoreVerification: React.FC = () => {
const giftCard = await getShopGiftByCode(verificationCode.trim())
await updateShopGift({
...giftCard,
status: 1
status: 1,
takeTime: dayjs.unix(Date.now() / 1000).format('YYYY-MM-DD HH:mm:ss')
})
Taro.showToast({
title: '核销成功',

Loading…
Cancel
Save