feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
561
src_bak/pages/user/address-edit.tsx
Normal file
561
src_bak/pages/user/address-edit.tsx
Normal file
@@ -0,0 +1,561 @@
|
||||
import React, {useState, useEffect, useRef} from 'react'
|
||||
import {View, Text, Input, Textarea, ScrollView} from '@tarojs/components'
|
||||
import Taro, {useRouter} from '@tarojs/taro'
|
||||
import {Address} from '@nutui/nutui-react-taro'
|
||||
import {getShopUserAddress, addShopUserAddress, updateShopUserAddress} from '@/api/shop/shopUserAddress'
|
||||
import type {ShopUserAddress} from '@/api/shop/shopUserAddress/model'
|
||||
import {parseLngLatFromText} from '@/utils/geofence'
|
||||
import RegionData from '@/api/json/regions-data.json'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '新增收货地址',
|
||||
})
|
||||
|
||||
type SelectedLocation = { lng: string; lat: string; name?: string; address?: string }
|
||||
|
||||
/** 检查是否为定位权限拒绝错误 */
|
||||
const isLocationDenied = (e: any) => {
|
||||
const msg = String(e?.errMsg || e?.message || e || '')
|
||||
return msg.includes('auth deny') || msg.includes('authorize') || msg.includes('permission') || msg.includes('denied') || msg.includes('scope.userLocation')
|
||||
}
|
||||
|
||||
const isUserCancel = (e: any) => {
|
||||
const msg = String(e?.errMsg || e?.message || e || '')
|
||||
return msg.includes('cancel')
|
||||
}
|
||||
|
||||
/** 检查经纬度是否有效 */
|
||||
const hasValidLngLat = (addr?: Partial<ShopUserAddress> | null) => {
|
||||
if (!addr) return false
|
||||
const p = parseLngLatFromText(`${(addr as any)?.lng ?? ''},${(addr as any)?.lat ?? ''}`)
|
||||
if (!p) return false
|
||||
if (p.lng === 0 && p.lat === 0) return false
|
||||
return true
|
||||
}
|
||||
|
||||
const AddressEditPage: React.FC = () => {
|
||||
const {id, fromWx} = useRouter().params
|
||||
const isEditMode = !!id
|
||||
const addressId = id ? Number(id) : undefined
|
||||
const fromWxMode = fromWx === '1' || fromWx === 'true'
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [formData, setFormData] = useState<Partial<ShopUserAddress>>({
|
||||
name: '',
|
||||
phone: '',
|
||||
province: '',
|
||||
city: '',
|
||||
region: '',
|
||||
address: '',
|
||||
isDefault: false,
|
||||
})
|
||||
const [regionText, setRegionText] = useState('')
|
||||
const [inputText, setInputText] = useState('')
|
||||
const [selectedLocation, setSelectedLocation] = useState<SelectedLocation | null>(null)
|
||||
const [regionLocked, setRegionLocked] = useState(false)
|
||||
const [regionPickerVisible, setRegionPickerVisible] = useState(false)
|
||||
const [regionOptions, setRegionOptions] = useState<any[]>([])
|
||||
const wxDraftRef = useRef<Partial<ShopUserAddress> | null>(null)
|
||||
const wxDraftPatchedRef = useRef(false)
|
||||
|
||||
/** 初始化省市区选择器数据 */
|
||||
useEffect(() => {
|
||||
const options = (RegionData as any[]).map((province) => ({
|
||||
value: province.label,
|
||||
text: province.label,
|
||||
children: province.children?.map((city: any) => ({
|
||||
value: city.label,
|
||||
text: city.label,
|
||||
children: city.children?.map((region: any) => ({
|
||||
value: region.label,
|
||||
text: region.label,
|
||||
})) || [],
|
||||
})) || [],
|
||||
}))
|
||||
setRegionOptions(options)
|
||||
}, [])
|
||||
|
||||
/** 解析省市区 */
|
||||
const parseRegion = (text: string) => {
|
||||
for (const province of RegionData as any[]) {
|
||||
if (text.includes(province.label)) {
|
||||
const result: any = {province: province.label}
|
||||
if (province.children) {
|
||||
for (const city of province.children) {
|
||||
if (text.includes(city.label)) {
|
||||
result.city = city.label
|
||||
if (city.children) {
|
||||
for (const region of city.children) {
|
||||
if (text.includes(region.label)) {
|
||||
result.region = region.label
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** 解析地址文本(智能识别) */
|
||||
const parseAddressText = (text: string) => {
|
||||
const result: any = {}
|
||||
|
||||
// 手机号正则
|
||||
const phoneRegex = /1[3-9]\d{9}/
|
||||
const phoneMatch = text.match(phoneRegex)
|
||||
if (phoneMatch) result.phone = phoneMatch[0]
|
||||
|
||||
// 姓名正则(2-4个中文字符,通常在开头)
|
||||
const nameRegex = /^[\u4e00-\u9fa5]{2,4}/
|
||||
const nameMatch = text.match(nameRegex)
|
||||
if (nameMatch) result.name = nameMatch[0]
|
||||
|
||||
// 省市区识别
|
||||
const regionResult = parseRegion(text)
|
||||
if (regionResult) {
|
||||
result.province = regionResult.province
|
||||
result.city = regionResult.city
|
||||
result.region = regionResult.region
|
||||
}
|
||||
|
||||
// 详细地址提取
|
||||
let addressText = text
|
||||
if (result.name) addressText = addressText.replace(result.name, '')
|
||||
if (result.phone) addressText = addressText.replace(result.phone, '')
|
||||
if (result.province) addressText = addressText.replace(result.province, '')
|
||||
if (result.city) addressText = addressText.replace(result.city, '')
|
||||
if (result.region) addressText = addressText.replace(result.region, '')
|
||||
result.address = addressText.replace(/[,,。\s]+/g, '').trim()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/** 地址智能识别 */
|
||||
const recognizeAddress = () => {
|
||||
if (!inputText.trim()) {
|
||||
Taro.showToast({title: '请输入要识别的文本', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = parseAddressText(inputText)
|
||||
const newFormData: any = {
|
||||
...formData,
|
||||
name: result.name || formData.name,
|
||||
phone: result.phone || formData.phone,
|
||||
address: result.address || formData.address,
|
||||
}
|
||||
|
||||
if (!regionLocked) {
|
||||
newFormData.province = result.province || formData.province
|
||||
newFormData.city = result.city || formData.city
|
||||
newFormData.region = result.region || formData.region
|
||||
}
|
||||
|
||||
setFormData(newFormData)
|
||||
|
||||
if (!regionLocked && result.province && result.city && result.region) {
|
||||
setRegionText(`${result.province} ${result.city} ${result.region}`)
|
||||
}
|
||||
|
||||
Taro.showToast({title: regionLocked ? '识别成功(所在地区以定位为准)' : '识别成功', icon: 'success'})
|
||||
setInputText('')
|
||||
} catch {
|
||||
Taro.showToast({title: '识别失败,请检查文本格式', icon: 'none'})
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择定位 */
|
||||
const chooseGeoLocation = async () => {
|
||||
const applyChosenLocation = (res: any) => {
|
||||
if (!res) return
|
||||
if (res.latitude === undefined || res.longitude === undefined) {
|
||||
Taro.showToast({title: '定位信息获取失败', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
const next: SelectedLocation = {
|
||||
lng: String(res.longitude),
|
||||
lat: String(res.latitude),
|
||||
name: res.name,
|
||||
address: res.address,
|
||||
}
|
||||
|
||||
// 尝试从地图返回的地址文本解析省市区
|
||||
const regionResult = res?.provinceName || res?.cityName || res?.adName
|
||||
? {
|
||||
province: String(res.provinceName || ''),
|
||||
city: String(res.cityName || ''),
|
||||
region: String(res.adName || '')
|
||||
}
|
||||
: parseRegion(String(res.address || ''))
|
||||
|
||||
const province = String(regionResult?.province || '').trim()
|
||||
const city = String(regionResult?.city || '').trim()
|
||||
const region = String(regionResult?.region || '').trim()
|
||||
if (!province || !city || !region) {
|
||||
Taro.showToast({title: '定位未识别到所在地区,请重新选择定位', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedLocation(next)
|
||||
setRegionLocked(true)
|
||||
|
||||
// 将地图选点的地址同步到"收货地址"(剥离省市区,避免重复)
|
||||
const rawAddr = String(res.address || '').trim()
|
||||
const name = String(res.name || '').trim()
|
||||
let detail = rawAddr
|
||||
for (const part of [province, city, region]) {
|
||||
if (part) detail = detail.replace(part, '')
|
||||
}
|
||||
detail = detail.replace(/[,,]+/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
const base = detail || rawAddr
|
||||
const nextDetailAddress = (() => {
|
||||
if (!base && !name) return ''
|
||||
if (!base) return name
|
||||
if (!name) return base
|
||||
return base.includes(name) ? base : `${base} ${name}`
|
||||
})()
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
lng: next.lng,
|
||||
lat: next.lat,
|
||||
address: nextDetailAddress || prev.address,
|
||||
province,
|
||||
city,
|
||||
region,
|
||||
}))
|
||||
setRegionText(`${province} ${city} ${region}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const initLat = selectedLocation?.lat ? Number(selectedLocation.lat) : undefined
|
||||
const initLng = selectedLocation?.lng ? Number(selectedLocation.lng) : undefined
|
||||
const latitude = typeof initLat === 'number' && Number.isFinite(initLat) ? initLat : undefined
|
||||
const longitude = typeof initLng === 'number' && Number.isFinite(initLng) ? initLng : undefined
|
||||
const res = await Taro.chooseLocation({latitude, longitude})
|
||||
applyChosenLocation(res)
|
||||
} catch (e: any) {
|
||||
if (isUserCancel(e)) return
|
||||
if (isLocationDenied(e)) {
|
||||
try {
|
||||
const modal = await Taro.showModal({
|
||||
title: '需要定位权限',
|
||||
content: '选择定位需要开启定位权限,请在设置中开启后重试。',
|
||||
confirmText: '去设置',
|
||||
})
|
||||
if (modal.confirm) {
|
||||
await Taro.openSetting()
|
||||
const res = await Taro.chooseLocation({})
|
||||
applyChosenLocation(res)
|
||||
}
|
||||
} catch (_e) { /* ignore */
|
||||
}
|
||||
return
|
||||
}
|
||||
Taro.showToast({title: '打开地图失败,请重试', icon: 'none'})
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开地区选择器 */
|
||||
const openRegionPicker = () => {
|
||||
if (regionLocked) {
|
||||
Taro.showToast({title: '所在地区已由定位确定,修改请重新选择定位', icon: 'none'})
|
||||
return
|
||||
}
|
||||
setRegionPickerVisible(true)
|
||||
}
|
||||
|
||||
/** 地区选择器确认 */
|
||||
const handleRegionChange = (value: string[]) => {
|
||||
if (regionLocked) {
|
||||
Taro.showToast({title: '所在地区已由定位确定,修改请重新选择定位', icon: 'none'})
|
||||
return
|
||||
}
|
||||
const [province, city, region] = value
|
||||
setFormData(prev => ({...prev, province, city, region}))
|
||||
setRegionText(value.join(' '))
|
||||
}
|
||||
|
||||
/** 保存地址 */
|
||||
const handleSave = async () => {
|
||||
// 表单校验
|
||||
if (!formData.name?.trim()) {
|
||||
Taro.showToast({title: '请输入收货人姓名', icon: 'none'})
|
||||
return
|
||||
}
|
||||
if (!formData.phone?.trim()) {
|
||||
Taro.showToast({title: '请输入手机号', icon: 'none'})
|
||||
return
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(formData.phone)) {
|
||||
Taro.showToast({title: '手机号格式不正确', icon: 'none'})
|
||||
return
|
||||
}
|
||||
if (!formData.province || !formData.city || !formData.region) {
|
||||
Taro.showToast({title: '请选择所在地区', icon: 'none'})
|
||||
return
|
||||
}
|
||||
if (!formData.address?.trim()) {
|
||||
Taro.showToast({title: '请输入详细地址', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
// 经纬度检查(可选,不阻断保存流程)
|
||||
const loc = selectedLocation || (hasValidLngLat(formData) ? {
|
||||
lng: String(formData.lng),
|
||||
lat: String(formData.lat)
|
||||
} : null)
|
||||
|
||||
try {
|
||||
const submitData: any = {
|
||||
name: formData.name,
|
||||
phone: formData.phone,
|
||||
country: formData.country || '中国',
|
||||
province: formData.province,
|
||||
city: formData.city,
|
||||
region: formData.region,
|
||||
address: formData.address,
|
||||
isDefault: formData.isDefault ?? true,
|
||||
...(loc ? {lng: loc.lng, lat: loc.lat} : {}),
|
||||
}
|
||||
|
||||
if (isEditMode && addressId) {
|
||||
submitData.id = addressId
|
||||
}
|
||||
|
||||
// 执行新增或更新
|
||||
if (isEditMode) {
|
||||
await updateShopUserAddress(submitData)
|
||||
} else {
|
||||
await addShopUserAddress(submitData)
|
||||
}
|
||||
|
||||
Taro.showToast({title: `${isEditMode ? '更新' : '保存'}成功`, icon: 'success'})
|
||||
setTimeout(() => Taro.navigateBack(), 1000)
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
Taro.showToast({title: `${isEditMode ? '更新' : '保存'}失败`, icon: 'none'})
|
||||
}
|
||||
}
|
||||
|
||||
const updateField = (field: string, value: any) => {
|
||||
setFormData(prev => ({...prev, [field]: value}))
|
||||
}
|
||||
|
||||
/** 初始化加载 */
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
// 动态设置页面标题
|
||||
Taro.setNavigationBarTitle({
|
||||
title: isEditMode ? '编辑收货地址' : (fromWxMode ? '完善收货地址' : '新增收货地址'),
|
||||
})
|
||||
|
||||
// 微信地址导入
|
||||
if (!isEditMode && fromWxMode && !wxDraftPatchedRef.current) {
|
||||
try {
|
||||
const draft = Taro.getStorageSync('WxAddressDraft')
|
||||
if (draft) {
|
||||
wxDraftPatchedRef.current = true
|
||||
wxDraftRef.current = draft as any
|
||||
Taro.removeStorageSync('WxAddressDraft')
|
||||
setFormData(prev => ({...prev, ...(draft as any)}))
|
||||
const p = String((draft as any)?.province || '').trim()
|
||||
const c = String((draft as any)?.city || '').trim()
|
||||
const r = String((draft as any)?.region || '').trim()
|
||||
const rText = [p, c, r].filter(Boolean).join(' ')
|
||||
if (rText) setRegionText(rText)
|
||||
}
|
||||
} catch (_e) { /* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑模式:加载已有地址数据
|
||||
if (isEditMode && addressId) {
|
||||
try {
|
||||
const addr = await getShopUserAddress(addressId)
|
||||
setFormData(addr)
|
||||
setRegionText(`${addr.province || ''} ${addr.city || ''} ${addr.region || ''}`)
|
||||
if (hasValidLngLat(addr)) {
|
||||
setSelectedLocation({lng: String(addr.lng), lat: String(addr.lat)})
|
||||
setRegionLocked(true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载地址失败:', error)
|
||||
Taro.showToast({title: '加载地址失败', icon: 'none'})
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
init()
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="flex items-center justify-center min-h-screen">
|
||||
<Text className="text-gray-400 text-sm">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50 flex flex-col">
|
||||
<ScrollView scrollY className="flex-1">
|
||||
{/* 地址智能识别区 */}
|
||||
<View className="bg-white px-3 pt-3 pb-2">
|
||||
<View
|
||||
className="rounded-lg p-2 relative"
|
||||
style={{border: '1px dashed #0e932e'}}
|
||||
>
|
||||
<Textarea
|
||||
style={{height: '80px', width: '100%', fontSize: '13px'}}
|
||||
value={inputText}
|
||||
onInput={e => setInputText(e.detail.value)}
|
||||
placeholder="粘贴文本,点击「识别」自动识别收货人、地址、电话"
|
||||
maxlength={200}
|
||||
/>
|
||||
<View
|
||||
className="absolute right-2 bottom-2"
|
||||
style={{
|
||||
backgroundColor: '#0e932e',
|
||||
borderRadius: '12px',
|
||||
paddingLeft: '12px',
|
||||
paddingRight: '12px',
|
||||
paddingTop: '4px',
|
||||
paddingBottom: '4px'
|
||||
}}
|
||||
onClick={recognizeAddress}
|
||||
>
|
||||
<Text className="text-white text-xs">识别</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="bg-gray-100" style={{height: '8px'}}/>
|
||||
|
||||
{/* 选择定位 */}
|
||||
<View className="bg-white px-4 mb-2">
|
||||
<View className="flex items-center py-3" onClick={chooseGeoLocation}>
|
||||
<View className="flex-1">
|
||||
<View className="flex items-center gap-1 mb-1">
|
||||
<Text className="text-sm text-gray-700">选择定位</Text>
|
||||
<Text className="text-gray-300" style={{fontSize: '10px'}}>(推荐)</Text>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-400">
|
||||
{selectedLocation?.address || (selectedLocation ? `经度:${selectedLocation.lng}, 纬度:${selectedLocation.lat}` : '用于判断是否超出配送范围')}
|
||||
</Text>
|
||||
{selectedLocation?.name && (
|
||||
<Text className="text-xs text-green-600 mt-1">{selectedLocation.name}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className="text-gray-300 text-xs">▶</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 表单区 */}
|
||||
<View className="bg-white px-4 py-2">
|
||||
|
||||
{/* 所在地区 */}
|
||||
<View className="flex items-center py-3 border-b border-gray-50" onClick={openRegionPicker}>
|
||||
<Text className="text-sm text-gray-700" style={{minWidth: '60px'}}>所在地区</Text>
|
||||
<View className="flex-1 flex items-center justify-between">
|
||||
<Text className={`text-sm ${regionText ? 'text-gray-800' : 'text-gray-400'}`}>
|
||||
{regionText || '请选择省市区'}
|
||||
</Text>
|
||||
<Text className="text-gray-300 text-xs">▶</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 详细地址 */}
|
||||
<View className="flex items-center py-3 border-b border-gray-50">
|
||||
<Text className="text-sm text-gray-700" style={{minWidth: '60px'}}>详细地址</Text>
|
||||
<Input
|
||||
className="flex-1 text-sm"
|
||||
placeholder="请输入详细收货地址"
|
||||
maxLength={50}
|
||||
value={formData.address || ''}
|
||||
onInput={e => updateField('address', e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 收货人 */}
|
||||
<View className="flex items-center py-3 border-b border-gray-50">
|
||||
<Text className="text-sm text-gray-700" style={{minWidth: '60px'}}>收货人</Text>
|
||||
<Input
|
||||
className="flex-1 text-sm"
|
||||
placeholder="请输入收货人姓名"
|
||||
maxLength={10}
|
||||
value={formData.name || ''}
|
||||
onInput={e => updateField('name', e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 手机号 */}
|
||||
<View className="flex items-center py-3 border-b border-gray-50">
|
||||
<Text className="text-sm text-gray-700" style={{minWidth: '60px'}}>手机号</Text>
|
||||
<Input
|
||||
className="flex-1 text-sm"
|
||||
placeholder="请输入手机号"
|
||||
type="number"
|
||||
maxlength={11}
|
||||
value={formData.phone || ''}
|
||||
onInput={e => updateField('phone', e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
|
||||
{/* 默认地址开关 */}
|
||||
<View className="bg-white px-4 mt-2">
|
||||
<View className="flex items-center justify-between py-3">
|
||||
<Text className="text-sm text-gray-700">设为默认地址</Text>
|
||||
<View
|
||||
className={`rounded-full relative ${formData.isDefault ? 'bg-green-500' : 'bg-gray-300'}`}
|
||||
style={{width: '40px', height: '22px'}}
|
||||
onClick={() => updateField('isDefault', !formData.isDefault)}
|
||||
>
|
||||
<View
|
||||
className={`absolute top-1 rounded-full bg-white ${formData.isDefault ? 'right-1' : 'left-1'}`}
|
||||
style={{width: '16px', height: '16px'}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部占位 */}
|
||||
<View style={{height: '100px'}}/>
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部保存按钮 - 使用 flex 布局而非 fixed */}
|
||||
<View className="bg-white border-t border-gray-200 px-4 py-3" style={{paddingBottom: '20px'}}>
|
||||
<View
|
||||
className="py-3 rounded-full text-center"
|
||||
style={{backgroundColor: '#0e932e'}}
|
||||
onClick={handleSave}
|
||||
>
|
||||
<Text className="text-white font-medium text-sm">{isEditMode ? '更新地址' : '保存并使用'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 省市区选择器 */}
|
||||
<Address
|
||||
visible={regionPickerVisible}
|
||||
options={regionOptions}
|
||||
title="选择所在地区"
|
||||
onChange={(value) => handleRegionChange(value as string[])}
|
||||
onClose={() => setRegionPickerVisible(false)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressEditPage
|
||||
116
src_bak/pages/user/address-list.tsx
Normal file
116
src_bak/pages/user/address-list.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { useAddress } from '@/hooks/useAddress'
|
||||
import AddressCard from '@/components/business/AddressCard'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '收货地址',
|
||||
})
|
||||
|
||||
const AddressListPage: React.FC = () => {
|
||||
const { addresses, loading, loadAddresses, deleteAddress, setDefault } = useAddress()
|
||||
|
||||
// 判断是否从结算页进来(选择地址模式)
|
||||
const { from, select } = Taro.getCurrentInstance().router?.params || {}
|
||||
const isSelectMode = select === '1' || from === 'checkout'
|
||||
|
||||
useDidShow(() => {
|
||||
loadAddresses()
|
||||
})
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该地址吗?',
|
||||
confirmColor: '#0e932e',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const success = await deleteAddress(id)
|
||||
if (success) {
|
||||
Taro.showToast({ title: '删除成功', icon: 'success' })
|
||||
} else {
|
||||
Taro.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = (id: number) => {
|
||||
Taro.navigateTo({ url: `/pages/user/address-edit?id=${id}` })
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
Taro.navigateTo({ url: '/pages/user/address-edit' })
|
||||
}
|
||||
|
||||
/** 选择地址:设为默认后返回上一页(结算页) */
|
||||
const handleSelect = async (id: number) => {
|
||||
if (!isSelectMode) return
|
||||
|
||||
// 选择模式:设为默认地址后返回
|
||||
const addr = addresses.find(a => a.id === id)
|
||||
if (!addr) return
|
||||
|
||||
if (!addr.isDefault) {
|
||||
await setDefault(id)
|
||||
}
|
||||
|
||||
// 通知结算页刷新地址
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50 flex flex-col">
|
||||
{/* 地址列表区域 */}
|
||||
<ScrollView scrollY className="flex-1">
|
||||
<View className="p-3 pb-20">
|
||||
{loading ? (
|
||||
<View className="flex items-center justify-center py-20">
|
||||
<Text className="text-gray-400 text-sm">加载中...</Text>
|
||||
</View>
|
||||
) : addresses.length === 0 ? (
|
||||
<View className="pt-20">
|
||||
<EmptyState
|
||||
text="暂无收货地址"
|
||||
actionText="添加地址"
|
||||
onAction={handleAdd}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
addresses.map(addr => (
|
||||
<AddressCard
|
||||
key={addr.id}
|
||||
address={addr}
|
||||
selectMode={isSelectMode}
|
||||
selected={!!addr.isDefault}
|
||||
onClick={() => isSelectMode ? handleSelect(addr.id!) : handleEdit(addr.id!)}
|
||||
showActions
|
||||
onEdit={() => handleEdit(addr.id!)}
|
||||
onDelete={() => handleDelete(addr.id!)}
|
||||
onDefault={() => setDefault(addr.id!)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部固定按钮 */}
|
||||
<View className="bg-white border-t border-gray-200 px-4 py-3" style={{ paddingBottom: '20px' }}>
|
||||
<View
|
||||
className="py-3 rounded-full text-center"
|
||||
style={{ backgroundColor: '#0e932e' }}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
<Text className="text-white font-medium text-sm">新增收货地址</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressListPage
|
||||
169
src_bak/pages/user/balance-log/index.tsx
Normal file
169
src_bak/pages/user/balance-log/index.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useRequest } from '@/hooks/useRequest'
|
||||
import { pageUserBalanceLog, type UserBalanceLog } from '@/api/system/user/balance-log'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '余额明细',
|
||||
})
|
||||
|
||||
// 场景类型映射
|
||||
const SCENE_MAP: Record<number, string> = {
|
||||
0: '充值',
|
||||
1: '消费',
|
||||
2: '退款',
|
||||
3: '提现',
|
||||
4: '收入',
|
||||
5: '支出',
|
||||
6: '转账',
|
||||
7: '收款',
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ label: '全部', value: -1 },
|
||||
{ label: '收入', value: 1 },
|
||||
{ label: '支出', value: 0 },
|
||||
]
|
||||
|
||||
const BalanceLogPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState(-1)
|
||||
const [page, setPage] = useState(1)
|
||||
const [logs, setLogs] = useState<UserBalanceLog[]>([])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
|
||||
// 获取余额日志
|
||||
const { run: fetchLogs, loading } = useRequest(pageUserBalanceLog, {
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
if (data?.list) {
|
||||
if (page === 1) {
|
||||
setLogs(data.list)
|
||||
} else {
|
||||
setLogs(prev => [...prev, ...data.list])
|
||||
}
|
||||
setHasMore(data.list.length >= 20)
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
Taro.showToast({ title: err.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadData = (pageNum: number = 1) => {
|
||||
const params: any = { page: pageNum, limit: 20 }
|
||||
if (activeTab === 1) {
|
||||
params.moneyGt = 0 // 收入
|
||||
} else if (activeTab === 0) {
|
||||
params.moneyLt = 0 // 支出
|
||||
}
|
||||
fetchLogs(params)
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
loadData(1)
|
||||
}, [activeTab])
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (loading || !hasMore) return
|
||||
const nextPage = page + 1
|
||||
setPage(nextPage)
|
||||
loadData(nextPage)
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (money?: string) => {
|
||||
if (!money) return '0.00'
|
||||
const num = parseFloat(money)
|
||||
return num > 0 ? `+${num.toFixed(2)}` : num.toFixed(2)
|
||||
}
|
||||
|
||||
// 获取场景描述
|
||||
const getSceneText = (scene?: number) => {
|
||||
return SCENE_MAP[scene || 0] || '其他'
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
{/* Tab 栏 */}
|
||||
<View className='bg-white flex'>
|
||||
{tabs.map(tab => (
|
||||
<View
|
||||
key={tab.value}
|
||||
className={`flex-1 text-center py-3 relative ${
|
||||
activeTab === tab.value ? 'text-orange-500 font-medium' : 'text-gray-600'
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab.value)}
|
||||
>
|
||||
<Text className='text-sm'>{tab.label}</Text>
|
||||
{activeTab === tab.value && (
|
||||
<View className='absolute bottom-0 left-0 right-0 flex justify-center'>
|
||||
<View className='w-8 h-px bg-orange-500 rounded' />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 日志列表 */}
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='flex-1'
|
||||
onScrollToLower={loadMore}
|
||||
>
|
||||
{logs.length === 0 && !loading ? (
|
||||
<View className='text-center py-16'>
|
||||
<Text className='text-sm text-gray-400'>暂无余额明细</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='p-3'>
|
||||
{logs.map(log => (
|
||||
<View key={log.logId} className='bg-white rounded-lg p-4 mb-2'>
|
||||
<View className='flex justify-between items-center'>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm text-gray-800 block'>
|
||||
{log.describe || getSceneText(log.scene)}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-400 mt-0 block'>{log.createTime}</Text>
|
||||
</View>
|
||||
<Text className={`text-base font-bold ${
|
||||
parseFloat(log.money || '0') > 0 ? 'text-green-600' : 'text-red-500'
|
||||
}`}>
|
||||
{formatMoney(log.money)}
|
||||
</Text>
|
||||
</View>
|
||||
{log.balance !== undefined && (
|
||||
<View className='mt-2 pt-2 border-t border-gray-50'>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
余额: ¥{parseFloat(String(log.balance)).toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* 加载更多 */}
|
||||
{hasMore && (
|
||||
<View className='text-center py-4'>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
{loading ? '加载中...' : '上拉加载更多'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{!hasMore && logs.length > 0 && (
|
||||
<View className='text-center py-4'>
|
||||
<Text className='text-xs text-gray-400'>没有更多了</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default BalanceLogPage
|
||||
200
src_bak/pages/user/benefit-exchange-confirm/index.scss
Normal file
200
src_bak/pages/user/benefit-exchange-confirm/index.scss
Normal file
@@ -0,0 +1,200 @@
|
||||
.confirm-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
/* 地址卡片 */
|
||||
.address-card {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
margin: 24rpx 24rpx 0;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 48rpx 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.addr-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.addr-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.addr-phone {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.addr-detail {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.addr-empty {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #ff6b35;
|
||||
text-align: center;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.addr-arrow {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 28rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* 权益包信息 */
|
||||
.pkg-info-card {
|
||||
background: #fff;
|
||||
margin: 16rpx 24rpx 0;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pkg-img {
|
||||
width: 200rpx;
|
||||
height: 180rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pkg-detail {
|
||||
flex: 1;
|
||||
padding: 20rpx 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pkg-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.pkg-desc {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.pkg-cost-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.cost-item {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cost-item.points { color: #ff6b35; }
|
||||
.cost-item.balance { color: #ff6b35; }
|
||||
.cost-item.free { color: #07c160; }
|
||||
|
||||
/* 须知 */
|
||||
.notice-card {
|
||||
background: #fff;
|
||||
margin: 16rpx 24rpx 0;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.notice-item {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 我的资产 */
|
||||
.assets-card {
|
||||
background: #fff;
|
||||
margin: 16rpx 24rpx 0;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.assets-title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.assets-row {
|
||||
display: flex;
|
||||
gap: 48rpx;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.asset-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #ff6b35;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.asset-label {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
padding: 28rpx 0;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff9a56 100%);
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.confirm-btn.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
178
src_bak/pages/user/benefit-exchange-confirm/index.tsx
Normal file
178
src_bak/pages/user/benefit-exchange-confirm/index.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { View, Text, Image } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getBenefitPackageDetail, exchangeBenefitPackage } from '@/api/shop/shopMemberBenefit';
|
||||
import { getDefaultAddress } from '@/api/shop/shopUserAddress';
|
||||
import { getUserBalance } from '@/api/system/user/balance';
|
||||
import './index.scss';
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '确认兑换',
|
||||
});
|
||||
|
||||
export default function BenefitExchangeConfirmPage() {
|
||||
const router = useRouter();
|
||||
const pkgId = Number(router.params.id);
|
||||
|
||||
const [pkg, setPkg] = useState<any>(null);
|
||||
const [address, setAddress] = useState<any>(null);
|
||||
const [userBalance, setUserBalance] = useState<string>('--');
|
||||
const [userPoints, setUserPoints] = useState<string>('--');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadPackage();
|
||||
loadAddress();
|
||||
loadUserBalance();
|
||||
}, []);
|
||||
|
||||
const loadPackage = async () => {
|
||||
const res = await getBenefitPackageDetail(pkgId);
|
||||
if (res.code === 200) setPkg(res.data);
|
||||
};
|
||||
|
||||
const loadAddress = async () => {
|
||||
const res = await getDefaultAddress();
|
||||
if (res.code === 200 && res.data) setAddress(res.data);
|
||||
};
|
||||
|
||||
const loadUserBalance = async () => {
|
||||
try {
|
||||
const data = await getUserBalance();
|
||||
setUserBalance(data.balance || '0');
|
||||
setUserPoints(String(data.points || 0));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAddress = () => {
|
||||
Taro.navigateTo({ url: '/pages/user/address-list?select=1' });
|
||||
};
|
||||
|
||||
const handleExchange = async () => {
|
||||
if (!pkg) return;
|
||||
|
||||
if (pkg.needShip && !address) {
|
||||
Taro.showToast({ title: '请先选择收货地址', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
Taro.showModal({
|
||||
title: '确认兑换',
|
||||
content: `确认兑换「${pkg.name}」?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res2 = await exchangeBenefitPackage({
|
||||
packageId: pkgId,
|
||||
addressId: address?.id,
|
||||
});
|
||||
if (res2.code === 200) {
|
||||
Taro.showToast({ title: '兑换申请成功', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
Taro.redirectTo({ url: '/pages/user/benefit-exchange-list/index' });
|
||||
}, 1500);
|
||||
} else {
|
||||
Taro.showToast({ title: res2.message || '兑换失败', icon: 'none' });
|
||||
}
|
||||
} catch (e) {
|
||||
Taro.showToast({ title: '网络错误', icon: 'none' });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!pkg) return null;
|
||||
|
||||
return (
|
||||
<View className="confirm-page">
|
||||
{/* 收货地址 */}
|
||||
{pkg.needShip === 1 && (
|
||||
<View className="address-card" onClick={handleSelectAddress}>
|
||||
{address ? (
|
||||
<>
|
||||
<View className="addr-info">
|
||||
<Text className="addr-name">{address.name}</Text>
|
||||
<Text className="addr-phone">{address.phone}</Text>
|
||||
</View>
|
||||
<Text className="addr-detail">
|
||||
{address.province}{address.city}{address.district}{address.detail}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text className="addr-empty">+ 请选择收货地址</Text>
|
||||
)}
|
||||
<Text className="addr-arrow">></Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 权益包信息 */}
|
||||
<View className="pkg-info-card">
|
||||
{pkg.coverImage && (
|
||||
<Image className="pkg-img" src={pkg.coverImage} mode="aspectFill" />
|
||||
)}
|
||||
<View className="pkg-detail">
|
||||
<Text className="pkg-name">{pkg.name}</Text>
|
||||
{pkg.description && (
|
||||
<Text className="pkg-desc">{pkg.description}</Text>
|
||||
)}
|
||||
<View className="pkg-cost-row">
|
||||
{pkg.pointsCost > 0 && (
|
||||
<Text className="cost-item points">{pkg.pointsCost} 积分</Text>
|
||||
)}
|
||||
{pkg.balanceCost > 0 && (
|
||||
<Text className="cost-item balance">¥{pkg.balanceCost}</Text>
|
||||
)}
|
||||
{!pkg.pointsCost && !pkg.balanceCost && (
|
||||
<Text className="cost-item free">免费兑换</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 我的资产 */}
|
||||
<View className="assets-card">
|
||||
<Text className="assets-title">我的资产</Text>
|
||||
<View className="assets-row">
|
||||
{pkg.pointsCost > 0 && (
|
||||
<View className="asset-item">
|
||||
<Text className="asset-value">{userPoints}</Text>
|
||||
<Text className="asset-label">当前积分</Text>
|
||||
</View>
|
||||
)}
|
||||
{pkg.balanceCost > 0 && (
|
||||
<View className="asset-item">
|
||||
<Text className="asset-value">{userBalance}</Text>
|
||||
<Text className="asset-label">当前余额</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 须知 */}
|
||||
<View className="notice-card">
|
||||
<Text className="notice-title">兑换须知</Text>
|
||||
<Text className="notice-item">· 兑换后平台将安排配送,请耐心等待</Text>
|
||||
<Text className="notice-item">· 仅待处理状态可申请取消</Text>
|
||||
<Text className="notice-item">· 如有问题请联系客服</Text>
|
||||
{pkg.validDays && (
|
||||
<Text className="notice-item">· 权益有效期:{pkg.validDays}天</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="bottom-bar">
|
||||
<View
|
||||
className={`confirm-btn ${submitting ? 'disabled' : ''}`}
|
||||
onClick={!submitting ? handleExchange : undefined}
|
||||
>
|
||||
{submitting ? '提交中...' : '确认兑换'}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
158
src_bak/pages/user/benefit-exchange-list/index.scss
Normal file
158
src_bak/pages/user/benefit-exchange-list/index.scss
Normal file
@@ -0,0 +1,158 @@
|
||||
.exchange-list-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #ff6b35;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background: #ff6b35;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.list-scroll {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.exchange-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.pkg-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.pending { color: #fa8c16; background: rgba(250,140,22,.1); }
|
||||
.shipped { color: #1890ff; background: rgba(24,144,255,.1); }
|
||||
.done { color: #07c160; background: rgba(7,193,96,.1); }
|
||||
.cancelled { color: #999; background: rgba(153,153,153,.1); }
|
||||
|
||||
.cost-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.cost-text {
|
||||
font-size: 24rpx;
|
||||
color: #ff6b35;
|
||||
}
|
||||
|
||||
.address-row {
|
||||
display: flex;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.addr-label {
|
||||
flex-shrink: 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.addr-val {
|
||||
flex: 1;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.express-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.express-label {
|
||||
flex-shrink: 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.express-val {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
color: #1890ff;
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border: 1rpx solid #1890ff;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12rpx;
|
||||
padding-top: 12rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.create-time {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
padding: 6rpx 20rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.loading-tip {
|
||||
text-align: center;
|
||||
padding: 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
170
src_bak/pages/user/benefit-exchange-list/index.tsx
Normal file
170
src_bak/pages/user/benefit-exchange-list/index.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getMyExchangeList, cancelExchange } from '@/api/shop/shopMemberBenefit';
|
||||
import EmptyState from '@/components/common/EmptyState';
|
||||
import './index.scss';
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '兑换记录',
|
||||
});
|
||||
|
||||
const TABS = [
|
||||
{ label: '全部', value: -1 },
|
||||
{ label: '待处理', value: 0 },
|
||||
{ label: '已发货', value: 1 },
|
||||
{ label: '已完成', value: 2 },
|
||||
];
|
||||
|
||||
const STATUS_TEXT: Record<number, string> = {
|
||||
0: '待处理',
|
||||
1: '已发货',
|
||||
2: '已完成',
|
||||
3: '已取消',
|
||||
};
|
||||
|
||||
const STATUS_CLASS: Record<number, string> = {
|
||||
0: 'pending',
|
||||
1: 'shipped',
|
||||
2: 'done',
|
||||
3: 'cancelled',
|
||||
};
|
||||
|
||||
export default function BenefitExchangeListPage() {
|
||||
const [activeTab, setActiveTab] = useState(-1);
|
||||
const [list, setList] = useState<any[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadList(true);
|
||||
}, [activeTab]);
|
||||
|
||||
const loadList = async (isRefresh = false) => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
const currentPage = isRefresh ? 1 : page;
|
||||
try {
|
||||
const params: any = { page: currentPage, limit: 20 };
|
||||
if (activeTab >= 0) params.status = activeTab;
|
||||
const res = await getMyExchangeList(params);
|
||||
if (res.code === 200) {
|
||||
const newList = res.data.list || [];
|
||||
setList(isRefresh ? newList : [...list, ...newList]);
|
||||
setPage(currentPage + 1);
|
||||
setHasMore(newList.length >= 20);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = (item: any) => {
|
||||
Taro.showModal({
|
||||
title: '取消兑换',
|
||||
content: '确认取消该兑换申请吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return;
|
||||
const res2 = await cancelExchange(item.id);
|
||||
if (res2.code === 200) {
|
||||
Taro.showToast({ title: '已取消', icon: 'success' });
|
||||
loadList(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleTrack = (item: any) => {
|
||||
if (item.expressNo) {
|
||||
Taro.setClipboardData({ data: item.expressNo });
|
||||
Taro.showToast({ title: '快递单号已复制', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="exchange-list-page">
|
||||
{/* 标签栏 */}
|
||||
<View className="tab-bar">
|
||||
{TABS.map((tab) => (
|
||||
<View
|
||||
key={tab.value}
|
||||
className={`tab-item ${activeTab === tab.value ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(tab.value)}
|
||||
>
|
||||
{tab.label}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
className="list-scroll"
|
||||
scrollY
|
||||
onScrollToLower={() => hasMore && loadList()}
|
||||
>
|
||||
{!loading && list.length === 0 && (
|
||||
<EmptyState description="暂无兑换记录" />
|
||||
)}
|
||||
|
||||
{list.map((item) => (
|
||||
<View key={item.id} className="exchange-card">
|
||||
<View className="card-header">
|
||||
<Text className="pkg-name">{item.packageName}</Text>
|
||||
<Text className={`status-tag ${STATUS_CLASS[item.status]}`}>
|
||||
{STATUS_TEXT[item.status]}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="cost-row">
|
||||
{item.pointsCost > 0 && (
|
||||
<Text className="cost-text">消耗 {item.pointsCost} 积分</Text>
|
||||
)}
|
||||
{item.balanceCost > 0 && (
|
||||
<Text className="cost-text">消耗 ¥{item.balanceCost}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{item.addressSnapshot && (
|
||||
<View className="address-row">
|
||||
<Text className="addr-label">收货地址:</Text>
|
||||
<Text className="addr-val">
|
||||
{(() => {
|
||||
try {
|
||||
const addr = JSON.parse(item.addressSnapshot);
|
||||
return `${addr.name} ${addr.phone} ${addr.province}${addr.city}${addr.district}${addr.detail}`;
|
||||
} catch {
|
||||
return item.addressSnapshot;
|
||||
}
|
||||
})()}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{item.status === 1 && item.expressNo && (
|
||||
<View className="express-row" onClick={() => handleTrack(item)}>
|
||||
<Text className="express-label">快递:</Text>
|
||||
<Text className="express-val">
|
||||
{item.expressCompany} {item.expressNo}
|
||||
</Text>
|
||||
<Text className="copy-btn">复制</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="card-footer">
|
||||
<Text className="create-time">{item.createTime}</Text>
|
||||
{item.status === 0 && (
|
||||
<View className="cancel-btn" onClick={() => handleCancel(item)}>
|
||||
取消兑换
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{loading && (
|
||||
<View className="loading-tip">加载中...</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
136
src_bak/pages/user/benefit-packages/index.scss
Normal file
136
src_bak/pages/user/benefit-packages/index.scss
Normal file
@@ -0,0 +1,136 @@
|
||||
.benefit-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.top-banner {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff9a56 100%);
|
||||
padding: 32rpx 32rpx 40rpx;
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.banner-sub {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255,255,255,0.85);
|
||||
}
|
||||
|
||||
.history-link {
|
||||
position: absolute;
|
||||
right: 32rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 24rpx;
|
||||
color: rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
.pkg-list {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.pkg-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pkg-cover {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pkg-body {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.pkg-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.pkg-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pkg-tag {
|
||||
font-size: 20rpx;
|
||||
color: #ff6b35;
|
||||
background: rgba(255, 107, 53, 0.1);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.pkg-desc {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.pkg-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.pkg-cost {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.cost-points {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #ff6b35;
|
||||
}
|
||||
|
||||
.cost-balance {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #ff6b35;
|
||||
}
|
||||
|
||||
.cost-free {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.pkg-stock {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.exchange-btn {
|
||||
padding: 16rpx 32rpx;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff9a56 100%);
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.exchange-btn.disabled {
|
||||
background: #ccc;
|
||||
}
|
||||
97
src_bak/pages/user/benefit-packages/index.tsx
Normal file
97
src_bak/pages/user/benefit-packages/index.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getBenefitPackageList } from '@/api/shop/shopMemberBenefit';
|
||||
import EmptyState from '@/components/common/EmptyState';
|
||||
import './index.scss';
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '会员权益包',
|
||||
});
|
||||
|
||||
export default function BenefitPackagesPage() {
|
||||
const [list, setList] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadList();
|
||||
}, []);
|
||||
|
||||
const loadList = async () => {
|
||||
try {
|
||||
const res = await getBenefitPackageList();
|
||||
if (res.code === 200) setList(res.data || []);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExchange = (pkg: any) => {
|
||||
Taro.navigateTo({
|
||||
url: `/pages/user/benefit-exchange-confirm/index?id=${pkg.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleHistory = () => {
|
||||
Taro.navigateTo({ url: '/pages/user/benefit-exchange-list/index' });
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="benefit-page">
|
||||
{/* 顶部提示 */}
|
||||
<View className="top-banner">
|
||||
<Text className="banner-title">专属会员权益</Text>
|
||||
<Text className="banner-sub">成为会员,即可申请兑换以下权益包</Text>
|
||||
<Text className="history-link" onClick={handleHistory}>
|
||||
兑换记录 >
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView className="pkg-list" scrollY>
|
||||
{!loading && list.length === 0 && (
|
||||
<EmptyState description="暂无可用权益包" />
|
||||
)}
|
||||
{list.map((pkg) => (
|
||||
<View key={pkg.id} className="pkg-card">
|
||||
{pkg.coverImage && (
|
||||
<Image className="pkg-cover" src={pkg.coverImage} mode="aspectFill" />
|
||||
)}
|
||||
<View className="pkg-body">
|
||||
<View className="pkg-header">
|
||||
<Text className="pkg-name">{pkg.name}</Text>
|
||||
{pkg.tag && <Text className="pkg-tag">{pkg.tag}</Text>}
|
||||
</View>
|
||||
{pkg.description && (
|
||||
<Text className="pkg-desc">{pkg.description}</Text>
|
||||
)}
|
||||
<View className="pkg-footer">
|
||||
<View className="pkg-cost">
|
||||
{pkg.pointsCost > 0 && (
|
||||
<Text className="cost-points">{pkg.pointsCost}积分</Text>
|
||||
)}
|
||||
{pkg.balanceCost > 0 && (
|
||||
<Text className="cost-balance">¥{pkg.balanceCost}</Text>
|
||||
)}
|
||||
{pkg.pointsCost === 0 && pkg.balanceCost === 0 && (
|
||||
<Text className="cost-free">免费</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className="pkg-stock">
|
||||
{pkg.stock === -1
|
||||
? '不限库存'
|
||||
: `剩余 ${pkg.stock} 件`}
|
||||
</View>
|
||||
<View
|
||||
className={`exchange-btn ${pkg.stock === 0 ? 'disabled' : ''}`}
|
||||
onClick={() => pkg.stock !== 0 && handleExchange(pkg)}
|
||||
>
|
||||
{pkg.stock === 0 ? '已兑完' : '立即兑换'}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
3
src_bak/pages/user/commission/index.config.ts
Normal file
3
src_bak/pages/user/commission/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '佣金明细',
|
||||
}
|
||||
159
src_bak/pages/user/commission/index.scss
Normal file
159
src_bak/pages/user/commission/index.scss
Normal file
@@ -0,0 +1,159 @@
|
||||
.commission-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
margin: 24rpx 24rpx 0;
|
||||
padding: 32rpx;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff9a56 100%);
|
||||
border-radius: 16rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stats-value.pending {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stats-value.settled {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.withdraw-btn {
|
||||
position: absolute;
|
||||
right: 32rpx;
|
||||
top: 32rpx;
|
||||
padding: 12rpx 32rpx;
|
||||
background: #fff;
|
||||
color: #ff6b35;
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
margin-top: 24rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #ff6b35;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 48rpx;
|
||||
height: 4rpx;
|
||||
background: #ff6b35;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.commission-list {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.commission-item {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.commission-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.commission-order {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.commission-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #ff6b35;
|
||||
}
|
||||
|
||||
.commission-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.status-0 {
|
||||
color: #ff9a56;
|
||||
background: rgba(255, 154, 86, 0.1);
|
||||
}
|
||||
|
||||
.status-1 {
|
||||
color: #07c160;
|
||||
background: rgba(7, 193, 96, 0.1);
|
||||
}
|
||||
|
||||
.status-2 {
|
||||
color: #999;
|
||||
background: rgba(153, 153, 153, 0.1);
|
||||
}
|
||||
|
||||
.settle-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
151
src_bak/pages/user/commission/index.tsx
Normal file
151
src_bak/pages/user/commission/index.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getCommissionStats, getMyCommissionList } from '@/api/shop/shopCommissionRecord';
|
||||
import EmptyState from '@/components/common/EmptyState';
|
||||
import { Loading } from '@nutui/nutui-react-taro';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '我的佣金',
|
||||
});
|
||||
|
||||
export default function CommissionPage() {
|
||||
const [stats, setStats] = useState<any>({});
|
||||
const [list, setList] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState(0); // 0待结算/1已结算
|
||||
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
loadList(true);
|
||||
}, []);
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const res = await getCommissionStats();
|
||||
if (res.code === 200) {
|
||||
setStats(res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadList = async (isRefresh = false) => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const currentPage = isRefresh ? 1 : page;
|
||||
const res = await getMyCommissionList({
|
||||
status: activeTab,
|
||||
page: currentPage,
|
||||
limit: 20
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
const newList = res.data.list || [];
|
||||
setList(isRefresh ? newList : [...list, ...newList]);
|
||||
setPage(currentPage + 1);
|
||||
setHasMore(newList.length >= 20);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载列表失败', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (tab: number) => {
|
||||
setActiveTab(tab);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
loadList(true);
|
||||
};
|
||||
|
||||
const handleWithdraw = () => {
|
||||
Taro.navigateTo({ url: '/pages/user/withdraw/index' });
|
||||
};
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
const map: Record<number, string> = {
|
||||
0: '待结算',
|
||||
1: '已结算',
|
||||
2: '已失效'
|
||||
};
|
||||
return map[status] || '未知';
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="commission-page">
|
||||
{/* 统计区域 */}
|
||||
<View className="stats-card">
|
||||
<View className="stats-row">
|
||||
<View className="stats-item">
|
||||
<Text className="stats-label">待结算佣金(元)</Text>
|
||||
<Text className="stats-value pending">{stats.pendingTotal || '0.00'}</Text>
|
||||
</View>
|
||||
<View className="stats-item">
|
||||
<Text className="stats-label">已结算佣金(元)</Text>
|
||||
<Text className="stats-value settled">{stats.settledTotal || '0.00'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="withdraw-btn" onClick={handleWithdraw}>
|
||||
去提现
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 标签页 */}
|
||||
<View className="tab-bar">
|
||||
<View
|
||||
className={`tab-item ${activeTab === 0 ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(0)}
|
||||
>
|
||||
待结算({stats.pendingCount || 0})
|
||||
</View>
|
||||
<View
|
||||
className={`tab-item ${activeTab === 1 ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(1)}
|
||||
>
|
||||
已结算({stats.settledCount || 0})
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 列表 */}
|
||||
<ScrollView
|
||||
className="commission-list"
|
||||
scrollY
|
||||
onScrollToLower={() => hasMore && loadList()}
|
||||
>
|
||||
{list.length === 0 && !loading ? (
|
||||
<EmptyState description="暂无佣金记录" />
|
||||
) : (
|
||||
list.map((item) => (
|
||||
<View key={item.id} className="commission-item">
|
||||
<View className="commission-info">
|
||||
<Text className="commission-order">订单号: {item.orderNo}</Text>
|
||||
<Text className="commission-amount">+{item.commissionAmount}</Text>
|
||||
</View>
|
||||
<View className="commission-meta">
|
||||
<Text className="meta-text">订单金额: ¥{item.orderAmount}</Text>
|
||||
<Text className={`status-tag status-${item.status}`}>
|
||||
{getStatusText(item.status)}
|
||||
</Text>
|
||||
</View>
|
||||
{item.settleTime && (
|
||||
<Text className="settle-time">
|
||||
结算时间: {item.settleTime}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
{loading && <Loading>加载中...</Loading>}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
259
src_bak/pages/user/coupon-list.tsx
Normal file
259
src_bak/pages/user/coupon-list.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { pageShopUserCoupon, removeShopUserCoupon } from '@/api/shop/shopUserCoupon'
|
||||
import type { ShopUserCoupon, ShopUserCouponParam } from '@/api/shop/shopUserCoupon/model'
|
||||
import CouponCard from '@/components/business/CouponCard'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '我的优惠券',
|
||||
})
|
||||
|
||||
// Tab 配置
|
||||
type CouponTab = 'available' | 'used' | 'expired'
|
||||
|
||||
const tabList: { title: string; key: CouponTab }[] = [
|
||||
{ title: '未使用', key: 'available' },
|
||||
{ title: '已使用', key: 'used' },
|
||||
{ title: '已过期', key: 'expired' },
|
||||
]
|
||||
|
||||
interface TabState {
|
||||
coupons: ShopUserCoupon[]
|
||||
page: number
|
||||
finished: boolean
|
||||
loading: boolean
|
||||
initialized: boolean
|
||||
}
|
||||
|
||||
const CouponListPage: React.FC = () => {
|
||||
// 根据 URL 参数初始化 tab
|
||||
const { tab: initTab } = Taro.getCurrentInstance().router?.params || {}
|
||||
const getInitTabIndex = () => {
|
||||
switch (initTab) {
|
||||
case 'used': return 1
|
||||
case 'expired': return 2
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
const [tabIndex, setTabIndex] = useState(getInitTabIndex())
|
||||
|
||||
// 每个 tab 独立维护状态
|
||||
const [tabStates, setTabStates] = useState<TabState[]>(
|
||||
tabList.map(() => ({
|
||||
coupons: [],
|
||||
page: 1,
|
||||
finished: false,
|
||||
loading: false,
|
||||
initialized: false,
|
||||
}))
|
||||
)
|
||||
|
||||
const currentState = tabStates[tabIndex]
|
||||
|
||||
const updateTabState = useCallback(
|
||||
(index: number, partial: Partial<TabState>) => {
|
||||
setTabStates(prev => {
|
||||
const next = [...prev]
|
||||
next[index] = { ...next[index], ...partial }
|
||||
return next
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const loadCoupons = useCallback(
|
||||
async (targetIndex: number, p: number) => {
|
||||
const state = tabStates[targetIndex]
|
||||
if (state.loading) return
|
||||
|
||||
updateTabState(targetIndex, { loading: true })
|
||||
|
||||
try {
|
||||
const tabKey = tabList[targetIndex].key
|
||||
const params: ShopUserCouponParam = { page: p, limit: 10 }
|
||||
|
||||
// 根据 tab 设置筛选条件
|
||||
// status: 0=未使用, 1=已使用, 2=已过期
|
||||
switch (tabKey) {
|
||||
case 'available':
|
||||
params.status = 0
|
||||
break
|
||||
case 'used':
|
||||
params.status = 1
|
||||
break
|
||||
case 'expired':
|
||||
params.status = 2
|
||||
break
|
||||
}
|
||||
|
||||
const res = await pageShopUserCoupon(params)
|
||||
const list = res?.list || []
|
||||
|
||||
updateTabState(targetIndex, {
|
||||
coupons: p === 1 ? list : [...state.coupons, ...list],
|
||||
page: p,
|
||||
finished: list.length < 10,
|
||||
loading: false,
|
||||
initialized: true,
|
||||
})
|
||||
} catch {
|
||||
updateTabState(targetIndex, { loading: false, initialized: true })
|
||||
}
|
||||
},
|
||||
[tabStates, updateTabState]
|
||||
)
|
||||
|
||||
// 切换 tab 时加载数据
|
||||
useEffect(() => {
|
||||
if (!tabStates[tabIndex].initialized) {
|
||||
loadCoupons(tabIndex, 1)
|
||||
}
|
||||
}, [tabIndex])
|
||||
|
||||
// 加载更多
|
||||
const handleLoadMore = () => {
|
||||
if (!currentState.finished && !currentState.loading) {
|
||||
loadCoupons(tabIndex, currentState.page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换 tab
|
||||
const handleTabChange = (index: number) => {
|
||||
if (index !== tabIndex) {
|
||||
setTabIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除优惠券
|
||||
const handleDelete = (id: string) => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该优惠券吗?',
|
||||
confirmColor: '#0e932e',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await removeShopUserCoupon(Number(id))
|
||||
// 从当前 tab 移除
|
||||
updateTabState(tabIndex, {
|
||||
coupons: tabStates[tabIndex].coupons.filter(c => c.id !== id)
|
||||
})
|
||||
Taro.showToast({ title: '删除成功', icon: 'success' })
|
||||
} catch {
|
||||
Taro.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取空状态文本
|
||||
const getEmptyText = () => {
|
||||
switch (tabList[tabIndex].key) {
|
||||
case 'available': return '暂无可用优惠券'
|
||||
case 'used': return '暂无已使用优惠券'
|
||||
case 'expired': return '暂无已过期优惠券'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取空状态操作
|
||||
const getEmptyAction = () => {
|
||||
if (tabList[tabIndex].key === 'available') {
|
||||
return {
|
||||
text: '去领取',
|
||||
action: () => Taro.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 判断是否禁用
|
||||
const isDisabled = () => {
|
||||
const key = tabList[tabIndex].key
|
||||
return key === 'used' || key === 'expired'
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex flex-col'>
|
||||
{/* 自定义 Tabs 标题栏 */}
|
||||
<View className='bg-white flex flex-row items-center border-b border-gray-100'>
|
||||
{tabList.map((tab, index) => (
|
||||
<View
|
||||
key={tab.key}
|
||||
className='flex-1 flex flex-col items-center justify-center py-3'
|
||||
onClick={() => handleTabChange(index)}
|
||||
>
|
||||
<Text
|
||||
className='text-sm font-medium'
|
||||
style={{
|
||||
color: index === tabIndex ? '#ee0a24' : '#666',
|
||||
}}
|
||||
>
|
||||
{tab.title}
|
||||
</Text>
|
||||
{index === tabIndex && (
|
||||
<View
|
||||
className='mt-1 rounded-full'
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '3px',
|
||||
backgroundColor: '#ee0a24',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 内容区域 - 每个 tab 独立渲染 */}
|
||||
<View className='flex-1 relative'>
|
||||
{tabList.map((tab, idx) => (
|
||||
<View
|
||||
key={tab.key}
|
||||
className='absolute inset-0'
|
||||
style={{
|
||||
display: idx === tabIndex ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='flex-1'
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={100}
|
||||
>
|
||||
<View className='p-3'>
|
||||
{tabStates[idx].coupons.length > 0 ? (
|
||||
tabStates[idx].coupons.map(coupon => (
|
||||
<CouponCard
|
||||
key={coupon.id}
|
||||
coupon={coupon}
|
||||
disabled={isDisabled()}
|
||||
showDelete={isDisabled()}
|
||||
onDelete={() => handleDelete(coupon.id!)}
|
||||
/>
|
||||
))
|
||||
) : tabStates[idx].initialized && !tabStates[idx].loading ? (
|
||||
<EmptyState
|
||||
text={getEmptyText()}
|
||||
actionText={getEmptyAction()?.text}
|
||||
onAction={getEmptyAction()?.action}
|
||||
/>
|
||||
) : null}
|
||||
<LoadMore
|
||||
loading={tabStates[idx].loading}
|
||||
finished={tabStates[idx].finished}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponListPage
|
||||
3
src_bak/pages/user/customer-service/index.config.ts
Normal file
3
src_bak/pages/user/customer-service/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '在线客服',
|
||||
}
|
||||
406
src_bak/pages/user/customer-service/index.tsx
Normal file
406
src_bak/pages/user/customer-service/index.tsx
Normal file
@@ -0,0 +1,406 @@
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react'
|
||||
import { View, Text, Button, Input, ScrollView } from '@tarojs/components'
|
||||
import Taro, { useReachBottom } from '@tarojs/taro'
|
||||
import NavBar from '@/components/NavBar'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import { pageShopChatConversation, addShopChatConversation } from '@/api/shop/shopChatConversation'
|
||||
import { pageShopChatMessage, addShopChatMessage } from '@/api/shop/shopChatMessage'
|
||||
import type { ShopChatConversation, ShopChatConversationParam } from '@/api/shop/shopChatConversation/model'
|
||||
import type { ShopChatMessage } from '@/api/shop/shopChatMessage/model'
|
||||
import type { PageResult } from '@/api'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '在线客服',
|
||||
})
|
||||
|
||||
const SERVICE_ONLINE_HOURS = '9:00-21:00'
|
||||
const SERVICE_HOTLINE = '400-888-8888'
|
||||
const SERVICE_WECHAT = 'paopao_service'
|
||||
|
||||
const CustomerServicePage: React.FC = () => {
|
||||
const [conversations, setConversations] = useState<ShopChatConversation[]>([])
|
||||
const [currentConversation, setCurrentConversation] = useState<ShopChatConversation | null>(null)
|
||||
const [messages, setMessages] = useState<ShopChatMessage[]>([])
|
||||
const [inputMessage, setInputMessage] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [sending, setSending] = useState(false)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [msgPage, setMsgPage] = useState(1)
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const scrollToBottom = useRef(false)
|
||||
|
||||
const isOnline = () => {
|
||||
const now = new Date()
|
||||
const h = now.getHours()
|
||||
return h >= 9 && h < 21
|
||||
}
|
||||
|
||||
// 加载会话列表
|
||||
const loadConversations = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params: ShopChatConversationParam = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
order: 'desc',
|
||||
sort: 'updateTime',
|
||||
}
|
||||
const result: PageResult<ShopChatConversation> = await pageShopChatConversation(params)
|
||||
if (result?.list) {
|
||||
setConversations(result.list)
|
||||
if (result.list.length > 0) {
|
||||
const latest = result.list[0]
|
||||
setCurrentConversation(latest)
|
||||
loadMessages(latest.id!, 1, false)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取会话列表失败:', e)
|
||||
setConversations([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 加载消息列表
|
||||
const loadMessages = useCallback(async (conversationId: number, pageNum: number = 1, isLoadMore = false) => {
|
||||
if (isLoadMore) setLoadingMore(true)
|
||||
try {
|
||||
const result = await pageShopChatMessage({
|
||||
conversationId,
|
||||
page: pageNum,
|
||||
limit: 20,
|
||||
order: 'asc',
|
||||
sort: 'createTime',
|
||||
} as any)
|
||||
if (result?.list) {
|
||||
if (isLoadMore) {
|
||||
setMessages(prev => [...result.list!, ...prev])
|
||||
} else {
|
||||
setMessages(result.list)
|
||||
scrollToBottom.current = true
|
||||
}
|
||||
setHasMore((result.list.length ?? 0) >= 20)
|
||||
setMsgPage(pageNum)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取消息列表失败:', e)
|
||||
if (!isLoadMore) setMessages([])
|
||||
} finally {
|
||||
setLoadingMore(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadConversations()
|
||||
}, [loadConversations])
|
||||
|
||||
useReachBottom(() => {
|
||||
if (hasMore && currentConversation && !loadingMore) {
|
||||
loadMessages(currentConversation.id!, msgPage + 1, true)
|
||||
}
|
||||
})
|
||||
|
||||
// 创建新会话
|
||||
const ensureConversation = async (): Promise<ShopChatConversation | null> => {
|
||||
if (currentConversation) return currentConversation
|
||||
try {
|
||||
await addShopChatConversation({ type: 0 })
|
||||
// 重新拉取会话列表获取新会话
|
||||
const result = await pageShopChatConversation({ page: 1, limit: 20, order: 'desc', sort: 'updateTime' })
|
||||
if (result?.list && result.list.length > 0) {
|
||||
const conv = result.list[0]
|
||||
setConversations(result.list)
|
||||
setCurrentConversation(conv)
|
||||
return conv
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('创建会话失败:', e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const handleSendMessage = async () => {
|
||||
const text = inputMessage.trim()
|
||||
if (!text) {
|
||||
Taro.showToast({ title: '请输入消息内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
setSending(true)
|
||||
setInputMessage('')
|
||||
try {
|
||||
const conv = await ensureConversation()
|
||||
if (!conv?.id) {
|
||||
Taro.showToast({ title: '无法创建会话,请稍后重试', icon: 'none' })
|
||||
setInputMessage(text)
|
||||
return
|
||||
}
|
||||
|
||||
await addShopChatMessage({
|
||||
content: text,
|
||||
type: 'text',
|
||||
toUserId: 0, // 发送给客服(服务端路由)
|
||||
} as any)
|
||||
|
||||
// 消息发送成功后刷新消息列表
|
||||
await loadMessages(conv.id, 1, false)
|
||||
|
||||
// 同步更新会话列表 lastMessage
|
||||
setConversations(prev => prev.map(c =>
|
||||
c.id === conv.id ? { ...c, lastMessage: text, updateTime: new Date().toISOString() } : c
|
||||
))
|
||||
} catch (e: any) {
|
||||
console.error('发送消息失败:', e)
|
||||
setInputMessage(text)
|
||||
Taro.showToast({ title: e?.message || '发送失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
setSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 拨打热线
|
||||
const handleCallHotline = () => {
|
||||
Taro.makePhoneCall({ phoneNumber: SERVICE_HOTLINE })
|
||||
}
|
||||
|
||||
// 复制微信号
|
||||
const handleCopyWechat = () => {
|
||||
Taro.setClipboardData({
|
||||
data: SERVICE_WECHAT,
|
||||
success: () => Taro.showToast({ title: '已复制微信号', icon: 'success' }),
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转帮助中心
|
||||
const handleGoToHelp = () => {
|
||||
Taro.navigateTo({ url: '/pages/user/help-center/index' })
|
||||
}
|
||||
|
||||
// 选择会话
|
||||
const handleSelectConversation = (conv: ShopChatConversation) => {
|
||||
setCurrentConversation(conv)
|
||||
setMessages([])
|
||||
setMsgPage(1)
|
||||
setHasMore(true)
|
||||
loadMessages(conv.id!, 1, false)
|
||||
}
|
||||
|
||||
const formatTime = (timeStr?: string) => {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
const h = String(date.getHours()).padStart(2, '0')
|
||||
const min = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${m}-${d} ${h}:${min}`
|
||||
}
|
||||
|
||||
const online = isOnline()
|
||||
|
||||
return (
|
||||
<View className="flex flex-col min-h-screen bg-gray-100">
|
||||
<NavBar title="在线客服" />
|
||||
|
||||
<ScrollView scrollY className="flex-1">
|
||||
{/* 客服状态栏 */}
|
||||
<View className="bg-white px-4 py-3 flex items-center justify-between">
|
||||
<View className="flex items-center gap-2">
|
||||
<View
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: online ? '#52c41a' : '#d1d5db' }}
|
||||
/>
|
||||
<Text className="text-sm text-gray-700">{online ? '客服在线' : '客服离线'}</Text>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-400">服务时间: {SERVICE_ONLINE_HOURS}</Text>
|
||||
</View>
|
||||
|
||||
{/* 微信在线客服按钮 */}
|
||||
<View className="px-4 py-3 bg-white border-t border-gray-50">
|
||||
<Button
|
||||
openType="contact"
|
||||
className="w-full h-12 rounded-lg text-white font-medium text-base border-0"
|
||||
style={{ backgroundColor: '#07c160', lineHeight: '48px' }}
|
||||
>
|
||||
微信在线客服
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 消息记录入口 */}
|
||||
{!showHistory && (
|
||||
<View className="bg-white mx-4 mt-4 p-4 rounded-lg">
|
||||
<View
|
||||
className="flex items-center justify-between"
|
||||
onClick={() => setShowHistory(true)}
|
||||
>
|
||||
<View className="flex items-center gap-2">
|
||||
<Text className="text-lg">💬</Text>
|
||||
<Text className="text-sm text-gray-700">
|
||||
历史消息记录 {conversations.length > 0 ? `(${conversations.length}条会话)` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-gray-400 text-sm">→</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 历史会话与消息列表 */}
|
||||
{showHistory && (
|
||||
<View className="mx-4 mt-4">
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="text-sm font-medium text-gray-700">历史会话</Text>
|
||||
<Text
|
||||
className="text-sm"
|
||||
style={{ color: '#3b82f6' }}
|
||||
onClick={() => setShowHistory(false)}
|
||||
>
|
||||
收起
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{loading ? (
|
||||
<LoadMore loading />
|
||||
) : conversations.length === 0 ? (
|
||||
<EmptyState message="暂无历史会话" small />
|
||||
) : (
|
||||
<View>
|
||||
{conversations.map(conv => (
|
||||
<View
|
||||
key={conv.id}
|
||||
className="bg-white p-3 rounded-lg mb-2"
|
||||
style={{ border: currentConversation?.id === conv.id ? '2px solid #10b981' : 'none' }}
|
||||
onClick={() => handleSelectConversation(conv)}
|
||||
>
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-sm text-gray-700" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{conv.content || conv.lastMessage || '暂无消息'}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-400 ml-2">{formatTime(conv.updateTime)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 当前会话的消息 */}
|
||||
{currentConversation && messages.length > 0 && (
|
||||
<View className="bg-white rounded-lg p-3 mb-4">
|
||||
<Text className="text-xs text-gray-400 mb-3 block">消息记录</Text>
|
||||
{loadingMore && <LoadMore loading />}
|
||||
{messages.map(msg => (
|
||||
<View
|
||||
key={msg.id}
|
||||
className={`flex mb-3 ${msg.formUserId === currentConversation.userId ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<View
|
||||
className="px-3 py-2 rounded-lg max-w-xs"
|
||||
style={{
|
||||
backgroundColor: msg.formUserId === currentConversation.userId ? '#10b981' : '#f3f4f6',
|
||||
maxWidth: '70%',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="text-sm"
|
||||
style={{ color: msg.formUserId === currentConversation.userId ? '#fff' : '#374151' }}
|
||||
>
|
||||
{msg.content}
|
||||
</Text>
|
||||
<Text
|
||||
className="text-xs block mt-1"
|
||||
style={{ color: msg.formUserId === currentConversation.userId ? 'rgba(255,255,255,0.7)' : '#9ca3af' }}
|
||||
>
|
||||
{formatTime(msg.createTime)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 其他联系方式 */}
|
||||
<View className="bg-white mx-4 mt-4 p-4 rounded-lg">
|
||||
<Text className="text-base font-medium text-gray-800 mb-3 block">其他联系方式</Text>
|
||||
<View
|
||||
className="flex items-center justify-between py-3 border-b border-gray-50"
|
||||
onClick={handleCallHotline}
|
||||
>
|
||||
<View className="flex items-center gap-2">
|
||||
<Text className="text-lg">📞</Text>
|
||||
<View>
|
||||
<Text className="text-sm text-gray-700 block">客服热线</Text>
|
||||
<Text className="text-xs text-gray-400">{SERVICE_HOTLINE}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className="text-xs" style={{ color: '#3b82f6' }}>拨打</Text>
|
||||
</View>
|
||||
<View
|
||||
className="flex items-center justify-between py-3"
|
||||
onClick={handleCopyWechat}
|
||||
>
|
||||
<View className="flex items-center gap-2">
|
||||
<Text className="text-lg">💬</Text>
|
||||
<View>
|
||||
<Text className="text-sm text-gray-700 block">微信号</Text>
|
||||
<Text className="text-xs text-gray-400">{SERVICE_WECHAT}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className="text-xs" style={{ color: '#3b82f6' }}>复制</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 帮助中心入口 */}
|
||||
<View className="bg-white mx-4 mt-4 p-4 rounded-lg mb-4">
|
||||
<Text className="text-base font-medium text-gray-800 mb-3 block">常见问题</Text>
|
||||
<View
|
||||
className="flex items-center justify-between py-2"
|
||||
onClick={handleGoToHelp}
|
||||
>
|
||||
<Text className="text-sm text-gray-700">查看帮助中心</Text>
|
||||
<Text className="text-gray-400 text-sm">→</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 留言(离线时) */}
|
||||
{!online && (
|
||||
<View className="bg-white mx-4 mb-4 p-4 rounded-lg">
|
||||
<Text className="text-base font-medium text-gray-800 mb-3 block">离线留言</Text>
|
||||
<Text className="text-sm text-gray-400 block">
|
||||
客服暂时不在线,可在下方输入框留言,客服上线后会尽快回复您。
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{/* 消息输入区 - 始终显示,支持发送留言 */}
|
||||
<View className="bg-white border-t border-gray-200 px-4 py-3 flex items-center gap-2">
|
||||
<Input
|
||||
className="flex-1 border border-gray-200 rounded-full text-sm"
|
||||
style={{ paddingLeft: '16px', paddingRight: '16px', height: '40px', lineHeight: '40px' }}
|
||||
placeholder={online ? '输入消息...' : '留言给客服...'}
|
||||
value={inputMessage}
|
||||
onInput={(e: any) => setInputMessage(e.detail.value)}
|
||||
onConfirm={handleSendMessage}
|
||||
disabled={sending}
|
||||
/>
|
||||
<View
|
||||
className="rounded-full flex items-center justify-center"
|
||||
style={{
|
||||
width: '72px',
|
||||
height: '40px',
|
||||
backgroundColor: sending ? '#d1d5db' : '#10b981',
|
||||
}}
|
||||
onClick={sending ? undefined : handleSendMessage}
|
||||
>
|
||||
<Text className="text-sm text-white">{sending ? '发送中' : '发送'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerServicePage
|
||||
97
src_bak/pages/user/favorite-list/index.tsx
Normal file
97
src_bak/pages/user/favorite-list/index.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { listShopGoodsFavorite } from '@/api/shop/shopGoodsFavorite'
|
||||
import type { ShopGoodsFavorite } from '@/api/shop/shopGoodsFavorite/model'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
import Price from '@/components/common/Price'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '我的收藏',
|
||||
})
|
||||
|
||||
const FavoriteListPage: React.FC = () => {
|
||||
const [list, setList] = useState<ShopGoodsFavorite[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
const [finished, setFinished] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadFavorites(1)
|
||||
}, [])
|
||||
|
||||
const loadFavorites = async (p: number) => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await listShopGoodsFavorite({ page: p, limit: 20 })
|
||||
const newList = res || []
|
||||
if (p === 1) {
|
||||
setList(newList)
|
||||
} else {
|
||||
setList(prev => [...prev, ...newList])
|
||||
}
|
||||
setFinished(newList.length < 20)
|
||||
setPage(p)
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleItemClick = (goodsId: number) => {
|
||||
Taro.navigateTo({ url: `/pages/shop/product-detail?id=${goodsId}` })
|
||||
}
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (!finished && !loading) {
|
||||
loadFavorites(page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='h-screen'
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={100}
|
||||
>
|
||||
<View className='p-3'>
|
||||
{list.length > 0 ? (
|
||||
<View className='grid grid-cols-2 gap-3'>
|
||||
{list.map(item => (
|
||||
<View
|
||||
key={item.favoriteId}
|
||||
className='bg-white rounded-lg overflow-hidden'
|
||||
onClick={() => handleItemClick(item.goodsId!)}
|
||||
>
|
||||
<Image
|
||||
className='w-full'
|
||||
style={{ height: '160px' }}
|
||||
src={item.goodsImage}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View className='p-2'>
|
||||
<Text className='text-sm text-gray-800 line-clamp-2 block'>
|
||||
{item.goodsName}
|
||||
</Text>
|
||||
<View className='mt-1'>
|
||||
<Price price={item.salePrice || '0'} size='small' loginMask />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
!loading && <EmptyState text='暂无收藏' />
|
||||
)}
|
||||
<LoadMore loading={loading} finished={finished} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default FavoriteListPage
|
||||
146
src_bak/pages/user/help-center/index.tsx
Normal file
146
src_bak/pages/user/help-center/index.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '帮助中心',
|
||||
})
|
||||
|
||||
interface HelpCategory {
|
||||
id: number
|
||||
title: string
|
||||
icon: string
|
||||
questions: HelpQuestion[]
|
||||
}
|
||||
|
||||
interface HelpQuestion {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
}
|
||||
|
||||
const helpData: HelpCategory[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: '购物指南',
|
||||
icon: '🛍️',
|
||||
questions: [
|
||||
{ id: 101, title: '如何下单购买商品?', content: '您可以在商品详情页点击"立即购买"或"加入购物车"进行购买。支持微信支付、余额支付等多种支付方式。' },
|
||||
{ id: 102, title: '购物车有什么用?', content: '购物车可以临时存放您想要购买的商品,您可以随时调整商品数量、删除商品或去结算。' },
|
||||
{ id: 103, title: '如何查看我的订单?', content: '在"我的"页面点击"我的订单"即可查看所有订单,包括待付款、待发货、待收货等不同状态的订单。' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '支付与退款',
|
||||
icon: '💰',
|
||||
questions: [
|
||||
{ id: 201, title: '支持哪些支付方式?', content: '我们支持微信支付、支付宝支付(如有)、货到付款、积分抵扣等多种支付方式。' },
|
||||
{ id: 202, title: '如何申请退款?', content: '在订单详情页点击"申请退款"按钮,填写退款原因和说明,提交后等待商家审核。' },
|
||||
{ id: 203, title: '退款多久到账?', content: '退款审核通过后,原路退回您的支付账户。微信支付一般1-3个工作日到账,余额支付即时到账。' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '配送与物流',
|
||||
icon: '🚚',
|
||||
questions: [
|
||||
{ id: 301, title: '订单多久发货?', content: '一般情况下,订单会在24小时内发货。特殊情况下可能会延迟,我们会及时通知您。' },
|
||||
{ id: 302, title: '如何查看物流信息?', content: '在订单详情页可以看到"查看物流"按钮,点击即可查看详细的物流配送信息。' },
|
||||
{ id: 303, title: '可以指定送货时间吗?', content: '目前暂不支持指定送货时间,但您可以在订单备注中说明您的配送偏好,我们会尽量安排。' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '会员与积分',
|
||||
icon: '⭐',
|
||||
questions: [
|
||||
{ id: 401, title: '如何获得积分?', content: '您可以通过每日签到、购物消费、邀请好友等方式获得积分。积分可以在积分商城兑换商品或抵扣现金。' },
|
||||
{ id: 402, title: '会员有什么特权?', content: '不同等级的会员享受不同的折扣优惠、专属客服、生日礼物等特权。会员等级越高,享受的特权越多。' },
|
||||
{ id: 403, title: '积分会过期吗?', content: '积分有效期为一年,到期后未使用的积分将自动清零。请及时使用您的积分。' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '账户与安全',
|
||||
icon: '🔒',
|
||||
questions: [
|
||||
{ id: 501, title: '如何修改密码?', content: '在"我的"->"设置"->"修改密码"中,输入原密码和新密码即可完成修改。' },
|
||||
{ id: 502, title: '忘记密码怎么办?', content: '在登录页面点击"忘记密码",通过手机号验证后可以重置密码。' },
|
||||
{ id: 503, title: '如何保护账户安全?', content: '建议您设置复杂的密码、开启登录验证、不要将账户信息透露给他人,定期修改密码。' },
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const HelpCenterPage: React.FC = () => {
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null)
|
||||
|
||||
const handleQuestionClick = (question: HelpQuestion) => {
|
||||
Taro.navigateTo({
|
||||
url: `/pages/user/help-detail/index?id=${question.id}&title=${encodeURIComponent(question.title)}&content=${encodeURIComponent(question.content)}`
|
||||
})
|
||||
}
|
||||
|
||||
const toggleCategory = (categoryId: number) => {
|
||||
setExpandedId(expandedId === categoryId ? null : categoryId)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView scrollY className='h-screen'>
|
||||
<View className='p-3'>
|
||||
{/* 搜索框(预留) */}
|
||||
<View className='bg-white rounded-lg p-3 mb-3 flex items-center'>
|
||||
<Text className='text-gray-400 text-sm'>🔍 搜索问题...</Text>
|
||||
</View>
|
||||
|
||||
{/* 分类列表 */}
|
||||
{helpData.map(category => (
|
||||
<View key={category.id} className='bg-white rounded-lg mb-3 overflow-hidden'>
|
||||
<View
|
||||
className='p-4 flex items-center justify-between'
|
||||
onClick={() => toggleCategory(category.id)}
|
||||
>
|
||||
<View className='flex items-center gap-2'>
|
||||
<Text className='text-xl'>{category.icon}</Text>
|
||||
<Text className='text-base font-medium text-gray-800'>{category.title}</Text>
|
||||
</View>
|
||||
<Text className='text-gray-400'>
|
||||
{expandedId === category.id ? '▲' : '▼'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{expandedId === category.id && (
|
||||
<View className='border-t border-gray-100'>
|
||||
{category.questions.map((question, index) => (
|
||||
<View
|
||||
key={question.id}
|
||||
className={`p-4 ${index < category.questions.length - 1 ? 'border-b border-gray-50' : ''}`}
|
||||
onClick={() => handleQuestionClick(question)}
|
||||
>
|
||||
<Text className='text-sm text-gray-600'>{question.title}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* 联系客服入口 */}
|
||||
<View
|
||||
className='bg-white rounded-lg p-4 flex items-center justify-center gap-2 mt-3'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/customer-service/index' })}
|
||||
>
|
||||
<Text className='text-xl'>📞</Text>
|
||||
<Text className='text-sm text-blue-500'>联系在线客服</Text>
|
||||
</View>
|
||||
|
||||
<View className='h-4'></View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default HelpCenterPage
|
||||
50
src_bak/pages/user/help-detail/index.tsx
Normal file
50
src_bak/pages/user/help-detail/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '问题详情',
|
||||
})
|
||||
|
||||
const HelpDetailPage: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const [title, setTitle] = useState('')
|
||||
const [content, setContent] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const { title: t, content: c } = router.params
|
||||
if (t) setTitle(decodeURIComponent(t))
|
||||
if (c) setContent(decodeURIComponent(c))
|
||||
}, [router.params])
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView scrollY className='h-screen'>
|
||||
<View className='p-4'>
|
||||
<View className='bg-white rounded-lg p-4'>
|
||||
<Text className='text-lg font-medium text-gray-800 block mb-4'>
|
||||
{title}
|
||||
</Text>
|
||||
<View className='border-t border-gray-100 pt-4'>
|
||||
<Text className='text-sm text-gray-600 leading-7'>
|
||||
{content}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 相关问题推荐(预留) */}
|
||||
<View className='mt-4 bg-white rounded-lg p-4'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>
|
||||
相关问题
|
||||
</Text>
|
||||
<Text className='text-sm text-gray-400'>
|
||||
暂无相关问题
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default HelpDetailPage
|
||||
113
src_bak/pages/user/history-list/index.tsx
Normal file
113
src_bak/pages/user/history-list/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '浏览历史',
|
||||
})
|
||||
|
||||
interface HistoryItem {
|
||||
goodsId: number
|
||||
name?: string
|
||||
image?: string
|
||||
price?: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const BrowseHistoryPage: React.FC = () => {
|
||||
const [list, setList] = useState<HistoryItem[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
loadHistory()
|
||||
}, [])
|
||||
|
||||
const loadHistory = () => {
|
||||
try {
|
||||
const history = Taro.getStorageSync('browse_history') || []
|
||||
setList(history)
|
||||
} catch {
|
||||
setList([])
|
||||
}
|
||||
}
|
||||
|
||||
const handleItemClick = (goodsId: number) => {
|
||||
Taro.navigateTo({ url: `/pages/shop/product-detail?id=${goodsId}` })
|
||||
}
|
||||
|
||||
const handleClearHistory = () => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要清空浏览历史吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.removeStorageSync('browse_history')
|
||||
setList([])
|
||||
Taro.showToast({ title: '已清空', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formatTime = (timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
{/* 顶部操作栏 */}
|
||||
{list.length > 0 && (
|
||||
<View className='bg-white px-4 py-2 flex justify-end border-b border-gray-100'>
|
||||
<Text
|
||||
className='text-sm text-red-500'
|
||||
onClick={handleClearHistory}
|
||||
>
|
||||
清空历史
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ScrollView scrollY className='h-screen'>
|
||||
<View className='p-3'>
|
||||
{list.length > 0 ? (
|
||||
<View className='grid grid-cols-2 gap-3'>
|
||||
{list.map(item => (
|
||||
<View
|
||||
key={`${item.goodsId}-${item.timestamp}`}
|
||||
className='bg-white rounded-lg overflow-hidden'
|
||||
onClick={() => handleItemClick(item.goodsId)}
|
||||
>
|
||||
<Image
|
||||
className='w-full'
|
||||
style={{ height: '160px' }}
|
||||
src={item.image}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View className='p-2'>
|
||||
<Text className='text-sm text-gray-800 line-clamp-2 block'>
|
||||
{item.name}
|
||||
</Text>
|
||||
<View className='flex items-center justify-between mt-1'>
|
||||
<Text className='text-red-500 text-sm font-medium'>
|
||||
¥{item.price || '0'}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
{formatTime(item.timestamp)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<EmptyState text='暂无浏览历史' />
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default BrowseHistoryPage
|
||||
233
src_bak/pages/user/index.tsx
Normal file
233
src_bak/pages/user/index.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import React from 'react'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import MemberBadge from '@/components/business/MemberBadge'
|
||||
|
||||
const UserPage: React.FC = () => {
|
||||
const { user, isLoggedIn } = useUser()
|
||||
|
||||
const menuItems = [
|
||||
{ icon: '📦', label: '我的订单', url: '/pages/order/list' },
|
||||
{ icon: '📅', label: '预约订单', url: '/pages/booking/list' },
|
||||
{ icon: '🏆', label: '赛事活动', url: '/pages/event/my/index' },
|
||||
{ icon: '🎫', label: '优惠券', url: '/pages/user/coupon-list' },
|
||||
{ icon: '💰', label: '我的钱包', url: '/pages/user/wallet' },
|
||||
{ icon: '📍', label: '收货地址', url: '/pages/user/address-list' },
|
||||
{ icon: '⭐', label: '积分明细', url: '/pages/user/points-record' },
|
||||
{ icon: '🔔', label: '消息通知', url: '/pages/index/notification' },
|
||||
{ icon: '⚙️', label: '设置', url: '/pages/user/setting' },
|
||||
]
|
||||
|
||||
const handleLogin = () => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-100 flex flex-col'>
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
{/* 顶部渐变背景区域 */}
|
||||
<View className='relative'>
|
||||
{/* 渐变背景 — 绿色系 */}
|
||||
<View
|
||||
className='absolute inset-0'
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #0d9488 0%, #059669 50%, #10b981 100%)',
|
||||
}}
|
||||
/>
|
||||
{/* 装饰圆形 */}
|
||||
<View
|
||||
className='absolute'
|
||||
style={{
|
||||
width: '240px',
|
||||
height: '240px',
|
||||
borderRadius: '120px',
|
||||
background: 'rgba(255, 255, 255, 0.08)',
|
||||
top: '-100px',
|
||||
right: '-80px',
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
className='absolute'
|
||||
style={{
|
||||
width: '180px',
|
||||
height: '180px',
|
||||
borderRadius: '90px',
|
||||
background: 'rgba(255, 255, 255, 0.06)',
|
||||
top: '-60px',
|
||||
left: '-60px',
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
className='absolute'
|
||||
style={{
|
||||
width: '120px',
|
||||
height: '120px',
|
||||
borderRadius: '60px',
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
bottom: '20px',
|
||||
right: '-30px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 用户信息卡片 */}
|
||||
<View className='relative mx-3 mt-3 p-4 rounded-2xl' style={{
|
||||
background: 'rgba(255, 255, 255, 0.15)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
}}>
|
||||
<View className='flex items-center gap-4' onClick={handleLogin}>
|
||||
{/* 头像区域 */}
|
||||
<View className='relative'>
|
||||
{isLoggedIn && user?.avatar ? (
|
||||
<Image
|
||||
className='w-16 h-16 rounded-full border-3 border-white/30'
|
||||
style={{ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.2)' }}
|
||||
src={user.avatar}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
className='w-16 h-16 rounded-full border-3 border-white/30 flex items-center justify-center'
|
||||
style={{ background: 'rgba(255, 255, 255, 0.25)', boxShadow: '0 4px 16px rgba(0, 0, 0, 0.2)' }}
|
||||
>
|
||||
<Text className='text-2xl text-white'>👤</Text>
|
||||
</View>
|
||||
)}
|
||||
{/* VIP标识 */}
|
||||
{isLoggedIn && user?.memberLevelName && (
|
||||
<View
|
||||
className='absolute -bottom-1 -right-1 px-1 py-1 rounded text-xs text-white'
|
||||
style={{ background: 'linear-gradient(135deg, #fbbf24, #f59e0b)' }}
|
||||
>
|
||||
VIP
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 用户信息 */}
|
||||
<View className='flex-1'>
|
||||
<View className='flex items-center gap-2'>
|
||||
<Text className='text-lg font-bold text-white'>
|
||||
{isLoggedIn ? (user?.nickname || user?.phone || '用户') : '点击登录'}
|
||||
</Text>
|
||||
{isLoggedIn && user?.memberLevelName && (
|
||||
<MemberBadge levelName={user.memberLevelName} />
|
||||
)}
|
||||
</View>
|
||||
{isLoggedIn ? (
|
||||
<Text className='text-xs text-white/60 mt-1'>ID: {(user as any)?.id || '暂无'}</Text>
|
||||
) : (
|
||||
<Text className='text-xs text-white/60 mt-1'>登录后享受更多服务</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 箭头 */}
|
||||
<View className='w-6 h-6 rounded-full bg-white/20 flex items-center justify-center'>
|
||||
<Text className='text-white/80 text-xs'>{'>'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 数据概览 — 4列白色文字 */}
|
||||
{isLoggedIn && (
|
||||
<View
|
||||
className='grid grid-cols-4 gap-2 mt-4 pt-4'
|
||||
style={{ borderTop: '1px solid rgba(255, 255, 255, 0.2)' }}
|
||||
>
|
||||
<View
|
||||
className='text-center py-2'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/wallet' })}
|
||||
>
|
||||
<Text className='text-xl font-bold text-white'>
|
||||
¥{((user as any)?.balance || '0.00')}
|
||||
</Text>
|
||||
<Text className='text-xs text-white/70 mt-1 block'>余额</Text>
|
||||
</View>
|
||||
<View
|
||||
className='text-center py-2'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/points/index' })}
|
||||
>
|
||||
<Text className='text-xl font-bold text-white'>
|
||||
{(user as any)?.points || 0}
|
||||
</Text>
|
||||
<Text className='text-xs text-white/70 mt-1 block'>积分</Text>
|
||||
</View>
|
||||
<View
|
||||
className='text-center py-2'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/coupon-list' })}
|
||||
>
|
||||
<Text className='text-xl font-bold text-white'>
|
||||
0
|
||||
</Text>
|
||||
<Text className='text-xs text-white/70 mt-1 block'>优惠券</Text>
|
||||
</View>
|
||||
<View
|
||||
className='text-center py-2'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/order/list' })}
|
||||
>
|
||||
<Text className='text-xl font-bold text-white'>
|
||||
0
|
||||
</Text>
|
||||
<Text className='text-xs text-white/70 mt-1 block'>订单</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
</View>
|
||||
|
||||
{/* 我的订单快捷入口 */}
|
||||
<View className='bg-white rounded-xl mx-3 mt-3 p-4' style={{ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)' }}>
|
||||
<View className='flex justify-between items-center mb-3'>
|
||||
<Text className='text-base font-medium text-gray-800'>我的订单</Text>
|
||||
<Text className='text-xs text-gray-400' onClick={() => Taro.navigateTo({ url: '/pages/order/list' })}>
|
||||
全部订单 {'>'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='grid grid-cols-4 gap-2'>
|
||||
{[
|
||||
{ icon: '💳', label: '待付款', status: 0, color: '#f093fb' },
|
||||
{ icon: '📦', label: '待发货', status: 1, color: '#667eea' },
|
||||
{ icon: '🚚', label: '待收货', status: 2, color: '#4facfe' },
|
||||
{ icon: '✅', label: '已完成', status: 3, color: '#52c41a' },
|
||||
].map(item => (
|
||||
<View
|
||||
key={item.status}
|
||||
className='flex flex-col items-center py-2 rounded-lg'
|
||||
style={{ background: `${item.color}10` }}
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/order/list?tab=${item.status}` })}
|
||||
>
|
||||
<Text className='text-xl mb-1'>{item.icon}</Text>
|
||||
<Text className='text-xs text-gray-600'>{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
{/* 功能菜单 */}
|
||||
<View className='bg-white rounded-xl mx-3 mt-3 overflow-hidden' style={{ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)' }}>
|
||||
{menuItems.map((item, idx) => (
|
||||
<View
|
||||
key={item.label}
|
||||
className={`flex items-center justify-between px-4 py-3 ${
|
||||
idx < menuItems.length - 1 ? 'border-b border-gray-50' : ''
|
||||
}`}
|
||||
onClick={() => Taro.navigateTo({ url: item.url })}
|
||||
>
|
||||
<View className='flex items-center gap-3'>
|
||||
<Text className='text-base'>{item.icon}</Text>
|
||||
<Text className='text-sm text-gray-700'>{item.label}</Text>
|
||||
</View>
|
||||
<Text className='text-gray-300 text-sm'>{'>'}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{/* 底部安全区域 */}
|
||||
<View className='h-20' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserPage
|
||||
3
src_bak/pages/user/invite-record/index.config.ts
Normal file
3
src_bak/pages/user/invite-record/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '邀请记录',
|
||||
}
|
||||
89
src_bak/pages/user/invite-record/index.tsx
Normal file
89
src_bak/pages/user/invite-record/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
import { listShopUserReferee } from '@/api/shop/shopUserReferee'
|
||||
import type { ShopUserReferee } from '@/api/shop/shopUserReferee/model'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '邀请记录',
|
||||
})
|
||||
|
||||
const InviteRecordPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const [records, setRecords] = useState<ShopUserReferee[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const scrollHeight = useScrollHeight(44)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
return
|
||||
}
|
||||
fetchRecords()
|
||||
}, [isLoggedIn])
|
||||
|
||||
const fetchRecords = async () => {
|
||||
try {
|
||||
const data = await listShopUserReferee({})
|
||||
setRecords(data || [])
|
||||
} catch (e) {
|
||||
console.error('获取邀请记录失败:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
{loading ? (
|
||||
<View className='flex items-center justify-center py-10'>
|
||||
<Text className='text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
) : records.length === 0 ? (
|
||||
<EmptyState text='暂无邀请记录' />
|
||||
) : (
|
||||
<View className='p-3'>
|
||||
{records.map((item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
className='bg-white rounded-lg p-3 mb-2'
|
||||
>
|
||||
<View className='flex justify-between items-center mb-1'>
|
||||
<Text className='text-sm font-medium text-gray-800'>
|
||||
{item.nickname || '用户'}
|
||||
</Text>
|
||||
<View
|
||||
className={`px-2 py-1 rounded-full text-xs ${
|
||||
item.status === 1 ? 'bg-green-50 text-green-500' : 'bg-orange-50 text-orange-500'
|
||||
}`}
|
||||
>
|
||||
<Text>{item.status === 1 ? '已注册' : '待注册'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='flex justify-between items-center'>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
邀请时间:{item.createTime}
|
||||
</Text>
|
||||
{item.isMember && (
|
||||
<Text className='text-xs text-orange-500'>会员</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default InviteRecordPage
|
||||
4
src_bak/pages/user/invite-subordinate/index.config.ts
Normal file
4
src_bak/pages/user/invite-subordinate/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '录入下级',
|
||||
enableShareAppMessage: true,
|
||||
})
|
||||
340
src_bak/pages/user/invite-subordinate/index.tsx
Normal file
340
src_bak/pages/user/invite-subordinate/index.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text, ScrollView, Input, Button, Image } from '@tarojs/components'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
import { getMyRegisterList, addSubordinate, updateSubordinate, MemberRegister } from '@/api/shop/shopMemberRegister'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '录入下级',
|
||||
})
|
||||
|
||||
const InviteSubordinatePage: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const { user, isLoggedIn } = useUser()
|
||||
const scrollHeight = useScrollHeight()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [list, setList] = useState<MemberRegister[]>([])
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [editingItem, setEditingItem] = useState<MemberRegister | null>(null)
|
||||
|
||||
// 表单数据
|
||||
const [phone, setPhone] = useState('')
|
||||
const [realName, setRealName] = useState('')
|
||||
|
||||
// 从首页跳转过来自动弹出添加弹窗
|
||||
useEffect(() => {
|
||||
const { from } = router.params
|
||||
if (from === 'home') {
|
||||
setShowAddModal(true)
|
||||
}
|
||||
}, [router.params])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
return
|
||||
}
|
||||
fetchList()
|
||||
}, [isLoggedIn])
|
||||
|
||||
const fetchList = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const data = await getMyRegisterList({ page: 1, limit: 100 })
|
||||
setList(data || [])
|
||||
} catch (e: any) {
|
||||
console.error('获取列表失败:', e)
|
||||
Taro.showToast({ title: e.message || '获取列表失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 手机号验证
|
||||
const validatePhone = (p: string) => {
|
||||
return /^1[3-9]\d{9}$/.test(p)
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!validatePhone(phone)) {
|
||||
Taro.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
if (editingItem) {
|
||||
// 修改
|
||||
const res = await updateSubordinate({
|
||||
id: editingItem.id,
|
||||
phone: phone.trim(),
|
||||
realName: realName.trim(),
|
||||
})
|
||||
if (res.code === 0) {
|
||||
Taro.showToast({ title: '修改成功', icon: 'success' })
|
||||
setShowAddModal(false)
|
||||
resetForm()
|
||||
fetchList()
|
||||
} else {
|
||||
Taro.showToast({ title: res.message || '修改失败', icon: 'none' })
|
||||
}
|
||||
} else {
|
||||
// 新增
|
||||
const res = await addSubordinate({
|
||||
phone: phone.trim(),
|
||||
realName: realName.trim(),
|
||||
})
|
||||
if (res.code === 0) {
|
||||
Taro.showToast({ title: '录入成功', icon: 'success' })
|
||||
setShowAddModal(false)
|
||||
resetForm()
|
||||
fetchList()
|
||||
} else {
|
||||
Taro.showToast({ title: res.message || '录入失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
setPhone('')
|
||||
setRealName('')
|
||||
setEditingItem(null)
|
||||
}
|
||||
|
||||
// 打开添加弹窗
|
||||
const openAddModal = () => {
|
||||
resetForm()
|
||||
setShowAddModal(true)
|
||||
}
|
||||
|
||||
// 打开编辑弹窗
|
||||
const openEditModal = (item: MemberRegister) => {
|
||||
setEditingItem(item)
|
||||
setPhone(item.phone)
|
||||
setRealName(item.realName || '')
|
||||
setShowAddModal(true)
|
||||
}
|
||||
|
||||
// 跳转到支付页面
|
||||
const goToPay = (item: MemberRegister) => {
|
||||
Taro.navigateTo({
|
||||
url: `/pages/user/register-pay/index?phone=${item.phone}®isterId=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (item: MemberRegister) => {
|
||||
if (item.status === 1) return { text: '已完成', color: 'text-green-500', bg: 'bg-green-50' }
|
||||
if (item.status === 2) return { text: '已取消', color: 'text-gray-400', bg: 'bg-gray-100' }
|
||||
if (item.registerFeeStatus === 1) return { text: '待审核', color: 'text-orange-500', bg: 'bg-orange-50' }
|
||||
return { text: '待支付注册费', color: 'text-blue-500', bg: 'bg-blue-50' }
|
||||
}
|
||||
|
||||
// 统计
|
||||
const stats = {
|
||||
total: list.length,
|
||||
pending: list.filter(l => l.status === 0).length,
|
||||
completed: list.filter(l => l.status === 1).length,
|
||||
}
|
||||
|
||||
if (!isLoggedIn) return null
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
{/* 顶部说明 */}
|
||||
<View className='mx-3 mt-3 p-4 rounded-xl' style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
|
||||
<Text className='text-white text-sm font-medium block'>录入下级会员</Text>
|
||||
<Text className='text-white text-xs opacity-80 mt-1 block leading-relaxed'>
|
||||
请输入下级会员的手机号,下级需支付198元注册费后成为正式会员
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<View className='mx-3 mt-3 bg-white rounded-xl p-4'>
|
||||
<View className='flex justify-around'>
|
||||
<View className='text-center'>
|
||||
<Text className='text-2xl font-bold text-gray-800'>{stats.total}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>已录入</Text>
|
||||
</View>
|
||||
<View className='text-center'>
|
||||
<Text className='text-2xl font-bold text-blue-500'>{stats.pending}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>待支付</Text>
|
||||
</View>
|
||||
<View className='text-center'>
|
||||
<Text className='text-2xl font-bold text-green-500'>{stats.completed}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>已完成</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 录入按钮 */}
|
||||
<View className='mx-3 mt-3'>
|
||||
<Button
|
||||
className='bg-blue-500 text-white rounded-full py-3'
|
||||
style={{ backgroundColor: '#667eea' }}
|
||||
onClick={openAddModal}
|
||||
>
|
||||
<Text className='text-white font-medium'>+ 录入下级</Text>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 列表 */}
|
||||
<View className='mx-3 mt-3 mb-4'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>下级列表</Text>
|
||||
|
||||
{loading ? (
|
||||
<View className='bg-white rounded-xl p-6 text-center'>
|
||||
<Text className='text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
) : list.length === 0 ? (
|
||||
<EmptyState text='暂无下级记录' subText='点击上方按钮录入下级会员' />
|
||||
) : (
|
||||
<View className='space-y-3'>
|
||||
{list.map((item) => {
|
||||
const status = getStatusText(item)
|
||||
return (
|
||||
<View key={item.id} className='bg-white rounded-xl p-4'>
|
||||
<View className='flex items-center justify-between'>
|
||||
<View className='flex items-center gap-3'>
|
||||
<View className='w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center'>
|
||||
<Text className='text-lg'>👤</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className='text-gray-800 font-medium'>
|
||||
{item.realName || '未填写姓名'}
|
||||
</Text>
|
||||
<Text className='text-gray-500 text-sm mt-0.5'>
|
||||
{item.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={`px-2 py-1 rounded-full ${status.bg}`}>
|
||||
<Text className={`text-xs ${status.color}`}>{status.text}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 费用信息 */}
|
||||
<View className='mt-3 pt-3 border-t border-gray-100 flex justify-between text-xs'>
|
||||
<View>
|
||||
<Text className='text-gray-400'>会员费(线下)</Text>
|
||||
<Text className='text-gray-600 mt-0.5'>¥298.00</Text>
|
||||
</View>
|
||||
<View className='text-right'>
|
||||
<Text className='text-gray-400'>注册费(平台)</Text>
|
||||
<Text className='text-gray-600 mt-0.5'>¥198.00</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className='mt-3 flex gap-2'>
|
||||
<Button
|
||||
size='mini'
|
||||
className='flex-1 rounded-full'
|
||||
style={{ borderColor: '#ddd', borderWidth: 1 }}
|
||||
onClick={() => openEditModal(item)}
|
||||
disabled={item.status === 1}
|
||||
>
|
||||
<Text className='text-gray-600 text-xs'>修改</Text>
|
||||
</Button>
|
||||
{item.registerFeeStatus === 0 && (
|
||||
<Button
|
||||
size='mini'
|
||||
className='flex-1 rounded-full bg-blue-500 text-white'
|
||||
style={{ backgroundColor: '#667eea' }}
|
||||
onClick={() => goToPay(item)}
|
||||
>
|
||||
<Text className='text-white text-xs'>查看支付</Text>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Text className='text-gray-300 text-xs mt-2 block'>
|
||||
录入时间: {item.createTime}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 添加/编辑弹窗 */}
|
||||
{showAddModal && (
|
||||
<View className='fixed inset-0 z-50'>
|
||||
{/* 遮罩 */}
|
||||
<View
|
||||
className='absolute inset-0 bg-black bg-opacity-50'
|
||||
onClick={() => { setShowAddModal(false); resetForm() }}
|
||||
/>
|
||||
{/* 内容 */}
|
||||
<View className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-5'>
|
||||
<View className='flex justify-between items-center mb-4'>
|
||||
<Text className='text-lg font-medium text-gray-800'>
|
||||
{editingItem ? '修改下级信息' : '录入下级'}
|
||||
</Text>
|
||||
<Text
|
||||
className='text-gray-400 text-xl'
|
||||
onClick={() => { setShowAddModal(false); resetForm() }}
|
||||
>
|
||||
×
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className='mb-4'>
|
||||
<Text className='text-sm text-gray-600 mb-2 block'>下级手机号 *</Text>
|
||||
<Input
|
||||
className='border border-gray-200 rounded-lg px-4 py-3 text-base'
|
||||
type='number'
|
||||
maxlength={11}
|
||||
placeholder='请输入下级手机号'
|
||||
value={phone}
|
||||
onInput={(e) => setPhone(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='mb-4'>
|
||||
<Text className='text-sm text-gray-600 mb-2 block'>下级姓名(选填)</Text>
|
||||
<Input
|
||||
className='border border-gray-200 rounded-lg px-4 py-3 text-base'
|
||||
placeholder='请输入下级姓名'
|
||||
value={realName}
|
||||
onInput={(e) => setRealName(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='bg-blue-50 rounded-lg p-3 mb-4'>
|
||||
<Text className='text-xs text-blue-600 leading-relaxed block'>
|
||||
温馨提示:下级需支付198元注册费后方可成为正式会员,请确保手机号填写正确。
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
className='rounded-full py-3'
|
||||
style={{ backgroundColor: '#667eea' }}
|
||||
loading={submitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<Text className='text-white font-medium'>
|
||||
{editingItem ? '保存修改' : '确认录入'}
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default InviteSubordinatePage
|
||||
5
src_bak/pages/user/member-upgrade/index.config.ts
Normal file
5
src_bak/pages/user/member-upgrade/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
navigationBarTitleText: '会员升级',
|
||||
navigationBarBackgroundColor: '#667eea',
|
||||
navigationBarTextStyle: 'white',
|
||||
}
|
||||
261
src_bak/pages/user/member-upgrade/index.tsx
Normal file
261
src_bak/pages/user/member-upgrade/index.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
import { useRequest } from '@/hooks/useRequest'
|
||||
import { getDealerSetting } from '@/api/shop/shopMemberCenter'
|
||||
import MemberBadge from '@/components/business/MemberBadge'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '会员升级',
|
||||
})
|
||||
|
||||
interface LevelInfo {
|
||||
id: number
|
||||
levelName: string
|
||||
levelIcon: string
|
||||
upgradeCondition: string
|
||||
upgradeAmount: number
|
||||
commissionRate: number
|
||||
selfBuyRate: number
|
||||
}
|
||||
|
||||
const MemberUpgradePage: React.FC = () => {
|
||||
const { user, isLoggedIn } = useUser()
|
||||
const scrollHeight = useScrollHeight()
|
||||
const [currentLevel, setCurrentLevel] = useState(0)
|
||||
|
||||
// 获取会员等级列表
|
||||
const { data: levelList, run: runLevelList, loading } = useRequest(getDealerSetting, {
|
||||
manual: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
return
|
||||
}
|
||||
runLevelList()
|
||||
}, [isLoggedIn])
|
||||
|
||||
// 处理升级
|
||||
const handleUpgrade = (level: LevelInfo) => {
|
||||
Taro.showModal({
|
||||
title: '升级确认',
|
||||
content: `确定升级为${level.levelName}吗?升级后将享受更多权益。`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.showToast({
|
||||
title: '升级功能开发中',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 当前等级信息
|
||||
const currentLevelName = user?.memberLevelName || '普通会员'
|
||||
const currentLevelIndex = levelList?.findIndex(l => l.levelName === currentLevelName) || 0
|
||||
|
||||
// 会员等级图标映射
|
||||
const levelIcons: Record<string, string> = {
|
||||
'普通会员': '👤',
|
||||
'白银会员': '🥈',
|
||||
'黄金会员': '🥇',
|
||||
'铂金会员': '💎',
|
||||
'钻石会员': '💠',
|
||||
'黑金会员': '🏆',
|
||||
}
|
||||
|
||||
// 获取图标
|
||||
const getLevelIcon = (name: string) => levelIcons[name] || '⭐'
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
{/* 顶部背景 */}
|
||||
<View className='p-5 pb-8 rounded-b-3xl'
|
||||
style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
|
||||
<View className='flex items-center justify-center flex-col'>
|
||||
<View className='w-20 h-20 rounded-full bg-white bg-opacity-20 flex items-center justify-center mb-3'>
|
||||
<Text className='text-4xl'>{getLevelIcon(currentLevelName)}</Text>
|
||||
</View>
|
||||
<Text className='text-white text-lg font-bold'>当前等级</Text>
|
||||
<View className='mt-2'>
|
||||
<MemberBadge levelName={currentLevelName} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 升级说明 */}
|
||||
<View className='mx-3 -mt-4 bg-white rounded-xl p-4 shadow-sm'>
|
||||
<View className='flex items-start gap-3'>
|
||||
<View className='w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0'>
|
||||
<Text className='text-base'>💡</Text>
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-gray-800 text-sm font-medium block'>升级说明</Text>
|
||||
<Text className='text-gray-500 text-xs mt-1 block leading-relaxed'>
|
||||
升级会员后可享受更多权益,包括更高的佣金比例、专属折扣、优先客服等特权。详情请联系客服了解。
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 会员等级列表 */}
|
||||
<View className='mx-3 mt-3'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>选择升级方案</Text>
|
||||
|
||||
{loading ? (
|
||||
<View className='flex items-center justify-center py-10'>
|
||||
<Text className='text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
) : !levelList || levelList.length === 0 ? (
|
||||
<View className='bg-white rounded-xl p-6 text-center'>
|
||||
<Text className='text-gray-400'>暂无升级方案</Text>
|
||||
<Text className='text-gray-400 text-xs mt-2'>请联系客服了解详情</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='space-y-3'>
|
||||
{levelList.map((level, idx) => {
|
||||
const isCurrent = level.levelName === currentLevelName
|
||||
const isHigher = idx > currentLevelIndex
|
||||
return (
|
||||
<View
|
||||
key={level.id}
|
||||
className={`bg-white rounded-xl p-4 ${isHigher ? 'border-2 border-dashed border-orange-300' : ''}`}
|
||||
>
|
||||
<View className='flex items-start gap-3'>
|
||||
{/* 等级图标 */}
|
||||
<View className={`w-12 h-12 rounded-xl flex items-center justify-center ${
|
||||
idx === 0 ? 'bg-gray-100' :
|
||||
idx === 1 ? 'bg-orange-100' :
|
||||
idx === 2 ? 'bg-yellow-100' :
|
||||
idx === 3 ? 'bg-blue-100' :
|
||||
idx === 4 ? 'bg-purple-100' : 'bg-pink-100'
|
||||
}`}>
|
||||
<Text className='text-2xl'>{getLevelIcon(level.levelName)}</Text>
|
||||
</View>
|
||||
|
||||
{/* 等级信息 */}
|
||||
<View className='flex-1'>
|
||||
<View className='flex items-center gap-2'>
|
||||
<Text className='text-gray-800 font-medium'>{level.levelName}</Text>
|
||||
{isCurrent && (
|
||||
<View className='px-2 py-0.5 bg-green-100 rounded-full'>
|
||||
<Text className='text-green-600 text-xs'>当前</Text>
|
||||
</View>
|
||||
)}
|
||||
{isHigher && (
|
||||
<View className='px-2 py-0.5 bg-orange-100 rounded-full'>
|
||||
<Text className='text-orange-600 text-xs'>可升级</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 佣金比例 */}
|
||||
<View className='flex gap-4 mt-2'>
|
||||
<View>
|
||||
<Text className='text-gray-500 text-xs'>团队佣金</Text>
|
||||
<Text className='text-orange-500 font-bold'>{(level.commissionRate * 100).toFixed(1)}%</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className='text-gray-500 text-xs'>自购返利</Text>
|
||||
<Text className='text-green-500 font-bold'>{(level.selfBuyRate * 100).toFixed(1)}%</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 升级条件 */}
|
||||
{level.upgradeCondition && (
|
||||
<Text className='text-gray-400 text-xs mt-2'>{level.upgradeCondition}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 升级按钮 */}
|
||||
{isHigher && (
|
||||
<View
|
||||
className='px-4 py-2 bg-orange-500 rounded-full'
|
||||
onClick={() => handleUpgrade(level)}
|
||||
>
|
||||
<Text className='text-white text-sm'>升级</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 权益对比 */}
|
||||
<View className='mx-3 mt-4'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>权益对比</Text>
|
||||
<View className='bg-white rounded-xl overflow-hidden'>
|
||||
<View className='flex bg-gray-50'>
|
||||
<View className='flex-1 px-3 py-2'>
|
||||
<Text className='text-gray-500 text-xs text-center'>权益项目</Text>
|
||||
</View>
|
||||
{['普通会员', '白银会员', '黄金会员'].map(name => (
|
||||
<View key={name} className='flex-1 px-2 py-2'>
|
||||
<Text className='text-gray-500 text-xs text-center'>{name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{[
|
||||
{ label: '购物折扣', values: ['9.5折', '9折', '8.5折'] },
|
||||
{ label: '团队佣金', values: ['5%', '10%', '15%'] },
|
||||
{ label: '自购返利', values: ['1%', '3%', '5%'] },
|
||||
{ label: '专属客服', values: ['❌', '✔', '✔'] },
|
||||
{ label: '优先发货', values: ['❌', '❌', '✔'] },
|
||||
].map((row, idx) => (
|
||||
<View
|
||||
key={row.label}
|
||||
className={`flex ${idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`}
|
||||
>
|
||||
<View className='flex-1 px-3 py-3'>
|
||||
<Text className='text-gray-700 text-sm text-center'>{row.label}</Text>
|
||||
</View>
|
||||
{row.values.map((val, vIdx) => (
|
||||
<View key={vIdx} className='flex-1 px-2 py-3'>
|
||||
<Text className={`text-sm text-center ${
|
||||
val === '❌' ? 'text-gray-300' :
|
||||
val === '✔' ? 'text-green-500' : 'text-gray-700'
|
||||
}`}>{val}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 联系客服 */}
|
||||
<View className='mx-3 mt-4 mb-4'>
|
||||
<View
|
||||
className='bg-white rounded-xl p-4 flex items-center justify-between'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/customer-service/index' })}
|
||||
>
|
||||
<View className='flex items-center gap-3'>
|
||||
<View className='w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center'>
|
||||
<Text className='text-lg'>📞</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className='text-gray-800 font-medium block'>如有疑问</Text>
|
||||
<Text className='text-gray-400 text-xs mt-0.5'>联系客服帮您解答</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className='text-gray-300'>›</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default MemberUpgradePage
|
||||
3
src_bak/pages/user/member/index.config.ts
Normal file
3
src_bak/pages/user/member/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '会员中心',
|
||||
}
|
||||
271
src_bak/pages/user/member/index.tsx
Normal file
271
src_bak/pages/user/member/index.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useRequest } from '@/hooks/useRequest'
|
||||
import { getMemberCenterData, type MemberCenterData } from '@/api/shop/shopMemberCenter'
|
||||
import { listShopUserReferee } from '@/api/shop/shopUserReferee'
|
||||
import MemberBadge from '@/components/business/MemberBadge'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '会员中心',
|
||||
})
|
||||
|
||||
const MemberPage: React.FC = () => {
|
||||
const { user, isLoggedIn } = useUser()
|
||||
|
||||
// 获取会员中心数据
|
||||
const { data: memberData, run: runMemberData, loading: memberLoading } = useRequest(getMemberCenterData, {
|
||||
manual: true,
|
||||
})
|
||||
|
||||
// 获取团队成员数据
|
||||
const { data: teamData, run: runTeamData } = useRequest(listShopUserReferee, {
|
||||
manual: true,
|
||||
})
|
||||
|
||||
// 登录后加载数据
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
return
|
||||
}
|
||||
runMemberData()
|
||||
runTeamData({})
|
||||
}, [isLoggedIn])
|
||||
|
||||
// 处理升级会员
|
||||
const handleUpgrade = () => {
|
||||
Taro.navigateTo({ url: '/pages/user/member-upgrade/index' })
|
||||
}
|
||||
|
||||
// 处理功能项点击
|
||||
const handleFeatureClick = (url: string) => {
|
||||
if (url) {
|
||||
Taro.navigateTo({ url })
|
||||
}
|
||||
}
|
||||
|
||||
// 会员功能菜单
|
||||
const memberFeatures = [
|
||||
{
|
||||
icon: '💰',
|
||||
label: '我的佣金',
|
||||
value: memberData?.commission !== undefined ? `¥${memberData.commission}` : '--',
|
||||
url: '/pages/user/commission/index',
|
||||
},
|
||||
{
|
||||
icon: '📊',
|
||||
label: '佣金明细',
|
||||
value: '',
|
||||
url: '/pages/user/commission/index',
|
||||
},
|
||||
{
|
||||
icon: '👥',
|
||||
label: '我的团队',
|
||||
value: memberData?.teamCount !== undefined ? `${memberData.teamCount}人` : '--',
|
||||
url: '/pages/user/team/index',
|
||||
},
|
||||
{
|
||||
icon: '📋',
|
||||
label: '邀请记录',
|
||||
value: '',
|
||||
url: '/pages/user/invite-record/index',
|
||||
},
|
||||
]
|
||||
|
||||
// 会员权益
|
||||
const memberBenefits = [
|
||||
{ icon: '🎁', text: '享受会员专属折扣' },
|
||||
{ icon: '💵', text: '获得分销佣金权益' },
|
||||
{ icon: '🎯', text: '优先客服支持' },
|
||||
{ icon: '🎉', text: '专属活动邀请' },
|
||||
]
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='flex flex-col bg-gray-50 min-h-screen'>
|
||||
{/* 会员卡片 - 渐变背景 */}
|
||||
<View className='mx-3 mt-3 p-5 rounded-2xl relative overflow-hidden'
|
||||
style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
|
||||
{/* 装饰圆形 */}
|
||||
<View className='absolute -right-6 -top-6 w-32 h-32 rounded-full opacity-10 bg-white' />
|
||||
<View className='absolute -left-4 -bottom-4 w-24 h-24 rounded-full opacity-10 bg-white' />
|
||||
|
||||
<View className='relative z-10'>
|
||||
{/* 用户信息行 */}
|
||||
<View className='flex items-center justify-between mb-4'>
|
||||
<View className='flex items-center gap-3'>
|
||||
{user?.avatar ? (
|
||||
<Image className='w-12 h-12 rounded-full border-2 border-white' src={user.avatar} mode='aspectFill' />
|
||||
) : (
|
||||
<View className='w-12 h-12 rounded-full bg-white bg-opacity-30 flex items-center justify-center'>
|
||||
<Text className='text-xl text-white'>👤</Text>
|
||||
</View>
|
||||
)}
|
||||
<View>
|
||||
<Text className='text-white text-lg font-bold'>{user?.nickname || user?.phone || '用户'}</Text>
|
||||
<View className='flex items-center gap-2 mt-1'>
|
||||
<MemberBadge levelName={memberData?.memberLevel || user?.memberLevelName || '普通会员'} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{/* 升级按钮 */}
|
||||
<View
|
||||
className='px-3 py-1.5 rounded-full flex items-center gap-1'
|
||||
style={{ backgroundColor: 'rgba(255,255,255,0.25)' }}
|
||||
onClick={handleUpgrade}
|
||||
>
|
||||
<Text className='text-white text-xs'>{memberData?.isDealer ? '已开通' : '升级会员'}</Text>
|
||||
<Text className='text-white text-xs'>→</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 佣金数据 */}
|
||||
<View className='flex justify-around pt-4 border-t border-white border-opacity-20'>
|
||||
<View className='text-center'>
|
||||
<Text className='text-white text-2xl font-bold block'>
|
||||
{memberLoading ? '...' : (memberData?.totalCommission || 0).toFixed(2)}
|
||||
</Text>
|
||||
<Text className='text-white text-xs opacity-80 mt-1'>累计佣金(元)</Text>
|
||||
</View>
|
||||
<View className='w-px bg-white opacity-20' />
|
||||
<View className='text-center'>
|
||||
<Text className='text-white text-2xl font-bold block'>
|
||||
{memberLoading ? '...' : (memberData?.commission || 0).toFixed(2)}
|
||||
</Text>
|
||||
<Text className='text-white text-xs opacity-80 mt-1'>可提现(元)</Text>
|
||||
</View>
|
||||
<View className='w-px bg-white opacity-20' />
|
||||
<View className='text-center'>
|
||||
<Text className='text-white text-2xl font-bold block'>
|
||||
{memberLoading ? '...' : (memberData?.teamCount || 0)}
|
||||
</Text>
|
||||
<Text className='text-white text-xs opacity-80 mt-1'>团队人数</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 功能入口卡片 */}
|
||||
<View className='bg-white mx-3 mt-3 rounded-xl overflow-hidden'>
|
||||
<View className='px-4 py-3 border-b border-gray-100'>
|
||||
<Text className='text-base font-medium text-gray-800'>分销管理</Text>
|
||||
</View>
|
||||
{memberFeatures.map((item, idx) => (
|
||||
<View
|
||||
key={item.label}
|
||||
className={`flex items-center justify-between px-4 py-3 ${
|
||||
idx < memberFeatures.length - 1 ? 'border-b border-gray-50' : ''
|
||||
}`}
|
||||
onClick={() => handleFeatureClick(item.url)}
|
||||
>
|
||||
<View className='flex items-center gap-3'>
|
||||
<Text className='text-xl'>{item.icon}</Text>
|
||||
<Text className='text-sm text-gray-700'>{item.label}</Text>
|
||||
</View>
|
||||
<View className='flex items-center gap-2'>
|
||||
{item.value && (
|
||||
<Text className='text-sm font-medium text-orange-500'>{item.value}</Text>
|
||||
)}
|
||||
<Text className='text-gray-300 text-sm'>›</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 会员权益 */}
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||||
<View className='flex items-center justify-between mb-3'>
|
||||
<Text className='text-base font-medium text-gray-800'>会员权益</Text>
|
||||
<Text
|
||||
className='text-xs text-blue-500'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/member-upgrade/index' })}
|
||||
>
|
||||
了解更多 ›
|
||||
</Text>
|
||||
</View>
|
||||
<View className='grid grid-cols-2 gap-3'>
|
||||
{memberBenefits.map((benefit, idx) => (
|
||||
<View key={idx} className='flex items-center gap-2 bg-gray-50 rounded-lg px-3 py-2'>
|
||||
<Text className='text-lg'>{benefit.icon}</Text>
|
||||
<Text className='text-xs text-gray-600 flex-1'>{benefit.text}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 推广赚钱 */}
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>推广赚钱</Text>
|
||||
<View className='bg-gradient-to-r from-orange-50 to-red-50 rounded-xl p-4'>
|
||||
<View className='flex items-start gap-3'>
|
||||
<View className='w-12 h-12 rounded-full bg-orange-100 flex items-center justify-center'>
|
||||
<Text className='text-2xl'>🎁</Text>
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-orange-600 font-medium block'>邀请好友赚佣金</Text>
|
||||
<Text className='text-gray-500 text-xs mt-1 block'>
|
||||
每邀请1位好友注册并消费,可获得佣金分成
|
||||
</Text>
|
||||
<View className='flex gap-2 mt-2'>
|
||||
<View
|
||||
className='px-3 py-1.5 bg-orange-500 rounded-full'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/promotion/index' })}
|
||||
>
|
||||
<Text className='text-white text-xs'>立即推广</Text>
|
||||
</View>
|
||||
<View
|
||||
className='px-3 py-1.5 border border-orange-500 rounded-full'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/invite-record/index' })}
|
||||
>
|
||||
<Text className='text-orange-500 text-xs'>邀请记录</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 团队概况 */}
|
||||
{teamData && teamData.length > 0 && (
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||||
<View className='flex items-center justify-between mb-3'>
|
||||
<Text className='text-base font-medium text-gray-800'>团队概况</Text>
|
||||
<Text
|
||||
className='text-xs text-blue-500'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/team/index' })}
|
||||
>
|
||||
查看全部 ›
|
||||
</Text>
|
||||
</View>
|
||||
<View className='grid grid-cols-3 gap-2'>
|
||||
<View className='bg-blue-50 rounded-lg p-3 text-center'>
|
||||
<Text className='text-blue-600 text-xl font-bold block'>{teamData.length}</Text>
|
||||
<Text className='text-gray-500 text-xs mt-1'>团队人数</Text>
|
||||
</View>
|
||||
<View className='bg-green-50 rounded-lg p-3 text-center'>
|
||||
<Text className='text-green-600 text-xl font-bold block'>
|
||||
{teamData.filter(m => (m as any)?.isActive).length}
|
||||
</Text>
|
||||
<Text className='text-gray-500 text-xs mt-1'>活跃成员</Text>
|
||||
</View>
|
||||
<View className='bg-purple-50 rounded-lg p-3 text-center'>
|
||||
<Text className='text-purple-600 text-xl font-bold block'>
|
||||
{teamData.filter(m => (m as any)?.isMember).length}
|
||||
</Text>
|
||||
<Text className='text-gray-500 text-xs mt-1'>会员人数</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className='h-6' />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default MemberPage
|
||||
260
src_bak/pages/user/points-record.tsx
Normal file
260
src_bak/pages/user/points-record.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useRequest } from '@/hooks/useRequest'
|
||||
import { pageUserPointsLog, getUserPointsInfo, type UserPointsInfo } from '@/api/system/user/points'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '积分明细',
|
||||
enablePullDownRefresh: true,
|
||||
})
|
||||
|
||||
// 积分类型映射
|
||||
const TYPE_MAP: Record<number, { label: string; icon: string; color: string; bgColor: string }> = {
|
||||
1: { label: '获得', icon: '📈', color: 'text-green-600', bgColor: 'bg-green-50' },
|
||||
2: { label: '消费', icon: '📉', color: 'text-red-500', bgColor: 'bg-red-50' },
|
||||
3: { label: '过期', icon: '⏰', color: 'text-gray-500', bgColor: 'bg-gray-50' },
|
||||
4: { label: '调整', icon: '🔧', color: 'text-blue-500', bgColor: 'bg-blue-50' },
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ label: '全部', value: -1 },
|
||||
{ label: '获得', value: 1 },
|
||||
{ label: '消费', value: 2 },
|
||||
{ label: '过期', value: 3 },
|
||||
]
|
||||
|
||||
const PointsRecordPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState(-1)
|
||||
const [page, setPage] = useState(1)
|
||||
const [logs, setLogs] = useState<UserPointsLog[]>([])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [pointsInfo, setPointsInfo] = useState<UserPointsInfo>({ points: 0, totalEarned: 0, totalUsed: 0, expiringSoon: 0 })
|
||||
|
||||
// 获取积分统计信息
|
||||
const { run: fetchPointsInfo, loading: infoLoading } = useRequest(getUserPointsInfo, {
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
setPointsInfo(data)
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error('获取积分信息失败:', err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取积分记录
|
||||
const { run: fetchLogs, loading } = useRequest(pageUserPointsLog, {
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
if (data?.list) {
|
||||
if (page === 1) {
|
||||
setLogs(data.list)
|
||||
} else {
|
||||
setLogs(prev => [...prev, ...data.list])
|
||||
}
|
||||
setHasMore(data.list.length >= 20)
|
||||
}
|
||||
Taro.stopPullDownRefresh()
|
||||
},
|
||||
onError: (err) => {
|
||||
Taro.showToast({ title: err.message || '加载失败', icon: 'none' })
|
||||
Taro.stopPullDownRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadData = (pageNum: number = 1) => {
|
||||
const params: any = { page: pageNum, limit: 20 }
|
||||
if (activeTab !== -1) {
|
||||
params.type = activeTab
|
||||
}
|
||||
fetchLogs(params)
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
loadData(1)
|
||||
fetchPointsInfo() // 加载积分统计信息
|
||||
}, [activeTab])
|
||||
|
||||
// 下拉刷新(Taro 自动识别此函数)
|
||||
function onPullDownRefresh() {
|
||||
setPage(1)
|
||||
loadData(1)
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (loading || !hasMore) return
|
||||
const nextPage = page + 1
|
||||
setPage(nextPage)
|
||||
loadData(nextPage)
|
||||
}
|
||||
|
||||
// 格式化积分
|
||||
const formatPoints = (points?: number) => {
|
||||
if (!points && points !== 0) return '0'
|
||||
return points > 0 ? `+${points}` : String(points)
|
||||
}
|
||||
|
||||
// 获取类型信息
|
||||
const getTypeInfo = (type?: number) => {
|
||||
return TYPE_MAP[type || 0] || { label: '未知', icon: '❓', color: 'text-gray-500', bgColor: 'bg-gray-50' }
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr?: string) => {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const hour = date.getHours().toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
// 按日期分组
|
||||
const groupLogsByDate = () => {
|
||||
const groups: { date: string; logs: UserPointsLog[] }[] = []
|
||||
let currentDate = ''
|
||||
let currentGroup: UserPointsLog[] = []
|
||||
|
||||
logs.forEach(log => {
|
||||
const date = log.createTime ? log.createTime.split(' ')[0] : ''
|
||||
if (date !== currentDate) {
|
||||
if (currentGroup.length > 0) {
|
||||
groups.push({ date: currentDate, logs: currentGroup })
|
||||
}
|
||||
currentDate = date
|
||||
currentGroup = [log]
|
||||
} else {
|
||||
currentGroup.push(log)
|
||||
}
|
||||
})
|
||||
|
||||
if (currentGroup.length > 0) {
|
||||
groups.push({ date: currentDate, logs: currentGroup })
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
const groupedLogs = groupLogsByDate()
|
||||
|
||||
return (
|
||||
<View className='min-h-screen' style={{ background: 'linear-gradient(to bottom, #fff7ed, #ffffff)' }}>
|
||||
{/* 顶部统计卡片 */}
|
||||
<View className='bg-white mx-3 mt-3 rounded-xl p-4 shadow-sm'>
|
||||
<View className='flex items-center justify-between'>
|
||||
<View className='text-center flex-1'>
|
||||
<Text className='text-2xl font-bold text-orange-500 block'>{infoLoading ? '-' : pointsInfo.points.toLocaleString()}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>当前积分</Text>
|
||||
</View>
|
||||
<View className='h-10 w-px bg-gray-200' />
|
||||
<View className='text-center flex-1'>
|
||||
<Text className='text-2xl font-bold text-green-500 block'>{infoLoading ? '-' : `+${pointsInfo.totalEarned.toLocaleString()}`}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>累计获得</Text>
|
||||
</View>
|
||||
<View className='h-10 w-px bg-gray-200' />
|
||||
<View className='text-center flex-1'>
|
||||
<Text className='text-2xl font-bold text-red-500 block'>{infoLoading ? '-' : `-${pointsInfo.totalUsed.toLocaleString()}`}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>累计使用</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Tab 栏 */}
|
||||
<View className='bg-white flex mt-3'>
|
||||
{tabs.map(tab => (
|
||||
<View
|
||||
key={tab.value}
|
||||
className={`flex-1 text-center py-3 relative ${
|
||||
activeTab === tab.value ? 'text-orange-500 font-medium' : 'text-gray-600'
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab.value)}
|
||||
>
|
||||
<Text className='text-sm'>{tab.label}</Text>
|
||||
{activeTab === tab.value && (
|
||||
<View className='absolute bottom-0 w-8 h-px bg-orange-500 rounded' style={{ left: '50%', transform: 'translateX(-50%)' }} />
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 日志列表 */}
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='h-full'
|
||||
onScrollToLower={loadMore}
|
||||
>
|
||||
{logs.length === 0 && !loading ? (
|
||||
<View className='text-center py-16'>
|
||||
<Text className='text-6xl mb-4 block'>📊</Text>
|
||||
<Text className='text-base text-gray-400 mb-2 block'>暂无积分记录</Text>
|
||||
<Text className='text-sm text-gray-300'>快去购物、签到获取积分吧</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='p-3'>
|
||||
{groupedLogs.map(group => (
|
||||
<View key={group.date} className='mb-3'>
|
||||
{/* 日期分隔 */}
|
||||
<View className='flex items-center mb-2'>
|
||||
<View className='flex-1 h-px bg-gray-200' />
|
||||
<Text className='text-xs text-gray-400 mx-3'>{group.date}</Text>
|
||||
<View className='flex-1 h-px bg-gray-200' />
|
||||
</View>
|
||||
|
||||
{/* 当日记录 */}
|
||||
{group.logs.map(log => {
|
||||
const typeInfo = getTypeInfo(log.type)
|
||||
return (
|
||||
<View key={log.logId} className='bg-white rounded-lg p-4 mb-2 shadow-sm'>
|
||||
<View className='flex justify-between items-center'>
|
||||
<View className='flex-1'>
|
||||
<View className='flex items-center gap-2'>
|
||||
<View className={`w-8 h-8 rounded-full ${typeInfo.bgColor} flex items-center justify-center`}>
|
||||
<Text className='text-base'>{typeInfo.icon}</Text>
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm text-gray-800 block font-medium'>{log.reason || typeInfo.label}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>{formatTime(log.createTime)}</Text>
|
||||
{log.orderId && (
|
||||
<Text className='text-xs text-gray-400 mt-0 block'>订单: {log.orderId}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className='text-right ml-3'>
|
||||
<Text className={`text-lg font-bold ${typeInfo.color}`}>
|
||||
{formatPoints(log.points)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* 加载更多 */}
|
||||
{hasMore && (
|
||||
<View className='text-center py-4'>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
{loading ? '加载中...' : '上拉加载更多'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{!hasMore && logs.length > 0 && (
|
||||
<View className='text-center py-4'>
|
||||
<Text className='text-xs text-gray-400'>— 已经到底了 —</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default PointsRecordPage
|
||||
203
src_bak/pages/user/profile.tsx
Normal file
203
src_bak/pages/user/profile.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Image, Input, Button as TaroButton } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import BottomButton from '@/components/common/BottomButton'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { updateUser } from '@/api/system/user'
|
||||
import type { User } from '@/api/system/user/model'
|
||||
import { TenantId } from '@/config/app'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '个人信息',
|
||||
})
|
||||
|
||||
const genderOptions = ['保密', '男', '女']
|
||||
|
||||
const ProfilePage: React.FC = () => {
|
||||
const { user, refreshUser } = useUser()
|
||||
const [form, setForm] = useState<Partial<User>>({})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showGenderSheet, setShowGenderSheet] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setForm({
|
||||
userId: (user as any)?.userId || (user as any)?.id,
|
||||
nickname: (user as any)?.nickname || '',
|
||||
avatar: (user as any)?.avatar || '',
|
||||
gender: (user as any)?.gender ?? 0,
|
||||
phone: (user as any)?.phone || '',
|
||||
})
|
||||
}
|
||||
}, [user])
|
||||
|
||||
// 微信头像选择回调(open-type="chooseAvatar")
|
||||
const handleWechatAvatar = (e: any) => {
|
||||
const { avatarUrl } = e.detail
|
||||
if (!avatarUrl) return
|
||||
Taro.showLoading({ title: '上传中...' })
|
||||
Taro.uploadFile({
|
||||
url: 'https://server.websoft.top/api/oss/upload',
|
||||
filePath: avatarUrl,
|
||||
name: 'file',
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
TenantId,
|
||||
},
|
||||
success: (uploadRes) => {
|
||||
const data = JSON.parse(uploadRes.data)
|
||||
if (data.code === 0 && data.data?.url) {
|
||||
setForm(prev => ({ ...prev, avatar: data.data.url }))
|
||||
Taro.showToast({ title: '上传成功', icon: 'success' })
|
||||
} else {
|
||||
Taro.showToast({ title: data.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({ title: '上传失败', icon: 'none' })
|
||||
},
|
||||
complete: () => {
|
||||
Taro.hideLoading()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 选择性别
|
||||
const handleGenderSelect = (index: number) => {
|
||||
setForm(prev => ({ ...prev, gender: index }))
|
||||
setShowGenderSheet(false)
|
||||
}
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
if (!form.nickname?.trim()) {
|
||||
Taro.showToast({ title: '昵称不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
await updateUser(form as User)
|
||||
await refreshUser()
|
||||
Taro.showToast({ title: '保存成功', icon: 'success' })
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
Taro.showToast({ title: '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const displayId = (user as any)?.userId || (user as any)?.id || ''
|
||||
const registerTime = (user as any)?.createTime || (user as any)?.createdAt || ''
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 pb-32'>
|
||||
{/* 头像 - 使用 Taro 原生 Button 支持 open-type="chooseAvatar" */}
|
||||
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-700 w-20'>头像</Text>
|
||||
<View className='flex items-center gap-2'>
|
||||
<TaroButton
|
||||
openType='chooseAvatar'
|
||||
onChooseAvatar={handleWechatAvatar}
|
||||
plain
|
||||
style={{
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
lineHeight: '40px',
|
||||
width: 'auto',
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
{form.avatar ? (
|
||||
<Image className='w-10 h-10 rounded-full' src={form.avatar} mode='aspectFill' />
|
||||
) : (
|
||||
<View className='w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center'>
|
||||
<Text className='text-gray-300 text-xs'>头像</Text>
|
||||
</View>
|
||||
)}
|
||||
</TaroButton>
|
||||
<Text className='text-gray-300 text-sm'>{'>'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 昵称 - 支持获取微信昵称 */}
|
||||
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-700 w-20'>昵称</Text>
|
||||
<Input
|
||||
className='flex-1 text-right text-sm text-gray-600'
|
||||
placeholder='请输入昵称'
|
||||
value={form.nickname || ''}
|
||||
onInput={(e: any) => setForm(prev => ({ ...prev, nickname: e.detail.value }))}
|
||||
maxlength={20}
|
||||
type='nickname'
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 性别 */}
|
||||
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50' onClick={() => setShowGenderSheet(true)}>
|
||||
<Text className='text-sm text-gray-700 w-20'>性别</Text>
|
||||
<View className='flex items-center gap-2'>
|
||||
<Text className='text-sm text-gray-600'>{genderOptions[form.gender ?? 0]}</Text>
|
||||
<Text className='text-gray-300 text-sm'>{'>'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 手机号 - 不可编辑 */}
|
||||
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-700 w-20'>手机号</Text>
|
||||
<Text className='text-sm text-gray-600'>{form.phone || '未绑定'}</Text>
|
||||
</View>
|
||||
|
||||
{/* 用户ID */}
|
||||
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-700 w-20'>用户ID</Text>
|
||||
<Text className='text-sm text-gray-600'>{displayId || '未设置'}</Text>
|
||||
</View>
|
||||
|
||||
{/* 注册时间 */}
|
||||
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-700 w-20'>注册时间</Text>
|
||||
<Text className='text-sm text-gray-600'>{registerTime || '未知'}</Text>
|
||||
</View>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<BottomButton
|
||||
text='保存'
|
||||
onClick={handleSave}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
{/* 性别选择弹窗 */}
|
||||
{showGenderSheet && (
|
||||
<View className='fixed inset-0 z-50'>
|
||||
<View className='absolute inset-0 bg-black bg-opacity-50' onClick={() => setShowGenderSheet(false)} />
|
||||
<View className='absolute bottom-0 left-0 right-0 bg-white rounded-t-xl'>
|
||||
<View className='px-4 py-3 border-b border-gray-50'>
|
||||
<Text className='text-base font-medium text-gray-800'>选择性别</Text>
|
||||
</View>
|
||||
{genderOptions.map((label, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className='px-4 py-3 border-b border-gray-50 flex items-center justify-between'
|
||||
onClick={() => handleGenderSelect(index)}
|
||||
>
|
||||
<Text className={`text-sm ${(form.gender ?? 0) === index ? 'text-green-600 font-medium' : 'text-gray-700'}`}>
|
||||
{label}
|
||||
</Text>
|
||||
{(form.gender ?? 0) === index && <Text className='text-green-600'>✓</Text>}
|
||||
</View>
|
||||
))}
|
||||
<View className='px-4 py-3' onClick={() => setShowGenderSheet(false)}>
|
||||
<Text className='text-sm text-gray-400 text-center block'>取消</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfilePage
|
||||
3
src_bak/pages/user/promotion/index.config.ts
Normal file
3
src_bak/pages/user/promotion/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '分销推广',
|
||||
}
|
||||
123
src_bak/pages/user/promotion/index.tsx
Normal file
123
src_bak/pages/user/promotion/index.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Button, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '分销推广',
|
||||
})
|
||||
|
||||
const PromotionPage: React.FC = () => {
|
||||
const { user, isLoggedIn } = useUser()
|
||||
const [inviteCode, setInviteCode] = useState('')
|
||||
const [inviteLink, setInviteLink] = useState('')
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState('')
|
||||
const scrollHeight = useScrollHeight(44)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
return
|
||||
}
|
||||
// 生成邀请码和链接
|
||||
const code = (user?.userId || user?.id)?.toString() || Taro.getStorageSync('UserId')?.toString() || ''
|
||||
setInviteCode(code)
|
||||
setInviteLink(`https://shop-api.websoft.top/invite?ref=${code}`)
|
||||
setQrCodeUrl(`https://shop-api.websoft.top/api/wx-login/getOrderQRCodeUnlimited/uid_${code}`)
|
||||
}, [isLoggedIn, user])
|
||||
|
||||
const handleCopyLink = () => {
|
||||
Taro.setClipboardData({
|
||||
data: inviteLink,
|
||||
success: () => {
|
||||
Taro.showToast({ title: '链接已复制', icon: 'success' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleShare = () => {
|
||||
Taro.showToast({ title: '请点击右上角分享', icon: 'none' })
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
{/* 推广卡片 */}
|
||||
<View className='mx-3 mt-3 p-4 rounded-xl' style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
|
||||
<Text className='text-white text-lg font-bold mb-1 block'>我的推广链接</Text>
|
||||
<Text className='text-white text-sm opacity-80 mb-3 block'>分享给好友,好友消费您可获得佣金</Text>
|
||||
<View className='bg-white rounded-lg p-3 mb-3'>
|
||||
<Text className='text-xs text-gray-500 mb-1 block'>邀请码</Text>
|
||||
<Text className='text-base font-bold text-gray-800'>{inviteCode}</Text>
|
||||
</View>
|
||||
{/* 邀请二维码 */}
|
||||
{qrCodeUrl ? (
|
||||
<View className='bg-white rounded-lg p-3 flex flex-col items-center'>
|
||||
<Text className='text-xs text-gray-500 mb-2 block'>扫码邀请好友</Text>
|
||||
<Image
|
||||
src={qrCodeUrl}
|
||||
style={{ width: '180px', height: '180px' }}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* 推广链接 */}
|
||||
<View className='mx-3 mt-3 p-4 bg-white rounded-lg'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-2 block'>推广链接</Text>
|
||||
<View className='bg-gray-50 rounded-lg p-3 mb-3'>
|
||||
<Text className='text-xs text-gray-600 break-all'>{inviteLink}</Text>
|
||||
</View>
|
||||
<View className='grid grid-cols-2 gap-2'>
|
||||
<Button
|
||||
className='rounded-full'
|
||||
style={{ backgroundColor: '#0e932e' }}
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
复制链接
|
||||
</Button>
|
||||
<Button
|
||||
className='rounded-full'
|
||||
style={{ backgroundColor: '#1989fa' }}
|
||||
onClick={handleShare}
|
||||
>
|
||||
分享好友
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 推广说明 */}
|
||||
<View className='mx-3 mt-3 p-4 bg-white rounded-lg'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-3 block'>推广规则</Text>
|
||||
<View className='flex items-start gap-2 mb-2'>
|
||||
<Text className='text-green-500 mt-0'>1.</Text>
|
||||
<Text className='text-sm text-gray-600 flex-1'>分享您的专属链接或邀请码给好友</Text>
|
||||
</View>
|
||||
<View className='flex items-start gap-2 mb-2'>
|
||||
<Text className='text-green-500 mt-0'>2.</Text>
|
||||
<Text className='text-sm text-gray-600 flex-1'>好友通过您的链接注册并成为会员</Text>
|
||||
</View>
|
||||
<View className='flex items-start gap-2 mb-2'>
|
||||
<Text className='text-green-500 mt-0'>3.</Text>
|
||||
<Text className='text-sm text-gray-600 flex-1'>好友在平台消费,您可获得相应佣金分成</Text>
|
||||
</View>
|
||||
<View className='flex items-start gap-2'>
|
||||
<Text className='text-green-500 mt-0'>4.</Text>
|
||||
<Text className='text-sm text-gray-600 flex-1'>佣金需在好友订单完成后再申请提现</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='h-6' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromotionPage
|
||||
196
src_bak/pages/user/recharge-record/index.tsx
Normal file
196
src_bak/pages/user/recharge-record/index.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useRequest } from '@/hooks/useRequest'
|
||||
import { pageUserRechargeOrder, type RechargeOrder } from '@/api/shop/shopRechargeOrder'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '充值记录',
|
||||
})
|
||||
|
||||
const tabs = [
|
||||
{ label: '全部', value: -1 },
|
||||
{ label: '已支付', value: 20 },
|
||||
{ label: '待支付', value: 10 },
|
||||
]
|
||||
|
||||
const RechargeRecordPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState(-1)
|
||||
const [page, setPage] = useState(1)
|
||||
const [orders, setOrders] = useState<RechargeOrder[]>([])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
|
||||
// 获取充值记录
|
||||
const { run: fetchOrders, loading } = useRequest(pageUserRechargeOrder, {
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
if (data?.records) {
|
||||
if (page === 1) {
|
||||
setOrders(data.records)
|
||||
} else {
|
||||
setOrders(prev => [...prev, ...data.records])
|
||||
}
|
||||
setHasMore(data.records.length >= 20)
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
Taro.showToast({ title: err.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadData = (pageNum: number = 1) => {
|
||||
const params: any = { page: pageNum, limit: 20 }
|
||||
if (activeTab !== -1) {
|
||||
params.payStatus = activeTab
|
||||
}
|
||||
fetchOrders(params)
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
setOrders([])
|
||||
loadData(1)
|
||||
}, [activeTab])
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (loading || !hasMore) return
|
||||
const nextPage = page + 1
|
||||
setPage(nextPage)
|
||||
loadData(nextPage)
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (money?: string) => {
|
||||
if (!money) return '0.00'
|
||||
return parseFloat(money).toFixed(2)
|
||||
}
|
||||
|
||||
// 格式化时间戳
|
||||
const formatTime = (timestamp?: number) => {
|
||||
if (!timestamp) return '-'
|
||||
const date = new Date(timestamp * 1000)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
// 获取支付状态文本
|
||||
const getStatusText = (status?: number) => {
|
||||
return status === 20 ? '已支付' : '待支付'
|
||||
}
|
||||
|
||||
// 获取支付状态颜色
|
||||
const getStatusColor = (status?: number) => {
|
||||
return status === 20 ? 'text-green-600' : 'text-gray-400'
|
||||
}
|
||||
|
||||
// 获取支付方式文本
|
||||
const getPayMethodText = (method?: string) => {
|
||||
if (!method) return '-'
|
||||
if (method.includes('wechat') || method.includes('wx')) return '微信支付'
|
||||
if (method.includes('alipay') || method.includes('ali')) return '支付宝'
|
||||
return method
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
{/* Tab 栏 */}
|
||||
<View className='bg-white flex'>
|
||||
{tabs.map(tab => (
|
||||
<View
|
||||
key={tab.value}
|
||||
className={`flex-1 text-center py-3 relative ${
|
||||
activeTab === tab.value ? 'text-orange-500 font-medium' : 'text-gray-600'
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab.value)}
|
||||
>
|
||||
<Text className='text-sm'>{tab.label}</Text>
|
||||
{activeTab === tab.value && (
|
||||
<View className='absolute bottom-0 left-0 right-0 flex justify-center'>
|
||||
<View className='w-8 h-px bg-orange-500 rounded' />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 充值记录列表 */}
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='flex-1'
|
||||
onScrollToLower={loadMore}
|
||||
>
|
||||
{orders.length === 0 && !loading ? (
|
||||
<View className='text-center py-16'>
|
||||
<Text className='text-sm text-gray-400'>暂无充值记录</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='p-3'>
|
||||
{orders.map(order => (
|
||||
<View key={order.orderId} className='bg-white rounded-lg p-4 mb-2'>
|
||||
<View className='flex justify-between items-center'>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm text-gray-800 block'>充值订单</Text>
|
||||
<Text className='text-xs text-gray-400 mt-0 block'>
|
||||
{order.createTime || formatTime(order.payTime)}
|
||||
</Text>
|
||||
{order.orderNo && (
|
||||
<Text className='text-xs text-gray-400 mt-0 block'>
|
||||
订单号: {order.orderNo}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='text-right'>
|
||||
<Text className='text-base font-bold text-orange-600'>
|
||||
+¥{formatMoney(order.actualMoney || order.payPrice)}
|
||||
</Text>
|
||||
<Text className={`text-xs mt-0 block ${getStatusColor(order.payStatus)}`}>
|
||||
{getStatusText(order.payStatus)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 详细信息 */}
|
||||
<View className='mt-2 pt-2 border-t border-gray-100 flex justify-between text-xs text-gray-400'>
|
||||
<Text>支付方式: {getPayMethodText(order.payMethod)}</Text>
|
||||
{order.payStatus === 20 && order.balance && (
|
||||
<Text>余额: ¥{formatMoney(order.balance)}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 赠送金额提示 */}
|
||||
{order.payStatus === 20 && order.giftMoney && parseFloat(order.giftMoney) > 0 && (
|
||||
<View className='mt-1 text-xs text-green-600'>
|
||||
含赠送: ¥{formatMoney(order.giftMoney)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* 加载更多 */}
|
||||
{hasMore && (
|
||||
<View className='text-center py-4'>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
{loading ? '加载中...' : '上拉加载更多'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{!hasMore && orders.length > 0 && (
|
||||
<View className='text-center py-4'>
|
||||
<Text className='text-xs text-gray-400'>没有更多了</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default RechargeRecordPage
|
||||
213
src_bak/pages/user/recharge.tsx
Normal file
213
src_bak/pages/user/recharge.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { useRequest } from '@/hooks/useRequest'
|
||||
import { createUserRecharge, payUserRecharge, type UserRecharge } from '@/api/shop/shopRecharge'
|
||||
import { getUserBalance } from '@/api/system/user/balance'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '充值',
|
||||
})
|
||||
|
||||
const amounts = [10, 20, 50, 100, 200, 500]
|
||||
|
||||
const RechargePage: React.FC = () => {
|
||||
const [selected, setSelected] = useState(0)
|
||||
const [customAmount, setCustomAmount] = useState('')
|
||||
const [balance, setBalance] = useState<string>('')
|
||||
|
||||
// 获取当前余额
|
||||
const { run: runGetBalance } = useRequest(getUserBalance, {
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
setBalance(data.balance)
|
||||
}
|
||||
})
|
||||
|
||||
// 页面显示时刷新余额
|
||||
useDidShow(() => {
|
||||
runGetBalance()
|
||||
})
|
||||
|
||||
// 发起支付
|
||||
const handlePay = async (rechargeOrder: UserRecharge) => {
|
||||
try {
|
||||
const payParams = await payUserRecharge(rechargeOrder.rechargeId!)
|
||||
if (payParams) {
|
||||
// 后端返回已支付(回调丢失自动修复场景)
|
||||
if ((payParams as Record<string, unknown>).paid === 'true') {
|
||||
Taro.showToast({ title: '充值成功', icon: 'success' })
|
||||
try {
|
||||
const balanceData = await getUserBalance()
|
||||
setBalance(balanceData.balance)
|
||||
} catch (e) { }
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack()
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
// 调用微信支付
|
||||
await Taro.requestPayment({
|
||||
timeStamp: payParams.timeStamp,
|
||||
nonceStr: payParams.nonceStr,
|
||||
package: payParams.package,
|
||||
signType: payParams.signType,
|
||||
paySign: payParams.paySign,
|
||||
success: async () => {
|
||||
Taro.showToast({ title: '充值成功', icon: 'success' })
|
||||
// 立即刷新余额
|
||||
try {
|
||||
const balanceData = await getUserBalance()
|
||||
setBalance(balanceData.balance)
|
||||
} catch (e) {
|
||||
// 忽略刷新失败
|
||||
}
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack()
|
||||
}, 1500)
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg.indexOf('cancel') > -1) {
|
||||
Taro.showToast({ title: '已取消支付', icon: 'none' })
|
||||
} else {
|
||||
Taro.showToast({ title: '支付失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '支付失败'
|
||||
Taro.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 创建充值订单
|
||||
const { run: runCreate, loading: creating } = useRequest(createUserRecharge, {
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
handlePay(data)
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
Taro.showToast({ title: err.message || '创建订单失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
|
||||
const handleRecharge = () => {
|
||||
const amount = customAmount || String(amounts[selected])
|
||||
if (!amount || Number(amount) <= 0) {
|
||||
Taro.showToast({ title: '请选择充值金额', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (Number(amount) < 1) {
|
||||
Taro.showToast({ title: '充值金额不能小于1元', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (Number(amount) > 5000) {
|
||||
Taro.showToast({ title: '单次充值金额不能超过5000元', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 创建充值订单
|
||||
runCreate({
|
||||
amount: amount,
|
||||
payType: 0, // 微信支付
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50 flex flex-col">
|
||||
<ScrollView scrollY className="flex-1">
|
||||
<View className="p-4">
|
||||
{/* 当前余额提示 */}
|
||||
{balance && (
|
||||
<View className="bg-gradient-to-r from-orange-500 to-orange-600 rounded-xl p-4 mb-3">
|
||||
<Text className="text-white text-xs opacity-80 block">当前余额</Text>
|
||||
<Text className="text-white text-2xl font-bold block mt-1">¥{parseFloat(balance).toFixed(2)}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 充值金额 */}
|
||||
<View className="bg-white rounded-xl p-4">
|
||||
<Text className="text-sm font-medium text-gray-800 mb-3 block">选择充值金额</Text>
|
||||
<View className="grid grid-cols-3 gap-2">
|
||||
{amounts.map((amount, idx) => (
|
||||
<View
|
||||
key={amount}
|
||||
className={`py-3 rounded-lg text-center border ${
|
||||
selected === idx && !customAmount
|
||||
? 'border-orange-500 bg-orange-50'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
onClick={() => { setSelected(idx); setCustomAmount('') }}
|
||||
>
|
||||
<Text className={`text-base font-medium ${selected === idx && !customAmount ? 'text-orange-600' : 'text-gray-700'}`}>
|
||||
¥{amount}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{/* 自定义金额 */}
|
||||
<View className="mt-3">
|
||||
<Text className="text-sm text-gray-600 mb-1 block">自定义金额</Text>
|
||||
<View className="flex items-center border border-gray-200 rounded-lg px-3 py-2">
|
||||
<Text className="text-sm text-gray-500 mr-1">¥</Text>
|
||||
<Input
|
||||
type="digit"
|
||||
className="flex-1 text-sm"
|
||||
placeholder="请输入金额(1-5000)"
|
||||
value={customAmount}
|
||||
onInput={e => setCustomAmount(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 支付方式 */}
|
||||
<View className="bg-white rounded-xl p-4 mt-3">
|
||||
<Text className="text-sm font-medium text-gray-800 mb-3 block">支付方式</Text>
|
||||
<View className="flex items-center justify-between py-2">
|
||||
<View className="flex items-center gap-2">
|
||||
<Text className="text-lg">💳</Text>
|
||||
<Text className="text-sm text-gray-700">微信支付</Text>
|
||||
</View>
|
||||
<View className="w-5 h-5 rounded-full border-2 border-orange-500 flex items-center justify-center">
|
||||
<View className="w-3 h-3 rounded-full bg-orange-500" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 充值说明 */}
|
||||
<View className="bg-white rounded-xl p-4 mt-3">
|
||||
<Text className="text-sm font-medium text-gray-800 mb-2 block">充值说明</Text>
|
||||
<View className="text-xs text-gray-400 leading-5">
|
||||
<Text className="block mb-1">1. 充值金额将立即到账</Text>
|
||||
<Text className="block mb-1">2. 充值后可在商城消费使用</Text>
|
||||
<Text className="block mb-1">3. 充值金额不可提现</Text>
|
||||
<Text className="block">4. 如有问题请联系客服</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部按钮占位 */}
|
||||
<View style={{ height: '80px' }} />
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部充值按钮 - flex 布局 */}
|
||||
<View className="bg-white border-t border-gray-200 px-4 py-3" style={{ paddingBottom: '20px' }}>
|
||||
<View
|
||||
className="py-3 rounded-full text-center"
|
||||
style={{ backgroundColor: '#f97316' }}
|
||||
onClick={handleRecharge}
|
||||
>
|
||||
<Text className="text-white font-medium text-sm">
|
||||
{creating ? '处理中...' : `立即充值 ¥${customAmount || amounts[selected]}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default RechargePage
|
||||
3
src_bak/pages/user/redeem/index.config.ts
Normal file
3
src_bak/pages/user/redeem/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '兑换码充值',
|
||||
})
|
||||
294
src_bak/pages/user/redeem/index.tsx
Normal file
294
src_bak/pages/user/redeem/index.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useRequest } from '@/hooks/useRequest'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
import { useRechargeCode, pageRechargeCodeRecords } from '@/api/shop/shopRechargeCode'
|
||||
import type { ShopRechargeCode } from '@/api/shop/shopRechargeCode/model'
|
||||
import { getUserBalance } from '@/api/system/user'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '兑换码充值',
|
||||
})
|
||||
|
||||
const RedeemPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const maxPopupHeight = useScrollHeight(0)
|
||||
|
||||
// 兑换码输入
|
||||
const [code, setCode] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [redeemSuccess, setRedeemSuccess] = useState(false)
|
||||
const [redeemAmount, setRedeemAmount] = useState('')
|
||||
|
||||
// 兑换记录
|
||||
const [records, setRecords] = useState<ShopRechargeCode[]>([])
|
||||
const [loadingRecords, setLoadingRecords] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
|
||||
// 兑换请求
|
||||
const { run: runUseCode } = useRequest(useRechargeCode, { manual: true })
|
||||
|
||||
// 加载兑换记录
|
||||
const loadRecords = async (isLoadMore = false) => {
|
||||
if (isLoadMore) {
|
||||
if (loadingMore || !hasMore) return
|
||||
setLoadingMore(true)
|
||||
} else {
|
||||
setLoadingRecords(true)
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await pageRechargeCodeRecords({ page: isLoadMore ? page + 1 : 1, limit: 10 })
|
||||
const newRecords = res?.records || []
|
||||
|
||||
if (isLoadMore) {
|
||||
setRecords(prev => [...prev, ...newRecords])
|
||||
setPage(prev => prev + 1)
|
||||
} else {
|
||||
setRecords(newRecords)
|
||||
}
|
||||
|
||||
setHasMore(newRecords.length >= 10)
|
||||
} catch (err) {
|
||||
console.error('加载兑换记录失败', err)
|
||||
} finally {
|
||||
setLoadingRecords(false)
|
||||
setLoadingMore(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
loadRecords()
|
||||
}
|
||||
}, [isLoggedIn])
|
||||
|
||||
// 兑换
|
||||
const handleRedeem = async () => {
|
||||
const trimCode = code.trim().toUpperCase()
|
||||
if (!trimCode) {
|
||||
Taro.showToast({ title: '请输入兑换码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const res = await runUseCode(trimCode)
|
||||
setRedeemSuccess(true)
|
||||
setRedeemAmount(res?.amount || '0')
|
||||
Taro.showToast({ title: '兑换成功', icon: 'success' })
|
||||
|
||||
// 刷新余额(通过事件通知)
|
||||
const pages = Taro.getCurrentPages()
|
||||
const userPage = pages.find(p => p.route === 'pages/user/wallet')
|
||||
if (userPage) {
|
||||
userPage.onShow()
|
||||
}
|
||||
|
||||
// 刷新记录
|
||||
await loadRecords()
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err.message || '兑换失败,兑换码无效或已过期', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
setCode('')
|
||||
setRedeemSuccess(false)
|
||||
setRedeemAmount('')
|
||||
}
|
||||
|
||||
// 复制兑换码
|
||||
const copyCode = (codeStr: string) => {
|
||||
Taro.setClipboardData({
|
||||
data: codeStr,
|
||||
success: () => {
|
||||
Taro.showToast({ title: '已复制', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动到底部加载更多
|
||||
const handleScrollToLower = () => {
|
||||
if (hasMore && !loadingMore) {
|
||||
loadRecords(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 状态文本
|
||||
const getStatusText = (status: number) => {
|
||||
const map: Record<number, string> = { 0: '未使用', 1: '已使用', 2: '已过期' }
|
||||
return map[status] || '未知'
|
||||
}
|
||||
|
||||
// 状态颜色
|
||||
const getStatusColor = (status: number) => {
|
||||
const map: Record<number, string> = { 0: 'text-blue-500', 1: 'text-green-500', 2: 'text-gray-400' }
|
||||
return map[status] || 'text-gray-400'
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex flex-col'>
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='flex-1'
|
||||
onScrollToLower={handleScrollToLower}
|
||||
lowerThreshold={100}
|
||||
>
|
||||
<View className='p-3'>
|
||||
{/* 兑换说明卡片 */}
|
||||
<View className='bg-white rounded-xl p-4 mb-3'>
|
||||
<View className='flex items-start gap-3'>
|
||||
<Text className='text-2xl'>🎫</Text>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm font-medium text-gray-800 block'>兑换说明</Text>
|
||||
<Text className='text-xs text-gray-500 block mt-1'>
|
||||
请输入管理员提供的兑换码,每个兑换码只能使用一次,兑换成功后余额将自动到账。
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 兑换表单 */}
|
||||
{!redeemSuccess ? (
|
||||
<View className='bg-white rounded-xl p-4'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-3 block'>兑换码充值</Text>
|
||||
|
||||
<View className='bg-gray-50 rounded-lg px-4 py-3 mb-4'>
|
||||
<Input
|
||||
className='text-sm'
|
||||
placeholder='请输入兑换码'
|
||||
value={code}
|
||||
onInput={(e) => setCode(e.detail.value)}
|
||||
maxlength={32}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className={`py-3 px-4 rounded-lg text-center text-sm font-medium ${
|
||||
code.trim() && !submitting
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-200 text-gray-400'
|
||||
}`}
|
||||
onClick={handleRedeem}
|
||||
>
|
||||
{submitting ? '兑换中...' : '立即兑换'}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
/* 兑换成功结果 */
|
||||
<View className='bg-white rounded-xl p-4'>
|
||||
<View className='text-center py-6'>
|
||||
<Text className='text-5xl block mb-4'>🎉</Text>
|
||||
<Text className='text-lg font-medium text-gray-800 block'>兑换成功!</Text>
|
||||
<Text className='text-sm text-gray-500 block mt-2'>
|
||||
已成功充值 ¥{redeemAmount},余额已到账
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className='flex gap-3'>
|
||||
<View
|
||||
className='flex-1 py-3 px-4 rounded-lg text-center text-sm font-medium bg-gray-100 text-gray-700'
|
||||
onClick={resetForm}
|
||||
>
|
||||
继续兑换
|
||||
</View>
|
||||
<View
|
||||
className='flex-1 py-3 px-4 rounded-lg text-center text-sm font-medium bg-green-600 text-white'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/wallet' })}
|
||||
>
|
||||
查看余额
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 兑换记录 */}
|
||||
<View className='bg-white rounded-xl mt-3 overflow-hidden'>
|
||||
<View className='p-4 border-b border-gray-100 flex justify-between items-center'>
|
||||
<Text className='text-sm font-medium text-gray-800'>兑换记录</Text>
|
||||
<Text
|
||||
className='text-xs text-gray-400'
|
||||
onClick={() => loadRecords()}
|
||||
>
|
||||
刷新 🔄
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{loadingRecords ? (
|
||||
<View className='py-10 text-center'>
|
||||
<Text className='text-sm text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
) : records.length === 0 ? (
|
||||
<View className='py-10 text-center'>
|
||||
<Text className='text-4xl block mb-3'>📋</Text>
|
||||
<Text className='text-sm text-gray-400'>暂无兑换记录</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
{records.map((record, idx) => (
|
||||
<View
|
||||
key={record.id}
|
||||
className={`p-4 flex justify-between items-center border-b border-gray-50 ${idx === records.length - 1 ? 'border-b-0' : ''}`}
|
||||
>
|
||||
<View className='flex-1'>
|
||||
<View className='flex items-center gap-2'>
|
||||
<Text className={`text-xs ${getStatusColor(record.status)}`}>
|
||||
{getStatusText(record.status)}
|
||||
</Text>
|
||||
{record.status === 1 && (
|
||||
<Text className='text-xs text-green-500'>
|
||||
+¥{Number(record.amount).toFixed(2)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text
|
||||
className='text-xs text-gray-400 mt-1 block'
|
||||
onClick={() => copyCode(record.code)}
|
||||
>
|
||||
码: {record.code} 📋
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-300 mt-1 block'>
|
||||
{record.usedAt || record.createTime}
|
||||
</Text>
|
||||
</View>
|
||||
{record.status === 1 && (
|
||||
<Text className='text-base font-bold text-green-500'>
|
||||
+¥{Number(record.amount).toFixed(2)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
|
||||
{loadingMore && (
|
||||
<View className='py-3 text-center'>
|
||||
<Text className='text-xs text-gray-400'>加载更多...</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!hasMore && records.length > 0 && (
|
||||
<View className='py-3 text-center'>
|
||||
<Text className='text-xs text-gray-400'>没有更多了</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className='h-6' />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default RedeemPage
|
||||
3
src_bak/pages/user/register-pay/index.config.ts
Normal file
3
src_bak/pages/user/register-pay/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '支付注册费',
|
||||
})
|
||||
240
src_bak/pages/user/register-pay/index.tsx
Normal file
240
src_bak/pages/user/register-pay/index.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Image, Input } from '@tarojs/components'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
import { getRegisterPayInfo, getMyRegisterStatus, RegisterPayInfo } from '@/api/shop/shopMemberRegister'
|
||||
import { getUserInfo } from '@/api/passport/login'
|
||||
import PayModal from '@/components/business/PayModal'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '支付注册费',
|
||||
})
|
||||
|
||||
const RegisterPayPage: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const { user, isLoggedIn, refreshUser } = useUser()
|
||||
const scrollHeight = useScrollHeight()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [payInfo, setPayInfo] = useState<RegisterPayInfo | null>(null)
|
||||
const [showPayModal, setShowPayModal] = useState(false)
|
||||
const [userStatus, setUserStatus] = useState<any>(null)
|
||||
|
||||
// 获取URL参数
|
||||
const phone = router.params.phone || ''
|
||||
const registerId = router.params.registerId ? Number(router.params.registerId) : 0
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
return
|
||||
}
|
||||
checkAndFetchData()
|
||||
}, [isLoggedIn])
|
||||
|
||||
// 检查用户状态并获取支付信息
|
||||
const checkAndFetchData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 检查当前用户状态
|
||||
const status = await getMyRegisterStatus()
|
||||
setUserStatus(status)
|
||||
|
||||
// 如果已经是会员,跳转到会员中心
|
||||
if (status?.isDealer) {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '您已经是会员,无需再次支付注册费',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
Taro.navigateBack()
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取支付信息
|
||||
if (phone) {
|
||||
const info = await getRegisterPayInfo(phone)
|
||||
if (info) {
|
||||
setPayInfo(info)
|
||||
} else {
|
||||
Taro.showToast({ title: '未找到支付信息,请联系上级会员', icon: 'none' })
|
||||
}
|
||||
} else {
|
||||
// 如果没有传入手机号,尝试获取当前用户的注册记录
|
||||
if (status?.register) {
|
||||
setPayInfo({
|
||||
id: status.register.id,
|
||||
phone: status.register.phone,
|
||||
realName: status.register.realName,
|
||||
registerFee: status.register.registerFee,
|
||||
dealerName: status.register.dealerName,
|
||||
dealerPhone: status.register.dealerPhone,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('获取数据失败:', e)
|
||||
Taro.showToast({ title: e.message || '获取数据失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 发起支付
|
||||
const handlePay = () => {
|
||||
if (!payInfo) {
|
||||
Taro.showToast({ title: '支付信息加载中,请稍候', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setShowPayModal(true)
|
||||
}
|
||||
|
||||
// 支付成功回调
|
||||
const handlePaySuccess = async () => {
|
||||
setShowPayModal(false)
|
||||
Taro.showModal({
|
||||
title: '支付成功',
|
||||
content: '恭喜!您已成为平台会员,请使用手机号登录查看。',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
// 刷新用户信息
|
||||
refreshUser?.()
|
||||
Taro.redirectTo({ url: '/pages/user/index' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 支付失败回调
|
||||
const handlePayFail = (err: string) => {
|
||||
setShowPayModal(false)
|
||||
Taro.showToast({ title: err || '支付失败', icon: 'none' })
|
||||
}
|
||||
|
||||
if (!isLoggedIn) return null
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
|
||||
<Text className='text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<View className='p-4' style={{ height: scrollHeight }}>
|
||||
{/* 支付卡片 */}
|
||||
<View className='bg-white rounded-2xl overflow-hidden shadow-sm'>
|
||||
{/* 顶部背景 */}
|
||||
<View
|
||||
className='p-6 text-center'
|
||||
style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}
|
||||
>
|
||||
<Text className='text-white text-sm'>注册费</Text>
|
||||
<View className='flex items-center justify-center mt-2'>
|
||||
<Text className='text-white text-3xl font-bold'>¥</Text>
|
||||
<Text className='text-white text-5xl font-bold ml-1'>198</Text>
|
||||
<Text className='text-white text-xl font-medium ml-1'>.00</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 支付信息 */}
|
||||
<View className='p-4'>
|
||||
<View className='flex items-start gap-3 mb-4'>
|
||||
<View className='w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0'>
|
||||
<Text className='text-base'>💡</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className='text-gray-800 text-sm font-medium block'>成为平台会员</Text>
|
||||
<Text className='text-gray-500 text-xs mt-1 block leading-relaxed'>
|
||||
支付注册费后,您将成为平台正式会员,享受会员专属权益和分佣待遇。
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 费用明细 */}
|
||||
<View className='bg-gray-50 rounded-xl p-4 mb-4'>
|
||||
<Text className='text-gray-600 text-sm font-medium mb-3 block'>费用明细</Text>
|
||||
<View className='space-y-2'>
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-gray-500 text-sm'>会员费(上级代收)</Text>
|
||||
<Text className='text-gray-600 text-sm'>¥298.00</Text>
|
||||
</View>
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-gray-500 text-sm'>注册费(平台收取)</Text>
|
||||
<Text className='text-gray-600 text-sm'>¥198.00</Text>
|
||||
</View>
|
||||
<View className='border-t border-gray-200 pt-2 flex justify-between'>
|
||||
<Text className='text-gray-700 text-sm font-medium'>合计</Text>
|
||||
<Text className='text-orange-500 text-base font-bold'>¥496.00</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 提示 */}
|
||||
<View className='bg-orange-50 rounded-xl p-3 mb-4'>
|
||||
<Text className='text-xs text-orange-600 leading-relaxed block'>
|
||||
⚠️ 会员费(¥298)由上级会员线下收取,请联系您的上级确认。注册费(¥198)通过平台支付。
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 会员权益 */}
|
||||
<View className='mb-4'>
|
||||
<Text className='text-gray-600 text-sm font-medium mb-3 block'>会员权益</Text>
|
||||
<View className='grid grid-cols-2 gap-2'>
|
||||
{[
|
||||
{ icon: '💰', text: '下级消费获得佣金' },
|
||||
{ icon: '🎁', text: '兑换权益包' },
|
||||
{ icon: '📈', text: '团队业绩统计' },
|
||||
{ icon: '💳', text: '佣金提现到微信' },
|
||||
].map((item, idx) => (
|
||||
<View key={idx} className='flex items-center gap-2 bg-gray-50 rounded-lg p-2'>
|
||||
<Text className='text-base'>{item.icon}</Text>
|
||||
<Text className='text-gray-600 text-xs'>{item.text}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 下一步按钮 */}
|
||||
<View className='mt-6'>
|
||||
<View
|
||||
className='rounded-full py-4 text-center'
|
||||
style={{ backgroundColor: '#667eea' }}
|
||||
onClick={handlePay}
|
||||
>
|
||||
<Text className='text-white text-lg font-medium'>立即支付 ¥198.00</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 联系客服 */}
|
||||
<View className='mt-4 text-center'>
|
||||
<Text
|
||||
className='text-gray-400 text-sm'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/customer-service/index' })}
|
||||
>
|
||||
遇到问题?联系客服
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 支付弹窗 */}
|
||||
{showPayModal && payInfo && (
|
||||
<PayModal
|
||||
amount={payInfo.registerFee || 198}
|
||||
subject='注册费'
|
||||
onSuccess={handlePaySuccess}
|
||||
onFail={handlePayFail}
|
||||
onClose={() => setShowPayModal(false)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegisterPayPage
|
||||
77
src_bak/pages/user/setting.tsx
Normal file
77
src_bak/pages/user/setting.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '设置',
|
||||
})
|
||||
|
||||
const SettingPage: React.FC = () => {
|
||||
const { isLoggedIn, logoutUser } = useUser()
|
||||
|
||||
const handleClearCache = () => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要清除缓存吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.clearStorageSync()
|
||||
Taro.showToast({ title: '缓存已清除', icon: 'success' })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
if (!isLoggedIn) return
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
logoutUser()
|
||||
Taro.reLaunch({ url: '/pages/index/index' })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{ label: '清除缓存', action: handleClearCache },
|
||||
{ label: '关于我们', action: () => Taro.showToast({ title: 'v1.0.0', icon: 'none' }) },
|
||||
{ label: '用户协议', action: () => Taro.navigateTo({ url: '/passport/agreement' }) },
|
||||
{ label: '隐私政策', action: () => Taro.navigateTo({ url: '/passport/agreement' }) },
|
||||
]
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 p-3'>
|
||||
<View className='bg-white rounded-xl overflow-hidden'>
|
||||
{menuItems.map((item, idx) => (
|
||||
<View
|
||||
key={item.label}
|
||||
className={`flex items-center justify-between px-4 py-3 ${
|
||||
idx < menuItems.length - 1 ? 'border-b border-gray-50' : ''
|
||||
}`}
|
||||
onClick={item.action}
|
||||
>
|
||||
<Text className='text-sm text-gray-700'>{item.label}</Text>
|
||||
<Text className='text-gray-300 text-sm'>{'>'}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{isLoggedIn && (
|
||||
<View className='mt-6 mx-4'>
|
||||
<View
|
||||
className='py-2 rounded-full text-center bg-white'
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<Text className='text-red-500 text-sm font-medium'>退出登录</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingPage
|
||||
217
src_bak/pages/user/shop-setting/index.tsx
Normal file
217
src_bak/pages/user/shop-setting/index.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Input, Switch } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
import { getShopSettingByCategory, batchSaveShopSetting } from '@/api/shop/shopSetting'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '商城设置',
|
||||
})
|
||||
|
||||
type Category = 'basic' | 'order' | 'points' | 'dealer' | 'notify'
|
||||
|
||||
const CATEGORIES: { key: Category; label: string }[] = [
|
||||
{ key: 'basic', label: '基础设置' },
|
||||
{ key: 'order', label: '订单设置' },
|
||||
{ key: 'points', label: '积分设置' },
|
||||
{ key: 'dealer', label: '分销设置' },
|
||||
{ key: 'notify', label: '通知设置' },
|
||||
]
|
||||
|
||||
const ShopSettingPage: React.FC = () => {
|
||||
const scrollHeight = useScrollHeight(44)
|
||||
const [activeCategory, setActiveCategory] = useState<Category>('basic')
|
||||
const [settings, setSettings] = useState<Record<string, string>>({})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings(activeCategory)
|
||||
}, [activeCategory])
|
||||
|
||||
const fetchSettings = async (category: Category) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const list = await getShopSettingByCategory(category)
|
||||
const map: Record<string, string> = {}
|
||||
list.forEach(item => {
|
||||
if (item.settingKey) {
|
||||
map[item.settingKey] = item.settingValue ?? ''
|
||||
}
|
||||
})
|
||||
setSettings(map)
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
await batchSaveShopSetting(activeCategory, settings as Record<string, unknown>)
|
||||
Taro.showToast({ title: '保存成功', icon: 'success' })
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (key: string, value: string) => {
|
||||
setSettings(prev => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const handleSwitchChange = (key: string, checked: boolean) => {
|
||||
setSettings(prev => ({ ...prev, [key]: checked ? '1' : '0' }))
|
||||
}
|
||||
|
||||
const renderSettingItem = (key: string, label: string, type: 'text' | 'switch' | 'number' = 'text', placeholder = '') => {
|
||||
const value = settings[key] ?? ''
|
||||
if (type === 'switch') {
|
||||
return (
|
||||
<View key={key} className='flex items-center justify-between px-4 py-3 border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-700'>{label}</Text>
|
||||
<Switch
|
||||
checked={value === '1' || value === 'true'}
|
||||
color='#0e932e'
|
||||
onChange={e => handleSwitchChange(key, e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View key={key} className='flex items-center justify-between px-4 py-3 border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-700 flex-shrink-0 mr-3'>{label}</Text>
|
||||
<Input
|
||||
className='text-sm text-gray-800 text-right flex-1'
|
||||
value={value}
|
||||
type={type === 'number' ? 'number' : 'text'}
|
||||
placeholder={placeholder || `请输入${label}`}
|
||||
placeholderStyle='color:#ccc;font-size:12px'
|
||||
onInput={e => handleChange(key, e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const renderCategorySettings = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<View className='flex items-center justify-center py-10'>
|
||||
<Text className='text-gray-400 text-sm'>加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
switch (activeCategory) {
|
||||
case 'basic':
|
||||
return (
|
||||
<View className='bg-white rounded-xl overflow-hidden'>
|
||||
{renderSettingItem('shopName', '商城名称', 'text', '请输入商城名称')}
|
||||
{renderSettingItem('shopLogo', '商城Logo', 'text', '请输入Logo地址')}
|
||||
{renderSettingItem('shopDesc', '商城描述', 'text', '请输入商城描述')}
|
||||
{renderSettingItem('shopPhone', '客服电话', 'text', '请输入客服电话')}
|
||||
{renderSettingItem('shopAddress', '商城地址', 'text', '请输入商城地址')}
|
||||
{renderSettingItem('shopEnabled', '商城开关', 'switch')}
|
||||
</View>
|
||||
)
|
||||
case 'order':
|
||||
return (
|
||||
<View className='bg-white rounded-xl overflow-hidden'>
|
||||
{renderSettingItem('orderAutoConfirmDays', '自动确认收货(天)', 'number', '如: 7')}
|
||||
{renderSettingItem('orderAutoCloseMins', '未付款自动关闭(分钟)', 'number', '如: 30')}
|
||||
{renderSettingItem('orderAutoCommentDays', '自动好评(天)', 'number', '如: 15')}
|
||||
{renderSettingItem('orderFreightFreeAmount', '免运费金额', 'number', '如: 99')}
|
||||
{renderSettingItem('orderRefundEnabled', '允许退款', 'switch')}
|
||||
</View>
|
||||
)
|
||||
case 'points':
|
||||
return (
|
||||
<View className='bg-white rounded-xl overflow-hidden'>
|
||||
{renderSettingItem('pointsEnabled', '积分功能', 'switch')}
|
||||
{renderSettingItem('pointsSigninEnabled', '签到积分', 'switch')}
|
||||
{renderSettingItem('pointsSigninAmount', '每日签到积分', 'number', '如: 10')}
|
||||
{renderSettingItem('pointsOrderRate', '消费积分比例(元/积分)', 'number', '如: 10')}
|
||||
{renderSettingItem('pointsExchangeRate', '积分兑换比例(积分/元)', 'number', '如: 100')}
|
||||
</View>
|
||||
)
|
||||
case 'dealer':
|
||||
return (
|
||||
<View className='bg-white rounded-xl overflow-hidden'>
|
||||
{renderSettingItem('dealerEnabled', '分销功能', 'switch')}
|
||||
{renderSettingItem('dealerApplyEnabled', '开放申请', 'switch')}
|
||||
{renderSettingItem('dealerLevel1Rate', '一级佣金比例(%)', 'number', '如: 10')}
|
||||
{renderSettingItem('dealerLevel2Rate', '二级佣金比例(%)', 'number', '如: 5')}
|
||||
{renderSettingItem('dealerWithdrawMin', '最低提现金额', 'number', '如: 10')}
|
||||
</View>
|
||||
)
|
||||
case 'notify':
|
||||
return (
|
||||
<View className='bg-white rounded-xl overflow-hidden'>
|
||||
{renderSettingItem('notifyOrderEnabled', '订单通知', 'switch')}
|
||||
{renderSettingItem('notifyPayEnabled', '支付通知', 'switch')}
|
||||
{renderSettingItem('notifyRefundEnabled', '退款通知', 'switch')}
|
||||
{renderSettingItem('notifyShipEnabled', '发货通知', 'switch')}
|
||||
{renderSettingItem('notifySignEnabled', '签收通知', 'switch')}
|
||||
</View>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
{/* 分类 Tab */}
|
||||
<View className='bg-white flex overflow-x-auto border-b border-gray-100'>
|
||||
{CATEGORIES.map(cat => (
|
||||
<View
|
||||
key={cat.key}
|
||||
className='flex-shrink-0 px-4 py-3 text-center'
|
||||
onClick={() => setActiveCategory(cat.key)}
|
||||
>
|
||||
<Text
|
||||
className='text-sm'
|
||||
style={{ color: activeCategory === cat.key ? '#0e932e' : '#666' }}
|
||||
>
|
||||
{cat.label}
|
||||
</Text>
|
||||
{activeCategory === cat.key && (
|
||||
<View
|
||||
className='mt-1 mx-auto rounded-full'
|
||||
style={{ height: '2px', backgroundColor: '#0e932e', width: '20px' }}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
<View className='p-3'>
|
||||
{renderCategorySettings()}
|
||||
</View>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
{!loading && (
|
||||
<View className='px-4 py-3'>
|
||||
<View
|
||||
className='py-3 rounded-xl text-center'
|
||||
style={{ backgroundColor: saving ? '#ccc' : '#0e932e' }}
|
||||
onClick={!saving ? handleSave : undefined}
|
||||
>
|
||||
<Text className='text-white text-sm font-medium'>
|
||||
{saving ? '保存中...' : '保存设置'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className='h-6' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShopSettingPage
|
||||
3
src_bak/pages/user/team/index.config.ts
Normal file
3
src_bak/pages/user/team/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '我的团队',
|
||||
}
|
||||
127
src_bak/pages/user/team/index.tsx
Normal file
127
src_bak/pages/user/team/index.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
import { listShopUserReferee } from '@/api/shop/shopUserReferee'
|
||||
import type { ShopUserReferee } from '@/api/shop/shopUserReferee/model'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '我的团队',
|
||||
})
|
||||
|
||||
const TeamPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const [teamMembers, setTeamMembers] = useState<ShopUserReferee[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const scrollHeight = useScrollHeight(44)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
return
|
||||
}
|
||||
fetchTeamMembers()
|
||||
}, [isLoggedIn])
|
||||
|
||||
const fetchTeamMembers = async () => {
|
||||
try {
|
||||
const data = await listShopUserReferee({})
|
||||
setTeamMembers(data || [])
|
||||
} catch (e) {
|
||||
console.error('获取团队信息失败:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
{/* 团队统计 */}
|
||||
<View className='mx-3 mt-3 p-4 bg-white rounded-lg'>
|
||||
<View className='flex justify-around'>
|
||||
<View className='text-center'>
|
||||
<Text className='text-2xl font-bold text-green-600 block'>
|
||||
{teamMembers.length}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-500'>团队人数</Text>
|
||||
</View>
|
||||
<View className='text-center'>
|
||||
<Text className='text-2xl font-bold text-blue-500 block'>
|
||||
{teamMembers.filter(m => m.isActive).length}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-500'>活跃成员</Text>
|
||||
</View>
|
||||
<View className='text-center'>
|
||||
<Text className='text-2xl font-bold text-orange-500 block'>
|
||||
{teamMembers.filter(m => m.isMember).length}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-500'>会员人数</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 团队成员列表 */}
|
||||
<View className='mx-3 mt-3'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-2 block'>团队成员</Text>
|
||||
{loading ? (
|
||||
<View className='flex items-center justify-center py-10'>
|
||||
<Text className='text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
) : teamMembers.length === 0 ? (
|
||||
<EmptyState text='暂无团队成员' />
|
||||
) : (
|
||||
<View className='bg-white rounded-lg overflow-hidden'>
|
||||
{teamMembers.map((member, idx) => (
|
||||
<View
|
||||
key={member.id}
|
||||
className={`flex items-center px-4 py-3 ${
|
||||
idx < teamMembers.length - 1 ? 'border-b border-gray-50' : ''
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
className='w-10 h-10 rounded-full bg-gray-100'
|
||||
src={member.avatar}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View className='flex-1 ml-3'>
|
||||
<View className='flex items-center gap-2'>
|
||||
<Text className='text-sm font-medium text-gray-800'>
|
||||
{member.nickname || '用户'}
|
||||
</Text>
|
||||
{member.isMember && (
|
||||
<View className='px-1 py-0 bg-orange-500 rounded'>
|
||||
<Text className='text-xs text-white'>会员</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
加入时间:{member.createTime}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='text-right'>
|
||||
<Text className='text-sm font-medium text-green-500'>
|
||||
¥{member.totalCommission || 0}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-400 block'>累计佣金</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className='h-6' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default TeamPage
|
||||
6
src_bak/pages/user/user.config.ts
Normal file
6
src_bak/pages/user/user.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
navigationBarTitleText: '我的',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f8f8f8'
|
||||
}
|
||||
9
src_bak/pages/user/user.scss
Normal file
9
src_bak/pages/user/user.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
padding: 32px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.nut-avatar {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
210
src_bak/pages/user/user.tsx
Normal file
210
src_bak/pages/user/user.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
import { useRequest } from '@/hooks/useRequest'
|
||||
import { getUserCardStats, getUserOrderStats, type UserCardStats, type UserOrderStats } from '@/api/shop/shopUserCard'
|
||||
import { getMyClerk } from '@/api/shop/shopStoreUser'
|
||||
import MemberBadge from '@/components/business/MemberBadge'
|
||||
|
||||
const UserPage: React.FC = () => {
|
||||
const scrollHeight = useScrollHeight()
|
||||
|
||||
const { user, isLoggedIn, loading: userLoading, refreshUser, syncFromStorage } = useUser()
|
||||
|
||||
// 查询门店关联信息
|
||||
const [storeInfo, setStoreInfo] = useState<any>(null)
|
||||
|
||||
// 获取用户卡片统计(余额/积分/优惠券/礼品卡)
|
||||
const { data: cardStats, run: runCardStats, loading: cardStatsLoading } = useRequest(getUserCardStats, {
|
||||
manual: true,
|
||||
refreshDeps: [isLoggedIn]
|
||||
})
|
||||
|
||||
// 获取用户订单统计
|
||||
const { data: orderStats, run: runOrderStats, loading: orderStatsLoading } = useRequest(getUserOrderStats, {
|
||||
manual: true,
|
||||
refreshDeps: [isLoggedIn]
|
||||
})
|
||||
|
||||
// 登录后加载数据
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
runCardStats()
|
||||
runOrderStats()
|
||||
// 查询门店关联
|
||||
getMyClerk().then(data => setStoreInfo(data)).catch(() => setStoreInfo(null))
|
||||
} else {
|
||||
setStoreInfo(null)
|
||||
}
|
||||
}, [isLoggedIn])
|
||||
|
||||
// 每次页面重新显示时(从登录页返回、从其他页面返回)检查登录状态并刷新数据
|
||||
useDidShow(() => {
|
||||
// 先从 storage 快速同步,立即更新 UI(无网络延迟)
|
||||
const hasUser = syncFromStorage()
|
||||
if (hasUser) {
|
||||
runCardStats()
|
||||
runOrderStats()
|
||||
// 刷新门店关联
|
||||
getMyClerk().then(data => setStoreInfo(data)).catch(() => setStoreInfo(null))
|
||||
}
|
||||
})
|
||||
|
||||
const menuItems = [
|
||||
// { icon: '🏆', label: '赛事活动', url: '/pages/event/my/index' },
|
||||
// { icon: '📅', label: '预约穿线', url: '/pages/booking/list/index' },
|
||||
// { icon: '🎫', label: '优惠券', url: '/pages/user/coupon-list' },
|
||||
{ icon: '💰', label: '我的钱包', url: '/pages/user/wallet' },
|
||||
{ icon: '📍', label: '收货地址', url: '/pages/user/address-list' },
|
||||
// { icon: '⭐', label: '积分明细', url: '/pages/user/points-record' },
|
||||
{ icon: '❤️', label: '我的收藏', url: '/pages/user/favorite-list/index' },
|
||||
// { icon: '🕐', label: '浏览历史', url: '/pages/user/history-list/index' },
|
||||
{ icon: '❓', label: '帮助中心', url: '/pages/user/help-center/index' },
|
||||
// { icon: '🔔', label: '消息通知', url: '/pages/index/notification' },
|
||||
// 门店管理:仅门店经理/店员显示
|
||||
// ...(storeInfo ? [{ icon: '🏪', label: '门店管理', url: '/pages/store/list/index' }] : []),
|
||||
{ icon: '⚙️', label: '设置', url: '/pages/user/setting' },
|
||||
]
|
||||
|
||||
const handleAvatarClick = () => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
} else {
|
||||
Taro.navigateTo({ url: '/pages/user/profile' })
|
||||
}
|
||||
}
|
||||
|
||||
const loading = cardStatsLoading || orderStatsLoading
|
||||
|
||||
// 下拉刷新状态
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
// 下拉刷新:同时刷新用户信息、卡片统计、订单统计
|
||||
const onRefresh = async () => {
|
||||
if (!isLoggedIn) { setRefreshing(false); return }
|
||||
setRefreshing(true)
|
||||
try {
|
||||
await Promise.all([refreshUser(), runCardStats(), runOrderStats()])
|
||||
} finally {
|
||||
setRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='h-full bg-gray-50'>
|
||||
<ScrollView scrollY refresherEnabled={!!isLoggedIn} refresherTriggered={refreshing} onRefresherRefresh={onRefresh} style={{ height: scrollHeight }}>
|
||||
{/* 用户信息卡片 */}
|
||||
<View className='mx-3 mt-3 p-4 bg-white rounded-xl'>
|
||||
<View className='flex items-center gap-3' onClick={handleAvatarClick}>
|
||||
{isLoggedIn && user?.avatar ? (
|
||||
<Image className='w-14 h-14 rounded-full' src={user.avatar} mode='aspectFill' />
|
||||
) : (
|
||||
<View className='w-14 h-14 rounded-full bg-gray-100 flex items-center justify-center'>
|
||||
<Text className='text-2xl text-gray-300'>👤</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className='flex-1'>
|
||||
<Text className='text-lg font-medium text-gray-800 block'>
|
||||
{isLoggedIn ? (user?.nickname || user?.phone || '用户') : '点击登录'}
|
||||
</Text>
|
||||
<View className='flex items-center gap-2 mt-1'>
|
||||
{isLoggedIn && <MemberBadge levelName={(user as any)?.memberLevelName} />}
|
||||
</View>
|
||||
</View>
|
||||
{isLoggedIn && <Text className='text-gray-300 text-sm'>{'>'}</Text>}
|
||||
</View>
|
||||
{/* 数据概览 */}
|
||||
{isLoggedIn && (
|
||||
<View className='grid grid-cols-3 gap-2 mt-4 pt-4 border-t border-gray-50'>
|
||||
<View className='text-center' onClick={() => Taro.navigateTo({ url: '/pages/user/wallet' })}>
|
||||
{loading ? (
|
||||
<Text className='text-lg font-bold text-gray-300 block'>...</Text>
|
||||
) : (
|
||||
<Text className='text-lg font-bold text-gray-800 block'>
|
||||
{cardStats?.balance || '0.00'}
|
||||
</Text>
|
||||
)}
|
||||
<Text className='text-xs text-gray-400'>余额</Text>
|
||||
</View>
|
||||
<View className='text-center' onClick={() => Taro.navigateTo({ url: '/pages/user/points-record' })}>
|
||||
{loading ? (
|
||||
<Text className='text-lg font-bold text-gray-300 block'>...</Text>
|
||||
) : (
|
||||
<Text className='text-lg font-bold text-gray-800 block'>
|
||||
{cardStats?.points || (user as any)?.points || 0}
|
||||
</Text>
|
||||
)}
|
||||
<Text className='text-xs text-gray-400'>积分</Text>
|
||||
</View>
|
||||
<View className='text-center' onClick={() => Taro.navigateTo({ url: '/pages/user/coupon-list' })}>
|
||||
{loading ? (
|
||||
<Text className='text-lg font-bold text-gray-300 block'>...</Text>
|
||||
) : (
|
||||
<Text className='text-lg font-bold text-gray-800 block'>
|
||||
{cardStats?.coupons || 0}
|
||||
</Text>
|
||||
)}
|
||||
<Text className='text-xs text-gray-400'>优惠券</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{/* 我的订单快捷入口 */}
|
||||
<View className='bg-white rounded-xl mx-3 mt-3 p-4'>
|
||||
<View className='flex justify-between items-center mb-3'>
|
||||
<Text className='text-base font-medium text-gray-800'>我的订单</Text>
|
||||
<Text className='text-xs text-gray-400' onClick={() => Taro.navigateTo({ url: '/pages/order/list' })}>
|
||||
全部订单 {'>'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='grid grid-cols-4 gap-2'>
|
||||
{[
|
||||
{ icon: '💳', label: '待付款', status: 0, count: orderStats?.pending },
|
||||
{ icon: '📦', label: '待发货', status: 1, count: orderStats?.paid },
|
||||
{ icon: '🚚', label: '待收货', status: 2, count: orderStats?.shipped },
|
||||
{ icon: '✅', label: '已完成', status: 3, count: orderStats?.completed },
|
||||
].map(item => (
|
||||
<View
|
||||
key={item.status}
|
||||
className='flex flex-col items-center py-2 relative'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/order/list?tab=${item.status}` })}
|
||||
>
|
||||
<Text className='text-xl mb-1'>{item.icon}</Text>
|
||||
<Text className='text-xs text-gray-600'>{item.label}</Text>
|
||||
{/* 数量角标 */}
|
||||
{item.count && item.count > 0 && (
|
||||
<View className='absolute top-0 right-2 bg-red-500 text-white text-xs rounded-full min-w-4 h-4 flex items-center justify-center px-1'>
|
||||
{item.count > 99 ? '99+' : item.count}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
{/* 功能菜单 */}
|
||||
<View className='bg-white rounded-xl mx-3 mt-3 overflow-hidden'>
|
||||
{menuItems.map((item, idx) => (
|
||||
<View
|
||||
key={item.label}
|
||||
className={`flex items-center justify-between px-4 py-3 ${
|
||||
idx < menuItems.length - 1 ? 'border-b border-gray-50' : ''
|
||||
}`}
|
||||
onClick={() => Taro.navigateTo({ url: item.url })}
|
||||
>
|
||||
<View className='flex items-center gap-3'>
|
||||
<Text className='text-base'>{item.icon}</Text>
|
||||
<Text className='text-sm text-gray-700'>{item.label}</Text>
|
||||
</View>
|
||||
<Text className='text-gray-300 text-sm'>{'>'}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<View className='h-6' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserPage
|
||||
88
src_bak/pages/user/wallet.tsx
Normal file
88
src_bak/pages/user/wallet.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useRequest } from '@/hooks/useRequest'
|
||||
import { getUserCardStats, type UserCardStats } from '@/api/shop/shopUserCard'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '我的钱包',
|
||||
})
|
||||
|
||||
const WalletPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
|
||||
// 获取用户余额信息(使用与"我的"页面相同的 API)
|
||||
const { data: balanceData, run: fetchBalance, loading: balanceLoading } = useRequest(getUserCardStats, {
|
||||
manual: true,
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
fetchBalance()
|
||||
}
|
||||
}, [isLoggedIn, fetchBalance])
|
||||
|
||||
// getUserCardStats 返回 { balance: string }
|
||||
const balance = (balanceData as UserCardStats)?.balance || '0.00'
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount: string) => {
|
||||
return parseFloat(amount).toFixed(2)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex flex-col'>
|
||||
<ScrollView scrollY className='flex-1' onScrollToLower={() => {}}>
|
||||
{/* 余额卡片 */}
|
||||
<View className='mx-3 mt-3 p-5 rounded-xl text-center' style={{ background: 'linear-gradient(135deg, #f59e0b, #f97316)' }}>
|
||||
<Text className='text-white text-sm opacity-80 block mb-1'>账户余额 (元)</Text>
|
||||
<Text className='text-white text-3xl font-bold block'>
|
||||
{balanceLoading ? '...' : formatAmount(balance)}
|
||||
</Text>
|
||||
<View className='flex gap-3 mt-4'>
|
||||
<View
|
||||
className='flex-1 py-2 rounded-full text-center'
|
||||
style={{ backgroundColor: 'rgba(255,255,255,0.2)' }}
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/recharge' })}
|
||||
>
|
||||
<Text className='text-white text-sm font-medium'>充值</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 快捷操作 */}
|
||||
<View className='mx-3 mt-3 bg-white rounded-xl p-4'>
|
||||
<View className='flex justify-between'>
|
||||
<View
|
||||
className='flex-1 flex flex-col items-center py-2'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/balance-log/index' })}
|
||||
>
|
||||
<Text className='text-lg mb-1'>📄</Text>
|
||||
<Text className='text-xs text-gray-600'>余额明细</Text>
|
||||
</View>
|
||||
<View
|
||||
className='flex-1 flex flex-col items-center py-2'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/recharge-record/index' })}
|
||||
>
|
||||
<Text className='text-lg mb-1'>💳</Text>
|
||||
<Text className='text-xs text-gray-600'>充值记录</Text>
|
||||
</View>
|
||||
<View
|
||||
className='flex-1 flex flex-col items-center py-2'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/redeem/index' })}
|
||||
>
|
||||
<Text className='text-lg mb-1'>🎫</Text>
|
||||
<Text className='text-xs text-gray-600'>兑换码</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='h-6' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletPage
|
||||
139
src_bak/pages/user/withdraw-list/index.scss
Normal file
139
src_bak/pages/user/withdraw-list/index.scss
Normal file
@@ -0,0 +1,139 @@
|
||||
.withdraw-list-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: 24rpx;
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #ff6b35;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 48rpx;
|
||||
height: 4rpx;
|
||||
background: #ff6b35;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.withdraw-scroll {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.withdraw-item {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.withdraw-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.withdraw-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: #ff9a56;
|
||||
background: rgba(255, 154, 86, 0.1);
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: #07c160;
|
||||
background: rgba(7, 193, 96, 0.1);
|
||||
}
|
||||
|
||||
.status-fail {
|
||||
color: #999;
|
||||
background: rgba(153, 153, 153, 0.1);
|
||||
}
|
||||
|
||||
.withdraw-info {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.withdraw-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.audit-remark {
|
||||
font-size: 22rpx;
|
||||
color: #ff6b35;
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
padding: 8rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
169
src_bak/pages/user/withdraw-list/index.tsx
Normal file
169
src_bak/pages/user/withdraw-list/index.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getWithdrawStats, getMyWithdrawList } from '@/api/shop/shopWithdrawRecord';
|
||||
import EmptyState from '@/components/common/EmptyState';
|
||||
import { Loading } from '@nutui/nutui-react-taro';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '提现记录',
|
||||
});
|
||||
|
||||
export default function WithdrawListPage() {
|
||||
const [stats, setStats] = useState<any>({});
|
||||
const [list, setList] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState(-1); // -1全部/0待审核/1通过/4完成
|
||||
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
loadList(true);
|
||||
}, []);
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const res = await getWithdrawStats();
|
||||
if (res.code === 200) {
|
||||
setStats(res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadList = async (isRefresh = false) => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const currentPage = isRefresh ? 1 : page;
|
||||
const params: any = {
|
||||
page: currentPage,
|
||||
limit: 20
|
||||
};
|
||||
|
||||
if (activeTab >= 0) {
|
||||
params.status = activeTab;
|
||||
}
|
||||
|
||||
const res = await getMyWithdrawList(params);
|
||||
|
||||
if (res.code === 200) {
|
||||
const newList = res.data.list || [];
|
||||
setList(isRefresh ? newList : [...list, ...newList]);
|
||||
setPage(currentPage + 1);
|
||||
setHasMore(newList.length >= 20);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载列表失败', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (tab: number) => {
|
||||
setActiveTab(tab);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
loadList(true);
|
||||
};
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
const map: Record<number, string> = {
|
||||
0: '待审核',
|
||||
1: '审核通过',
|
||||
2: '审核拒绝',
|
||||
3: '打款中',
|
||||
4: '已完成',
|
||||
5: '已取消'
|
||||
};
|
||||
return map[status] || '未知';
|
||||
};
|
||||
|
||||
const getStatusClass = (status: number) => {
|
||||
if (status === 4) return 'status-success';
|
||||
if (status === 2 || status === 5) return 'status-fail';
|
||||
return 'status-pending';
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="withdraw-list-page">
|
||||
{/* 统计区域 */}
|
||||
<View className="stats-card">
|
||||
<View className="stats-item">
|
||||
<Text className="stats-label">提现中(元)</Text>
|
||||
<Text className="stats-value">{stats.withdrawingTotal || '0.00'}</Text>
|
||||
</View>
|
||||
<View className="stats-item">
|
||||
<Text className="stats-label">今日提现(元)</Text>
|
||||
<Text className="stats-value">{stats.todayTotal || '0.00'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 标签页 */}
|
||||
<View className="tab-bar">
|
||||
<View
|
||||
className={`tab-item ${activeTab === -1 ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(-1)}
|
||||
>
|
||||
全部
|
||||
</View>
|
||||
<View
|
||||
className={`tab-item ${activeTab === 0 ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(0)}
|
||||
>
|
||||
待审核
|
||||
</View>
|
||||
<View
|
||||
className={`tab-item ${activeTab === 4 ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(4)}
|
||||
>
|
||||
已完成
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 列表 */}
|
||||
<ScrollView
|
||||
className="withdraw-scroll"
|
||||
scrollY
|
||||
onScrollToLower={() => hasMore && loadList()}
|
||||
>
|
||||
{list.length === 0 && !loading ? (
|
||||
<EmptyState description="暂无提现记录" />
|
||||
) : (
|
||||
list.map((item) => (
|
||||
<View key={item.id} className="withdraw-item">
|
||||
<View className="withdraw-header">
|
||||
<Text className="withdraw-amount">¥{item.withdrawAmount}</Text>
|
||||
<Text className={`status-tag ${getStatusClass(item.status)}`}>
|
||||
{getStatusText(item.status)}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="withdraw-info">
|
||||
<Text className="info-text">
|
||||
实际到账: ¥{item.actualAmount}
|
||||
</Text>
|
||||
{item.fee > 0 && (
|
||||
<Text className="info-text">
|
||||
手续费: ¥{item.fee}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className="withdraw-time">{item.createTime}</Text>
|
||||
{item.auditRemark && (
|
||||
<Text className="audit-remark">
|
||||
备注: {item.auditRemark}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
{loading && <Loading>加载中...</Loading>}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
250
src_bak/pages/user/withdraw/index.scss
Normal file
250
src_bak/pages/user/withdraw/index.scss
Normal file
@@ -0,0 +1,250 @@
|
||||
.withdraw-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* ========== 余额卡片 ========== */
|
||||
.balance-card {
|
||||
margin: 24rpx;
|
||||
padding: 40rpx 32rpx 32rpx;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
|
||||
border-radius: 20rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.balance-header {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.balance-amount-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.currency-symbol-lg {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.balance-number {
|
||||
font-size: 72rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.balance-tips {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.balance-tip-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* ========== 提现输入卡片 ========== */
|
||||
.withdraw-card {
|
||||
margin: 24rpx;
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.amount-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 2rpx solid #eee;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.currency-symbol {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
flex: 1;
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.all-withdraw {
|
||||
flex-shrink: 0;
|
||||
padding: 8rpx 20rpx;
|
||||
background: #fff3ee;
|
||||
color: #ff6b35;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid #ffd4bc;
|
||||
}
|
||||
|
||||
.fee-tips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-top: 16rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #e67e22;
|
||||
background: #fef9f5;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.fee-free {
|
||||
color: #27ae60;
|
||||
background: #f0faf4;
|
||||
}
|
||||
|
||||
.fee-icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* ========== 提现规则区域(核心审核项)========== */
|
||||
.rules-section {
|
||||
margin: 0 24rpx 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 标题栏 */
|
||||
.rules-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 28rpx 32rpx 20rpx;
|
||||
background: linear-gradient(135deg, #fef0eb 0%, #fff8f5 100%);
|
||||
border-bottom: 2rpx solid #fce8e1;
|
||||
}
|
||||
|
||||
.rules-header-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.rules-header-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
/* 规则网格 - 两列 */
|
||||
.rules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 24rpx 24rpx 8rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.rule-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 12rpx;
|
||||
background: #fafafa;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.rule-cell-wide {
|
||||
grid-column: span 2;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 18rpx 24rpx;
|
||||
}
|
||||
|
||||
.rule-cell-label {
|
||||
font-size: 23rpx;
|
||||
color: #999;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.rule-cell-value {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.rule-cell-wide .rule-cell-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 详细规则文字列表 */
|
||||
.rules-detail {
|
||||
padding: 20rpx 32rpx 28rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10rpx;
|
||||
padding: 10rpx 0;
|
||||
}
|
||||
|
||||
.detail-bullet {
|
||||
flex-shrink: 0;
|
||||
font-size: 26rpx;
|
||||
color: #ff6b35;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 25rpx;
|
||||
color: #666;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* ========== 底部按钮区 ========== */
|
||||
.bottom-area {
|
||||
margin-top: auto;
|
||||
padding: 20rpx 40rpx 40rpx;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 26rpx 0;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff9a56 100%);
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
border-radius: 48rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.35);
|
||||
}
|
||||
|
||||
.submit-btn.disabled {
|
||||
opacity: 0.45;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.submit-disclaimer {
|
||||
text-align: center;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.disclaimer-text {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
261
src_bak/pages/user/withdraw/index.tsx
Normal file
261
src_bak/pages/user/withdraw/index.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import { View, Text, Input } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getWithdrawConfig, getWithdrawStats, applyWithdraw } from '@/api/shop/shopWithdrawRecord';
|
||||
import PayModal from '@/components/business/PayModal';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '提现',
|
||||
});
|
||||
|
||||
export default function WithdrawPage() {
|
||||
const [config, setConfig] = useState<any>({});
|
||||
const [stats, setStats] = useState<any>({});
|
||||
const [amount, setAmount] = useState('');
|
||||
const [showPayModal, setShowPayModal] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
loadStats();
|
||||
}, []);
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const res: any = await getWithdrawConfig();
|
||||
if (res.code === 200 || res.code === 0) {
|
||||
setConfig(res.data || {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const res: any = await getWithdrawStats();
|
||||
if (res.code === 200 || res.code === 0) {
|
||||
setStats(res.data || {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 从配置或默认值读取
|
||||
const minAmount = config.withdraw_min_amount || '10';
|
||||
const maxAmount = config.withdraw_max_amount || '200';
|
||||
const dailyLimit = config.withdraw_daily_limit || '2000';
|
||||
const dailyCount = config.withdraw_daily_count || 3;
|
||||
const timeStart = config.withdraw_time_start || '';
|
||||
const timeEnd = config.withdraw_time_end || '';
|
||||
const feeRate = config.withdraw_fee_rate || '0';
|
||||
const arrivalTime = config.arrival_time || '';
|
||||
|
||||
const handleWithdraw = () => {
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
Taro.showToast({ title: '请输入提现金额', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const amountNum = parseFloat(amount);
|
||||
if (amountNum < parseFloat(minAmount)) {
|
||||
Taro.showToast({ title: `最低提现金额为${minAmount}元`, icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (amountNum > parseFloat(maxAmount)) {
|
||||
Taro.showToast({ title: `单次最大提现金额为${maxAmount}元`, icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
setShowPayModal(true);
|
||||
};
|
||||
|
||||
const handlePayConfirm = async (payType: number) => {
|
||||
setShowPayModal(false);
|
||||
if (payType !== 0) return;
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res: any = await applyWithdraw({ amount: parseFloat(amount) });
|
||||
if (res.code === 200 || res.code === 0) {
|
||||
Taro.showToast({ title: '提现申请已提交', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1500);
|
||||
} else {
|
||||
Taro.showToast({ title: res.message || '提交失败', icon: 'none' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提现申请失败', error);
|
||||
Taro.showToast({ title: '提交失败', icon: 'none' });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAllWithdraw = () => {
|
||||
const available = stats.withdrawingTotal || stats.balance || '0.00';
|
||||
setAmount(available);
|
||||
};
|
||||
|
||||
const availableBalance = stats.withdrawingTotal || stats.balance || '0.00';
|
||||
|
||||
return (
|
||||
<View className="withdraw-page">
|
||||
{/* 余额卡片 */}
|
||||
<View className="balance-card">
|
||||
<View className="balance-header">
|
||||
<Text className="balance-label">可提现余额(元)</Text>
|
||||
</View>
|
||||
<View className="balance-amount-row">
|
||||
<Text className="currency-symbol-lg">¥</Text>
|
||||
<Text className="balance-number">{availableBalance}</Text>
|
||||
</View>
|
||||
<View className="balance-tips">
|
||||
<Text className="balance-tip-text">佣金已结算,可申请提现</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 提现输入 */}
|
||||
<View className="withdraw-card">
|
||||
<View className="card-title">提现金额</View>
|
||||
<View className="amount-input-wrap">
|
||||
<Text className="currency-symbol">¥</Text>
|
||||
<Input
|
||||
className="amount-input"
|
||||
type="digit"
|
||||
placeholder={`最低 ${minAmount} 元`}
|
||||
value={amount}
|
||||
onInput={(e) => setAmount(e.detail.value)}
|
||||
/>
|
||||
<Text
|
||||
className="all-withdraw"
|
||||
onClick={handleAllWithdraw}
|
||||
>
|
||||
全部提现
|
||||
</Text>
|
||||
</View>
|
||||
{feeRate && parseFloat(feeRate) > 0 ? (
|
||||
<View className="fee-tips">
|
||||
<Text className="fee-icon">⚠️</Text>
|
||||
<Text>本次提现将收取 {feeRate}% 手续费</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className="fee-tips fee-free">
|
||||
<Text className="fee-icon">✅</Text>
|
||||
<Text>当前免收提现手续费</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ====== 提现规则展示(核心审核项)====== */}
|
||||
<View className="rules-section">
|
||||
{/* 标题栏 */}
|
||||
<View className="rules-header">
|
||||
<Text className="rules-header-icon">📋</Text>
|
||||
<Text className="rules-header-title">提现规则说明</Text>
|
||||
</View>
|
||||
|
||||
{/* 规则网格 - 两列布局更醒目 */}
|
||||
<View className="rules-grid">
|
||||
<View className="rule-cell">
|
||||
<Text className="rule-cell-label">最低提现</Text>
|
||||
<Text className="rule-cell-value">{minAmount} 元起</Text>
|
||||
</View>
|
||||
<View className="rule-cell">
|
||||
<Text className="rule-cell-label">单次限额</Text>
|
||||
<Text className="rule-cell-value">{maxAmount} 元/次</Text>
|
||||
</View>
|
||||
<View className="rule-cell">
|
||||
<Text className="rule-cell-label">每日限额</Text>
|
||||
<Text className="rule-cell-value">{dailyLimit} 元/天</Text>
|
||||
</View>
|
||||
<View className="rule-cell">
|
||||
<Text className="rule-cell-label">每日次数</Text>
|
||||
<Text className="rule-cell-value">{dailyCount} 次/天</Text>
|
||||
</View>
|
||||
<View className="rule-cell">
|
||||
<Text className="rule-cell-label">提现时间</Text>
|
||||
<Text className="rule-cell-value">{timeStart && timeEnd ? `${timeStart}-${timeEnd}` : '全天可提'}</Text>
|
||||
</View>
|
||||
<View className="rule-cell">
|
||||
<Text className="rule-cell-label">到账时间</Text>
|
||||
<Text className="rule-cell-value">{arrivalTime || '1-3个工作日'}</Text>
|
||||
</View>
|
||||
<View className="rule-cell rule-cell-wide">
|
||||
<Text className="rule-cell-label">手续费</Text>
|
||||
<Text className="rule-cell-value">
|
||||
{feeRate && parseFloat(feeRate) > 0 ? `${feeRate}%` : '免手续费'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 详细规则文字 */}
|
||||
<View className="rules-detail">
|
||||
<View className="detail-item">
|
||||
<Text className="detail-bullet">●</Text>
|
||||
<Text className="detail-text">提现金额范围为 ¥{minAmount} ~ ¥{maxAmount},超出范围无法提交</Text>
|
||||
</View>
|
||||
<View className="detail-item">
|
||||
<Text className="detail-bullet">●</Text>
|
||||
<Text className="detail-text">每日最多可提现 {dailyCount} 次,累计不超过 ¥{dailyLimit}</Text>
|
||||
</View>
|
||||
{timeStart && timeEnd ? (
|
||||
<View className="detail-item">
|
||||
<Text className="detail-bullet">●</Text>
|
||||
<Text className="detail-text">仅在每日 {timeStart} 至 {timeEnd} 期间可发起提现申请</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className="detail-item">
|
||||
<Text className="detail-bullet">●</Text>
|
||||
<Text className="detail-text">支持全天 24 小时发起提现申请</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className="detail-item">
|
||||
<Text className="detail-bullet">●</Text>
|
||||
<Text className="detail-text">提现申请提交后将在 {arrivalTime || '1-3个工作日'} 内审核到账,遇国家法定节假日顺延</Text>
|
||||
</View>
|
||||
{feeRate && parseFloat(feeRate) > 0 ? (
|
||||
<View className="detail-item">
|
||||
<Text className="detail-bullet">●</Text>
|
||||
<Text className="detail-text">每笔提现将收取 {feeRate}% 的服务手续费,从提现金额中直接扣除</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className="detail-item">
|
||||
<Text className="detail-bullet">●</Text>
|
||||
<Text className="detail-text">当前活动期间免收提现手续费,全额到账</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className="detail-item">
|
||||
<Text className="detail-bullet">●</Text>
|
||||
<Text className="detail-text">提现金额将原路退回至您的支付账户,请确保账户状态正常</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="bottom-area">
|
||||
<View
|
||||
className={`submit-btn ${amount && !submitting ? '' : 'disabled'}`}
|
||||
onClick={handleWithdraw}
|
||||
>
|
||||
{submitting ? '提交中...' : '确认提现'}
|
||||
</View>
|
||||
<View className="submit-disclaimer">
|
||||
<Text className="disclaimer-text">提交即表示您已阅读并同意以上提现规则</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<PayModal
|
||||
visible={showPayModal}
|
||||
amount={parseFloat(amount) || 0}
|
||||
onConfirm={handlePayConfirm}
|
||||
onClose={() => setShowPayModal(false)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user