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

View File

@@ -0,0 +1,177 @@
import React, { useState } from 'react'
import { View, Text } from '@tarojs/components'
import { Card, Button, Tag } from '@nutui/nutui-react-taro'
import { Scan, Success, Failure, ArrowLeft } from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import { confirmWechatQRLogin, parseQRContent } from '@/api/passport/qr-login'
enum ScanType {
LOGIN = 'login',
UNKNOWN = 'unknown',
}
interface ScanRecord {
id: number
time: string
success: boolean
type: ScanType
message: string
raw?: string
}
const UnifiedQRPage: React.FC = () => {
const [loading, setLoading] = useState(false)
const [lastRecord, setLastRecord] = useState<ScanRecord | null>(null)
const [scanHistory, setScanHistory] = useState<ScanRecord[]>([])
const pushHistory = (record: ScanRecord) => {
setLastRecord(record)
setScanHistory((prev) => [record, ...prev.slice(0, 4)])
}
const handleGoBack = () => {
Taro.navigateBack()
}
const buildRecord = (success: boolean, type: ScanType, message: string, raw?: string): ScanRecord => ({
id: Date.now(),
time: new Date().toLocaleString(),
success,
type,
message,
raw,
})
const handleLoginScan = async (raw: string) => {
const token = parseQRContent(raw)
const userId = Number(Taro.getStorageSync('UserId') || 0)
if (!token) {
throw new Error('未识别到可登录的二维码内容')
}
if (!userId) {
throw new Error('请先登录小程序账号后再扫码')
}
const result = await confirmWechatQRLogin(token, userId)
if (result?.status !== 'confirmed') {
throw new Error(result?.message || '二维码登录确认失败')
}
return '登录确认成功'
}
const handleStartScan = async () => {
if (loading) return
try {
setLoading(true)
const res = await Taro.scanCode({ scanType: ['qrCode'] })
const raw = (res.result || '').trim()
if (!raw) {
throw new Error('扫码结果为空')
}
const token = parseQRContent(raw)
if (token) {
const message = await handleLoginScan(raw)
pushHistory(buildRecord(true, ScanType.LOGIN, message, raw))
Taro.showToast({ title: message, icon: 'success' })
return
}
const unsupportedMessage = '当前版本仅支持登录二维码,核销能力待补充'
pushHistory(buildRecord(false, ScanType.UNKNOWN, unsupportedMessage, raw))
Taro.showToast({ title: unsupportedMessage, icon: 'none' })
} catch (error: any) {
const message =
error?.errMsg?.includes('cancel')
? '已取消扫码'
: error?.message || '扫码失败'
pushHistory(buildRecord(false, ScanType.UNKNOWN, message))
if (message !== '已取消扫码') {
Taro.showToast({ title: message, icon: 'none' })
}
} finally {
setLoading(false)
}
}
const getTypeTag = (type: ScanType) => {
if (type === ScanType.LOGIN) {
return <Tag type='success'></Tag>
}
return <Tag type='primary'></Tag>
}
return (
<View className='unified-qr-page min-h-screen bg-gray-50'>
<View className='bg-white px-4 py-3 border-b border-gray-100 flex items-center'>
<ArrowLeft className='text-gray-600 mr-3' size='20' onClick={handleGoBack} />
<View className='flex-1'>
<Text className='text-lg font-bold'></Text>
<Text className='text-sm text-gray-600 block'></Text>
</View>
</View>
<Card className='m-4'>
<View className='text-center py-6'>
<Scan className='text-blue-500 mx-auto mb-4' size='48' />
<Text className='text-lg font-medium text-gray-800 mb-2 block'></Text>
<Text className='text-gray-600 mb-6 block'></Text>
<Button
type='primary'
size='large'
loading={loading}
disabled={loading}
onClick={handleStartScan}
className='w-full'
>
{loading ? '扫码中...' : '开始扫码'}
</Button>
</View>
</Card>
{lastRecord && (
<Card className='mx-4 mb-4'>
<View className='flex items-start'>
<View className='mr-3 mt-1'>
{lastRecord.success ? (
<Success className='text-green-500' size='18' />
) : (
<Failure className='text-red-500' size='18' />
)}
</View>
<View className='flex-1'>
<View className='flex items-center justify-between mb-2'>
<Text className='font-medium'></Text>
{getTypeTag(lastRecord.type)}
</View>
<Text className='text-sm text-gray-800 block mb-1'>{lastRecord.message}</Text>
<Text className='text-xs text-gray-500 block'>{lastRecord.time}</Text>
</View>
</View>
</Card>
)}
{scanHistory.length > 0 && (
<Card className='mx-4 mb-4'>
<Text className='font-medium block mb-3'></Text>
{scanHistory.map((item) => (
<View key={item.id} className='py-2 border-b border-gray-100' style={item === scanHistory[scanHistory.length - 1] ? { borderBottom: 'none' } : undefined}>
<View className='flex items-center justify-between mb-1'>
<Text className='text-sm text-gray-800'>{item.message}</Text>
{getTypeTag(item.type)}
</View>
<Text className='text-xs text-gray-500 block'>{item.time}</Text>
</View>
))}
</Card>
)}
</View>
)
}
export default UnifiedQRPage