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

294
src_bak/pages/redeem.tsx Normal file
View 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