Files
template-10579/src/user/order/refund/index.tsx
赵忠林 eee4644d06 ```
feat(registration): 优化经销商注册流程并增加地址定位功能

- 修改导航栏标题从“邀请注册”为“注册成为会员”
- 修复重复提交问题并移除不必要的submitting状态
- 增加昵称和头像的必填验证提示
- 添加用户角色缺失时的默认角色写入机制
- 集成地图选点功能,支持经纬度获取和地址解析
- 实现微信地址导入功能,自动填充基本信息
- 增加定位权限检查和错误处理机制
- 添加.gitignore规则忽略备份文件夹src__bak
- 移除已废弃的银行卡和客户管理页面代码
- 优化表单验证规则和错误提示信息
- 实现经销商注册成功后自动跳转到“我的”页面
- 添加用户信息缓存刷新机制确保角色信息同步
```
2026-03-01 12:35:41 +08:00

473 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
import Taro, { useRouter } from '@tarojs/taro'
import { View, Text, Image } from '@tarojs/components'
import {
Cell,
CellGroup,
Radio,
RadioGroup,
TextArea,
Button,
Loading,
InputNumber
} from '@nutui/nutui-react-taro'
import { applyAfterSale } from '@/api/afterSale'
import { getShopOrder, updateShopOrder } from '@/api/shop/shopOrder'
import { listShopOrderGoods } from '@/api/shop/shopOrderGoods'
import './index.scss'
// 订单商品信息
interface OrderGoods {
goodsId: string
goodsName: string
goodsImage: string
goodsPrice: number
goodsNum: number
skuInfo?: string
canRefundNum: number // 可退款数量
}
// 退款原因选项
const REFUND_REASONS = [
'不想要了',
'商品质量问题',
'商品与描述不符',
'收到商品破损',
'发错商品',
'商品缺件',
'其他原因'
]
// 退款申请信息
interface RefundApplication {
refundType: 'full' | 'partial' // 退款类型:全额退款 | 部分退款
refundReason: string // 退款原因
refundDescription: string // 退款说明
refundAmount: number // 退款金额
refundGoods: Array<{
goodsId: string
refundNum: number
}> // 退款商品
evidenceImages: string[] // 凭证图片
contactPhone?: string // 联系电话
isUrgent: boolean // 是否加急处理
}
const RefundPage: React.FC = () => {
const router = useRouter()
const { orderId, orderNo } = router.params
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [markedClientRefund, setMarkedClientRefund] = useState(false)
const [orderGoods, setOrderGoods] = useState<OrderGoods[]>([])
const [orderAmount, setOrderAmount] = useState(0)
const [refundApp, setRefundApp] = useState<RefundApplication>({
refundType: 'full',
refundReason: '',
refundDescription: '',
refundAmount: 0,
refundGoods: [],
evidenceImages: [],
contactPhone: '',
isUrgent: false
})
const toMoneyNumber = (value: unknown, defaultValue: number = 0): number => {
if (typeof value === 'number') return Number.isFinite(value) ? value : defaultValue
if (typeof value === 'string') {
// Be tolerant of API strings like "¥12.34" or "1,234.56".
const cleaned = value.trim().replace(/,/g, '').replace(/[^\d.-]/g, '')
const n = Number.parseFloat(cleaned)
return Number.isFinite(n) ? n : defaultValue
}
return defaultValue
}
const formatMoney = (value: unknown): string => {
const n = toMoneyNumber(value, 0)
return n.toFixed(2)
}
const markOrderClientRefund = async () => {
if (markedClientRefund) return
if (!orderId) return
const orderIdNum = Number.parseInt(String(orderId), 10)
if (!Number.isFinite(orderIdNum)) return
try {
await updateShopOrder({
orderId: orderIdNum,
orderStatus: 7 // 客户端申请退款
})
setMarkedClientRefund(true)
} catch (e) {
console.error('更新订单状态为客户端申请退款失败:', e)
// 不阻塞用户填写表单;提交时仍会再次尝试更新一次
}
}
useEffect(() => {
if (orderId) {
loadOrderInfo()
}
}, [orderId])
// 加载订单信息
const loadOrderInfo = async () => {
try {
setLoading(true)
if (!orderId) {
throw new Error('缺少订单ID')
}
const orderIdNum = Number.parseInt(String(orderId), 10)
if (!Number.isFinite(orderIdNum)) {
throw new Error('订单ID不合法')
}
// 以订单实付金额为准(避免商品单价合计与优惠/运费等不一致)
const order = await getShopOrder(orderIdNum)
const payAmount = toMoneyNumber(order?.payPrice ?? order?.totalPrice, 0)
// 商品信息加载失败时,不阻塞退款申请(全额退款不依赖商品明细)
let mappedGoods: OrderGoods[] = []
try {
const goods = (await listShopOrderGoods({ orderId: orderIdNum })) || []
mappedGoods = goods.map((g, idx) => {
const goodsNum = Number(g.totalNum ?? 0) || 0
return {
goodsId: String(g.goodsId ?? idx),
goodsName: g.goodsName || '订单商品',
goodsImage: g.image || '/default-goods.png',
goodsPrice: toMoneyNumber(g.price, 0),
goodsNum,
canRefundNum: goodsNum,
skuInfo: g.spec
}
})
} catch (e) {
console.warn('加载订单商品失败(不阻塞退款申请):', e)
}
setOrderGoods(mappedGoods)
setOrderAmount(payAmount)
// 初始化退款申请信息:默认全额退款
setRefundApp(prev => ({
...prev,
refundType: 'full',
refundAmount: payAmount,
refundGoods: mappedGoods.map(g => ({
goodsId: g.goodsId,
refundNum: g.goodsNum
})),
evidenceImages: []
}))
} catch (error) {
console.error('加载订单信息失败:', error)
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
setLoading(false)
}
}
// 更新退款申请信息
const updateRefundApp = (field: keyof RefundApplication, value: any) => {
setRefundApp(prev => ({
...prev,
[field]: value
}))
}
// 切换退款类型
// const handleRefundTypeChange = (type: 'full' | 'partial') => {
// updateRefundApp('refundType', type)
//
// if (type === 'full') {
// // 全额退款
// updateRefundApp('refundAmount', orderAmount)
// updateRefundApp('refundGoods', orderGoods.map(goods => ({
// goodsId: goods.goodsId,
// refundNum: goods.goodsNum
// })))
// } else {
// // 部分退款
// updateRefundApp('refundAmount', 0)
// updateRefundApp('refundGoods', orderGoods.map(goods => ({
// goodsId: goods.goodsId,
// refundNum: 0
// })))
// }
// }
// 更新商品退款数量
const updateGoodsRefundNum = (goodsId: string, refundNum: number) => {
const newRefundGoods = refundApp.refundGoods.map(item =>
item.goodsId === goodsId ? { ...item, refundNum } : item
)
updateRefundApp('refundGoods', newRefundGoods)
// 重新计算退款金额
const newRefundAmount = newRefundGoods.reduce((sum, item) => {
const goods = orderGoods.find(g => g.goodsId === item.goodsId)
return sum + (goods ? goods.goodsPrice * item.refundNum : 0)
}, 0)
updateRefundApp('refundAmount', newRefundAmount)
}
// 提交退款申请
const submitRefund = async () => {
try {
// 验证必填信息
if (!refundApp.refundReason) {
Taro.showToast({
title: '请选择退款原因',
icon: 'none'
})
return
}
if (refundApp.refundAmount <= 0) {
Taro.showToast({
title: '退款金额必须大于0',
icon: 'none'
})
return
}
if (refundApp.refundType === 'partial') {
const hasRefundGoods = refundApp.refundGoods.some(item => item.refundNum > 0)
if (!hasRefundGoods) {
Taro.showToast({
title: '请选择要退款的商品',
icon: 'none'
})
return
}
}
setSubmitting(true)
// 构造请求参数
const params = {
orderId: orderId || '',
type: 'refund' as const,
reason: refundApp.refundReason,
description: refundApp.refundDescription,
amount: refundApp.refundAmount,
contactPhone: refundApp.contactPhone,
evidenceImages: refundApp.evidenceImages,
...(refundApp.refundGoods.some(item => item.refundNum > 0)
? {
goodsItems: refundApp.refundGoods
.filter(item => item.refundNum > 0)
.map(item => ({
goodsId: item.goodsId,
quantity: item.refundNum
}))
}
: {})
}
// 调用API提交退款申请
const result = await applyAfterSale(params)
if (result.success) {
// 更新订单状态为"客户端申请退款"
if (orderId) {
try {
await updateShopOrder({
orderId: parseInt(orderId),
orderStatus: 7 // 客户端申请退款
})
setMarkedClientRefund(true)
} catch (updateError) {
console.error('更新订单状态失败:', updateError)
// 即使更新订单状态失败,也继续执行后续操作
}
}
Taro.showToast({
title: '退款申请提交成功',
icon: 'success'
})
// 延迟返回上一页
setTimeout(() => {
Taro.navigateBack()
}, 1500)
} else {
throw new Error(result.message || '提交失败')
}
} catch (error) {
console.error('提交退款申请失败:', error)
Taro.showToast({
title: error instanceof Error ? error.message : '提交失败,请重试',
icon: 'none'
})
} finally {
setSubmitting(false)
}
}
if (loading) {
return (
<View className="refund-page">
<View className="loading-container">
<Loading type="spinner" />
<Text className="loading-text">...</Text>
</View>
</View>
)
}
return (
<View className="refund-page">
{/* 订单信息 */}
<View className="order-info">
<Text className="order-no">{orderNo}</Text>
<Text className="order-amount">¥{formatMoney(orderAmount)}</Text>
</View>
{/* 退款类型选择 */}
{/*<CellGroup title="退款类型">*/}
{/* <RadioGroup */}
{/* value={refundApp.refundType}*/}
{/* onChange={(value) => handleRefundTypeChange(value as 'full' | 'partial')}*/}
{/* >*/}
{/* <Cell>*/}
{/* <Radio value="full">全额退款</Radio>*/}
{/* </Cell>*/}
{/* <Cell>*/}
{/* <Radio value="partial">部分退款</Radio>*/}
{/* </Cell>*/}
{/* </RadioGroup>*/}
{/*</CellGroup>*/}
{/* 商品列表 */}
{refundApp.refundType === 'partial' && (
<View className="goods-section">
<View className="section-title">退</View>
{orderGoods.map(goods => {
const refundGoods = refundApp.refundGoods.find(item => item.goodsId === goods.goodsId)
const refundNum = refundGoods?.refundNum || 0
return (
<View key={goods.goodsId} className="goods-item">
<View className="goods-info">
<Image
className="goods-image"
src={goods.goodsImage}
mode="aspectFill"
/>
<View className="goods-detail">
<Text className="goods-name">{goods.goodsName}</Text>
{goods.skuInfo && (
<Text className="goods-sku">{goods.skuInfo}</Text>
)}
<Text className="goods-price">¥{goods.goodsPrice}</Text>
</View>
</View>
<View className="refund-control">
<Text className="control-label">退</Text>
<InputNumber
value={refundNum}
min={0}
max={goods.canRefundNum}
onChange={(value) => updateGoodsRefundNum(goods.goodsId, Number(value) || 0)}
/>
<Text className="max-num">{goods.canRefundNum}</Text>
</View>
</View>
)
})}
</View>
)}
{/* 退款金额 */}
<CellGroup title="退款金额">
<Cell>
<Text className="refund-amount">¥{formatMoney(refundApp.refundAmount)}</Text>
</Cell>
</CellGroup>
{/* 退款原因 */}
<CellGroup title="退款原因">
<RadioGroup
value={refundApp.refundReason}
onChange={(value) => {
updateRefundApp('refundReason', value)
void markOrderClientRefund()
}}
>
{REFUND_REASONS.map(reason => (
<Cell key={reason} className="reason-cell">
<Radio value={reason}>{reason}</Radio>
</Cell>
))}
</RadioGroup>
</CellGroup>
{/* 退款说明 */}
<View className="description-section">
<View className="section-title">退</View>
<TextArea
placeholder="请详细说明退款原因..."
value={refundApp.refundDescription}
onChange={(value) => updateRefundApp('refundDescription', value)}
maxLength={500}
showCount
rows={4}
autoHeight
/>
</View>
{/* 凭证图片:当前上传链路不可用,先隐藏(保留数据结构,后续可恢复) */}
{/*<View className="evidence-section">*/}
{/* <View className="section-title">上传凭证(可选)</View>*/}
{/* <Uploader*/}
{/* value={refundApp.evidenceImages.map(url => ({ url }))}*/}
{/* onChange={handleImageUpload}*/}
{/* multiple*/}
{/* maxCount={6}*/}
{/* previewType="picture"*/}
{/* deletable*/}
{/* />*/}
{/*</View>*/}
{/* 其他选项 */}
{/*<CellGroup>*/}
{/* <Cell className="option-cell">*/}
{/* <Checkbox*/}
{/* checked={refundApp.isUrgent}*/}
{/* onChange={(checked) => updateRefundApp('isUrgent', checked)}*/}
{/* >*/}
{/* 加急处理(可能产生额外费用)*/}
{/* </Checkbox>*/}
{/* </Cell>*/}
{/*</CellGroup>*/}
{/* 提交按钮 */}
<View className="submit-section">
<Button
type="primary"
block
loading={submitting}
onClick={submitRefund}
>
{submitting ? '提交中...' : '提交退款申请'}
</Button>
</View>
</View>
)
}
export default RefundPage