feat(user): 新增收货地址管理及售后申请页面

- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
2026-07-01 12:11:56 +08:00
parent bf6ed504cc
commit 1fa58040f3
636 changed files with 58878 additions and 716 deletions

195
src_bak/pages/users.tsx Normal file
View File

@@ -0,0 +1,195 @@
import React, { useEffect, useState, useCallback } from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import NavBar from '@/components/NavBar'
import { pageShopUserReferee } from '@/api/shop/shopUserReferee'
import { pageShopOrder } from '@/api/shop/shopOrder'
definePageConfig({
navigationBarTitleText: '用户分析',
})
type TimeRange = 'week' | 'month'
interface UserStat {
date: string
newUsers: number
}
const UserAnalysisPage: React.FC = () => {
const [timeRange, setTimeRange] = useState<TimeRange>('week')
const [loading, setLoading] = useState(true)
const [totalNewUsers, setTotalNewUsers] = useState(0)
const [totalBuyUsers, setTotalBuyUsers] = useState(0)
const [dayStats, setDayStats] = useState<UserStat[]>([])
const [conversionRate, setConversionRate] = useState(0)
const formatDateKey = (date: Date) => {
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
return `${m}-${d}`
}
const formatDateFull = (date: Date) => {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
return `${y}-${m}-${d}`
}
const loadData = useCallback(async (range: TimeRange) => {
setLoading(true)
try {
const now = new Date()
const days = range === 'week' ? 7 : 30
const startDate = new Date(now.getTime() - (days - 1) * 24 * 60 * 60 * 1000)
const startStr = `${formatDateFull(startDate)} 00:00:00`
const endStr = `${formatDateFull(now)} 23:59:59`
const [refereeRes, orderRes] = await Promise.all([
pageShopUserReferee({ page: 1, limit: 500, startTime: startStr, endTime: endStr } as any).catch(() => null),
pageShopOrder({ page: 1, limit: 500, startTime: startStr, endTime: endStr } as any).catch(() => null),
])
// 按天分组用户
const dayMap: Record<string, number> = {}
for (let i = 0; i < days; i++) {
const d = new Date(startDate.getTime() + i * 24 * 60 * 60 * 1000)
dayMap[formatDateKey(d)] = 0
}
const refereeList = refereeRes?.list || []
refereeList.forEach((r: any) => {
if (!r.createTime) return
const key = r.createTime.substring(5, 10)
if (dayMap.hasOwnProperty(key)) {
dayMap[key]++
}
})
const stats: UserStat[] = Object.entries(dayMap).map(([date, newUsers]) => ({ date, newUsers }))
setDayStats(stats)
setTotalNewUsers(refereeList.length)
// 计算购买用户数(通过订单去重)
const orderList = orderRes?.list || []
const buyerSet = new Set(orderList.map((o: any) => o.userId).filter(Boolean))
setTotalBuyUsers(buyerSet.size)
setConversionRate(refereeList.length > 0 ? (buyerSet.size / refereeList.length) * 100 : 0)
} catch (e) {
console.error('加载用户数据失败', e)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
loadData(timeRange)
}, [timeRange, loadData])
const maxNewUsers = Math.max(...dayStats.map(d => d.newUsers), 1)
return (
<View className="min-h-screen bg-gray-100">
<NavBar title="用户分析" />
{/* 时间范围选择 */}
<View className="bg-white flex">
{([{ key: 'week', label: '近7天' }, { key: 'month', label: '近30天' }] as const).map(option => (
<View
key={option.key}
className="flex-1 py-3 text-center relative"
style={{ color: timeRange === option.key ? '#ef4444' : '#6b7280' }}
onClick={() => setTimeRange(option.key)}
>
<Text className={timeRange === option.key ? 'font-medium' : ''}>{option.label}</Text>
{timeRange === option.key && (
<View className="absolute bottom-0 left-0 right-0 flex justify-center">
<View className="h-1 rounded-t" style={{ width: '32px', backgroundColor: '#ef4444' }} />
</View>
)}
</View>
))}
</View>
<ScrollView scrollY>
{loading ? (
<View className="p-8 text-center">
<Text className="text-gray-400 text-sm">...</Text>
</View>
) : (
<View className="p-4">
{/* 核心指标 */}
<View className="bg-white rounded-lg p-4 mb-4">
<View className="grid grid-cols-2 gap-3">
<View className="text-center p-3 rounded-lg" style={{ backgroundColor: '#eff6ff' }}>
<Text className="text-2xl font-bold block" style={{ color: '#3b82f6' }}>{totalNewUsers}</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center p-3 rounded-lg" style={{ backgroundColor: '#f0fdf4' }}>
<Text className="text-2xl font-bold block" style={{ color: '#10b981' }}>{totalBuyUsers}</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center p-3 rounded-lg" style={{ backgroundColor: '#fff7ed' }}>
<Text className="text-2xl font-bold block" style={{ color: '#f59e0b' }}>
{conversionRate.toFixed(1)}%
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center p-3 rounded-lg" style={{ backgroundColor: '#fdf4ff' }}>
<Text className="text-2xl font-bold block" style={{ color: '#a855f7' }}>
{totalNewUsers > 0 ? (totalNewUsers / (timeRange === 'week' ? 7 : 30)).toFixed(1) : '0.0'}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
</View>
</View>
{/* 新增用户趋势 */}
<View className="bg-white rounded-lg p-4 mb-4">
<Text className="text-base font-medium mb-4 block"></Text>
{dayStats.every(d => d.newUsers === 0) ? (
<View className="py-8 text-center">
<Text className="text-gray-400 text-sm"></Text>
</View>
) : (
dayStats.map((item, index) => (
<View key={index} className={`flex items-center mb-3 ${index === dayStats.length - 1 ? 'mb-0' : ''}`}>
<Text className="text-xs text-gray-500" style={{ width: '48px' }}>{item.date}</Text>
<View className="flex-1 mx-3 rounded-full" style={{ backgroundColor: '#e5e7eb', height: '24px' }}>
<View
className="rounded-full flex items-center justify-end"
style={{
width: `${Math.max((item.newUsers / maxNewUsers) * 100, item.newUsers > 0 ? 8 : 0)}%`,
backgroundColor: '#3b82f6',
height: '24px',
paddingRight: '8px',
minWidth: item.newUsers > 0 ? '32px' : '0',
}}
>
{item.newUsers > 0 && (
<Text className="text-xs text-white">{item.newUsers}</Text>
)}
</View>
</View>
</View>
))
)}
</View>
{/* 数据说明 */}
<View className="bg-white rounded-lg p-4">
<Text className="text-base font-medium mb-3 block"></Text>
<View className="flex flex-col" style={{ gap: '8px' }}>
<Text className="text-sm text-gray-500 block"> /</Text>
<Text className="text-sm text-gray-500 block"> </Text>
<Text className="text-sm text-gray-500 block"> / × 100%</Text>
</View>
</View>
</View>
)}
</ScrollView>
</View>
)
}
export default UserAnalysisPage