Files
market_page/miniprogram/src/pages/user/index.tsx
jeremygan2021 a47be29bf1
All checks were successful
Deploy to Server / deploy (push) Successful in 36s
video curcse
2026-02-27 14:09:11 +08:00

451 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { View, Text, Image, Button, Checkbox, CheckboxGroup, RichText } from '@tarojs/components'
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro'
import { useState } from 'react'
import { login as silentLogin } from '../../utils/request'
import './index.scss'
export default function UserIndex() {
const [userInfo, setUserInfo] = useState<any>(null)
const [showLoginModal, setShowLoginModal] = useState(false)
const [isAgreed, setIsAgreed] = useState(false)
const [showAgreement, setShowAgreement] = useState(false) // For showing agreement content
useDidShow(() => {
const info = Taro.getStorageSync('userInfo')
if (info) setUserInfo(info)
})
usePullDownRefresh(async () => {
try {
const res = await silentLogin()
if (res) {
setUserInfo(res)
}
Taro.stopPullDownRefresh()
} catch (e) {
Taro.stopPullDownRefresh()
Taro.showToast({ title: '刷新失败', icon: 'none' })
}
})
const goOrders = () => Taro.navigateTo({ url: '/pages/order/list' })
const goDistributor = () => Taro.navigateTo({ url: '/subpackages/distributor/index' })
const goInvite = () => Taro.navigateTo({ url: '/subpackages/distributor/invite' })
const goWithdraw = () => Taro.navigateTo({ url: '/subpackages/distributor/withdraw' })
const goActivityList = (tab = 'all') => Taro.navigateTo({ url: `/subpackages/forum/activity/index?tab=${tab}` })
const handleAddress = async () => {
try {
const res = await Taro.chooseAddress()
// 同步地址信息到后端
const token = Taro.getStorageSync('token')
if (token) {
await Taro.request({
url: 'https://market.quant-speed.com/api/wechat/update/',
method: 'POST',
header: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
data: {
province: res.provinceName,
city: res.cityName,
country: '中国' // 默认中国chooseAddress通常返回国内地址
}
})
// 更新本地 userInfo
const updatedInfo = { ...userInfo, province: res.provinceName, city: res.cityName, country: '中国' }
setUserInfo(updatedInfo)
Taro.setStorageSync('userInfo', updatedInfo)
Taro.showToast({ title: '地址信息已同步', icon: 'success' })
}
} catch(e) {
// 用户取消或其他错误,忽略
}
}
const handleAvatarClick = async () => {
if (!userInfo) return
try {
const { tempFilePaths } = await Taro.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'] })
if (!tempFilePaths.length) return
Taro.showLoading({ title: '上传中...' })
const token = Taro.getStorageSync('token')
const uploadRes = await Taro.uploadFile({
url: 'https://market.quant-speed.com/api/upload/image/',
filePath: tempFilePaths[0],
name: 'file',
header: {
'Authorization': `Bearer ${token}`
}
})
if (uploadRes.statusCode !== 200) {
throw new Error('上传失败')
}
const data = JSON.parse(uploadRes.data)
const newAvatarUrl = data.url
// 更新后端用户信息
await Taro.request({
url: 'https://market.quant-speed.com/api/wechat/update/',
method: 'POST',
header: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
data: {
avatar_url: newAvatarUrl
}
})
// 更新本地 userInfo
const updatedInfo = { ...userInfo, avatar_url: newAvatarUrl }
setUserInfo(updatedInfo)
Taro.setStorageSync('userInfo', updatedInfo)
Taro.hideLoading()
Taro.showToast({ title: '头像更新成功', icon: 'success' })
} catch (e) {
Taro.hideLoading()
Taro.showToast({ title: '头像更新失败', icon: 'none' })
console.error(e)
}
}
const handleNicknameClick = () => {
if (!userInfo) return
Taro.showModal({
title: '修改昵称',
content: userInfo.nickname || '',
// @ts-ignore
editable: true,
placeholderText: '请输入新昵称',
success: async function (res) {
if (res.confirm && (res as any).content) {
const newNickname = (res as any).content
if (newNickname === userInfo.nickname) return
try {
Taro.showLoading({ title: '更新中...' })
const token = Taro.getStorageSync('token')
await Taro.request({
url: 'https://market.quant-speed.com/api/wechat/update/',
method: 'POST',
header: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
data: {
nickname: newNickname
}
})
// 更新本地 userInfo
const updatedInfo = { ...userInfo, nickname: newNickname }
setUserInfo(updatedInfo)
Taro.setStorageSync('userInfo', updatedInfo)
Taro.hideLoading()
Taro.showToast({ title: '昵称已更新', icon: 'success' })
} catch (e) {
Taro.hideLoading()
Taro.showToast({ title: '更新失败', icon: 'none' })
console.error(e)
}
}
}
})
}
const handleLogout = () => {
Taro.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: function (res) {
if (res.confirm) {
Taro.removeStorageSync('token')
Taro.removeStorageSync('userInfo')
setUserInfo(null)
Taro.showToast({ title: '已退出登录', icon: 'success' })
}
}
})
}
const login = async () => {
try {
// 1. 获取微信登录 Code
const { code } = await Taro.login()
if (!code) throw new Error('登录失败:无法获取 Code')
// 2. 调用后端登录 (仅 Code)
const res = await Taro.request({
url: 'https://market.quant-speed.com/api/wechat/login/',
method: 'POST',
data: { code }
})
console.log('code:', code)
if (res.statusCode === 200 && res.data.token) {
console.log('登录成功,后端返回用户信息:', res.data)
Taro.setStorageSync('token', res.data.token)
Taro.setStorageSync('userInfo', res.data)
setUserInfo(res.data)
Taro.showToast({ title: '登录成功', icon: 'success' })
} else {
throw new Error(res.data.error || '登录请求失败')
}
} catch (e) {
Taro.showToast({ title: e.message || '登录失败', icon: 'none' })
}
}
const getPhoneNumber = async (e) => {
const { code: phoneCode, errMsg } = e.detail
if (errMsg !== "getPhoneNumber:ok") {
Taro.showToast({ title: '获取手机号失败', icon: 'none' })
return
}
try {
Taro.showLoading({ title: '登录中...' })
// 1. 获取登录 Code
const { code: loginCode } = await Taro.login()
// 2. 调用后端登录 (Code + PhoneCode)
console.log('loginCode:', loginCode)
console.log('phoneCode:', phoneCode)
const res = await Taro.request({
url: 'https://market.quant-speed.com/api/wechat/login/',
method: 'POST',
data: {
code: loginCode,
phone_code: phoneCode
}
})
Taro.hideLoading()
if (res.statusCode === 200 && res.data.token) {
console.log('手机号登录成功,后端返回用户信息:', res.data)
Taro.setStorageSync('token', res.data.token)
Taro.setStorageSync('userInfo', res.data)
setUserInfo(res.data)
setShowLoginModal(false) // Close modal on success
Taro.showToast({ title: '授权登录成功', icon: 'success' })
} else {
throw new Error(res.data.error || '登录失败')
}
} catch(err) {
Taro.hideLoading()
Taro.showToast({ title: err.message || '系统异常', icon: 'none' })
}
}
const serviceGroups = [
{
title: '基础服务',
items: [
{ title: '我的订单', icon: '📦', action: goOrders },
{ title: '地址管理', icon: '📝', action: handleAddress },
{ title: '活动管理', icon: '⌚️', action: () => goActivityList('mine') },
]
},
{
title: '分销中心',
items: [
{ title: '分销首页', icon: '⚡', action: goDistributor },
{ title: '推广邀请', icon: '🤝', action: goInvite },
{ title: '佣金提现', icon: '💰', action: goWithdraw },
]
},
{
title: '其他',
items: [
{ title: '联系客服', icon: '🎧', isContact: true },
...(userInfo ? [{ title: '退出登录', icon: '🚪', action: handleLogout }] : [])
]
}
]
const stats = [
{ label: '余额', value: '0.00' },
{ label: '积分', value: '0' },
{ label: '优惠券', value: '0' }
]
const handleAgreementCheck = (e) => {
setIsAgreed(!!e.detail.value.length)
}
const handleShowAgreement = (e) => {
e.stopPropagation()
setShowAgreement(true)
}
const handleLoginBtnClick = () => {
if (!isAgreed) {
Taro.showToast({ title: '请先阅读并同意用户协议', icon: 'none' })
return
}
// If agreed, the button openType='getPhoneNumber' handles it.
}
return (
<View className='page-container'>
{/* Profile Card */}
<View className='profile-card'>
<View className='avatar-container' onClick={handleAvatarClick}>
<Image src={userInfo?.avatar_url || 'https://via.placeholder.com/150/00b96b/FFFFFF?text=USER'} className='avatar' />
{userInfo && <View className='online-dot' />}
</View>
<View className='info-col'>
<Text className='nickname' onClick={handleNicknameClick}>{userInfo?.nickname || '未登录用户'}</Text>
{userInfo && (
<View className='badges-row'>
{/* 明星技术用户 */}
{userInfo.is_star && (
<View className='badge star'>
<Text className='badge-icon'>🌟</Text>
<Text className='badge-text'></Text>
</View>
)}
{/* 管理员 */}
{userInfo.is_admin && (
<View className='badge admin'>
<Text className='badge-icon'>🛡</Text>
<Text className='badge-text'></Text>
</View>
)}
{/* 网页用户徽章 - 仅在 has_web_badge 为 true 时显示 */}
{userInfo.has_web_badge && (
<View className='badge web active'>
<Text className='badge-icon'>🌐</Text>
<Text className='badge-text'></Text>
</View>
)}
</View>
)}
<Text className='uid'>ID: {userInfo ? (userInfo.phone_number || userInfo.id || '----') : '----'}</Text>
{!userInfo?.phone_number && (
<View className='login-btns'>
<Button
className='btn-login primary'
onClick={() => setShowLoginModal(true)}
>
</Button>
</View>
)}
</View>
<View className='card-bg-effect' />
</View>
{/* Stats Row */}
<View className='stats-row'>
{stats.map((item, idx) => (
<View key={idx} className='stat-item'>
<Text className='stat-val'>{item.value}</Text>
<Text className='stat-lbl'>{item.label}</Text>
</View>
))}
</View>
{/* Service Groups */}
<View className='service-container'>
{serviceGroups.map((group, gIdx) => (
<View key={gIdx} className='service-group'>
<Text className='group-title'>{group.title}</Text>
<View className='grid-layout'>
{group.items.map((item, idx) => (
<View key={idx} className='grid-item' onClick={item.action}>
<View className='icon-box'>
<Text className='icon'>{item.icon}</Text>
</View>
<Text className='item-title'>{item.title}</Text>
{item.isContact && <Button openType='contact' className='contact-overlay' />}
</View>
))}
</View>
</View>
))}
</View>
<View className='version-info'>
<Text>Quant Speed Market v1.0.0</Text>
<Text>Powered by Taro & React</Text>
</View>
{/* Login Modal */}
{showLoginModal && (
<View className='login-modal-mask' onClick={() => setShowLoginModal(false)}>
<View className='login-modal-content' onClick={e => e.stopPropagation()}>
<View className='modal-header'>
<Text className='modal-title'> Quant Speed</Text>
<Text className='modal-subtitle'></Text>
<View className='close-icon' onClick={() => setShowLoginModal(false)}>×</View>
</View>
<View className='modal-body'>
<Button
className={`btn-modal-login ${isAgreed ? 'primary' : 'disabled'}`}
openType={isAgreed ? 'getPhoneNumber' : undefined}
onGetPhoneNumber={getPhoneNumber}
onClick={handleLoginBtnClick}
>
</Button>
<Button className='btn-modal-cancel' onClick={() => setShowLoginModal(false)}>
</Button>
<View className='agreement-box'>
<CheckboxGroup onChange={handleAgreementCheck}>
<Checkbox value='agree' checked={isAgreed} color='#00b96b' className='agreement-checkbox' />
</CheckboxGroup>
<Text className='agreement-text'>
<Text className='link' onClick={handleShowAgreement}></Text> <Text className='link' onClick={handleShowAgreement}></Text>
</Text>
</View>
</View>
</View>
</View>
)}
{/* Agreement Detail Modal */}
{showAgreement && (
<View className='agreement-modal-mask'>
<View className='agreement-content'>
<Text className='agreement-title'></Text>
<View className='agreement-scroll'>
<View className='p'><Text>1. </Text></View>
<View className='p'><Text>使使</Text></View>
<View className='p'><Text>2. </Text></View>
<View className='p'><Text>2.1 Quant SpeedQuant Speed</Text></View>
<View className='p'><Text>3. </Text></View>
<View className='p'><Text>3.1 使访使</Text></View>
</View>
<Button className='btn-close' onClick={() => setShowAgreement(false)}></Button>
</View>
</View>
)}
</View>
)
}