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