Files
xinlong-shop-taro/src/pages/order/after-sale-apply.tsx
赵忠林 f3886664f7 fix(api): 替换所有接口请求的域名为 guilixu-api.websoft.top
- 将获取 STS Token 的接口地址由 paopao-api 改为 guilixu-api
- 更新图片上传接口地址为新的 guilixu-api 域名
- 修改用户推广页面中邀请码链接和二维码接口的域名
- 更改注册页微信登录接口请求的域名为 guilixu-api
2026-06-16 17:15:59 +08:00

341 lines
11 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 { View, Text, Input, Image, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { Button, Uploader } from '@nutui/nutui-react-taro'
import { getShopOrder } from '@/api/shop/shopOrder'
import type { ShopOrder } from '@/api/shop/shopOrder/model'
import Price from '@/components/common/Price'
definePageConfig({
navigationBarTitleText: '申请售后',
})
// 售后类型
const AFTER_SALE_TYPES = [
{ value: 'refund', label: '退款', desc: '仅退款,商品无需退回' },
{ value: 'return', label: '退货退款', desc: '退款并退货,商品需退回' },
]
// 退款原因选项
const REASON_OPTIONS = [
'商品损坏/有瑕疵',
'商品与描述不符',
'收到错误商品',
'商品少发/漏发',
'不想要了',
'其他原因',
]
const AfterSaleApplyPage: React.FC = () => {
const { orderId, type } = Taro.getCurrentInstance().router?.params || {}
const [order, setOrder] = useState<ShopOrder | null>(null)
const [loading, setLoading] = useState(false)
// 售后类型
const [afterSaleType, setAfterSaleType] = useState<'refund' | 'return'>(
(type as 'refund' | 'return') || 'refund'
)
// 退款原因
const [selectedReason, setSelectedReason] = useState('')
const [reasonDesc, setReasonDesc] = useState('')
// 图片上传
const [images, setImages] = useState<string[]>([])
// 选中的商品
const [selectedItems, setSelectedItems] = useState<number[]>([])
// 可退金额
const [refundAmount, setRefundAmount] = useState('0.00')
useEffect(() => {
if (orderId) {
loadOrder()
}
}, [orderId])
const loadOrder = async () => {
try {
const res = await getShopOrder(Number(orderId))
if (res) {
setOrder(res)
// 默认选中所有商品
const allItemIndices = (res.orderGoods || []).map((_, idx) => idx)
setSelectedItems(allItemIndices)
// 计算可退金额
const amount = calculateRefundAmount(res, allItemIndices)
setRefundAmount(amount)
}
} catch (err) {
console.error('加载订单失败', err)
Taro.showToast({ title: '加载失败', icon: 'none' })
}
}
// 计算可退金额
const calculateRefundAmount = (orderData: ShopOrder, selectedIdx: number[]): string => {
if (!orderData.orderGoods || selectedIdx.length === 0) return '0.00'
let total = 0
orderData.orderGoods.forEach((item, idx) => {
if (selectedIdx.includes(idx)) {
total += Number(item.price || 0) * (item.num || item.quantity || 1)
}
})
// 按比例计算优惠分摊
const totalPrice = Number(orderData.totalPrice || 0)
if (totalPrice > 0) {
const payPrice = Number(orderData.payPrice || 0)
const ratio = payPrice / totalPrice
total = total * ratio
}
return total.toFixed(2)
}
// 切换商品选中
const toggleItem = (idx: number) => {
const newSelected = selectedItems.includes(idx)
? selectedItems.filter(i => i !== idx)
: [...selectedItems, idx]
setSelectedItems(newSelected)
if (order) {
setRefundAmount(calculateRefundAmount(order, newSelected))
}
}
// 全选/取消全选
const toggleAll = () => {
if (!order?.orderGoods) return
if (selectedItems.length === order.orderGoods.length) {
setSelectedItems([])
setRefundAmount('0.00')
} else {
const allIdx = order.orderGoods.map((_, idx) => idx)
setSelectedItems(allIdx)
setRefundAmount(calculateRefundAmount(order, allIdx))
}
}
// 提交申请
const handleSubmit = async () => {
if (selectedItems.length === 0) {
Taro.showToast({ title: '请选择要售后的商品', icon: 'none' })
return
}
if (!selectedReason) {
Taro.showToast({ title: '请选择退款原因', icon: 'none' })
return
}
Taro.showModal({
title: '确认提交',
content: `确认提交${afterSaleType === 'refund' ? '退款' : '退货退款'}申请?`,
success: async (res) => {
if (res.confirm) {
await submitApply()
}
},
})
}
const submitApply = async () => {
if (!selectedReason) {
Taro.showToast({ title: '请选择退款原因', icon: 'none' })
return
}
setLoading(true)
try {
const res = await applyAfterSale({
orderId: orderId || '',
type: afterSaleType,
reason: selectedReason,
description: reasonDesc,
amount: parseFloat(refundAmount),
evidenceImages: images
})
if (res.success) {
Taro.showToast({ title: '申请提交成功', icon: 'success' })
setTimeout(() => {
Taro.redirectTo({ url: '/pages/order/after-sale-list' })
}, 1500)
} else {
Taro.showToast({ title: res.message || '提交失败', icon: 'none' })
}
} catch (err) {
console.error('申请售后失败:', err)
Taro.showToast({ title: '提交失败', icon: 'none' })
} finally {
setLoading(false)
}
}
if (!order) {
return (
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
<Text className='text-gray-400 text-sm'>...</Text>
</View>
)
}
return (
<View className='min-h-screen bg-gray-50 pb-20 flex flex-col'>
<ScrollView scrollY className='flex-1'>
{/* 售后类型选择 */}
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
<Text className='text-sm font-medium text-gray-800 mb-3 block'></Text>
<View className='flex gap-3'>
{AFTER_SALE_TYPES.map(item => (
<View
key={item.value}
className={`flex-1 p-3 rounded-lg border-2 text-center ${
afterSaleType === item.value
? 'border-green-500 bg-green-50'
: 'border-gray-200 bg-white'
}`}
onClick={() => setAfterSaleType(item.value as 'refund' | 'return')}
>
<Text className={`text-sm font-medium block ${
afterSaleType === item.value ? 'text-green-600' : 'text-gray-600'
}`}>
{item.label}
</Text>
<Text className='text-xs text-gray-400 mt-1 block'>{item.desc}</Text>
</View>
))}
</View>
</View>
{/* 商品选择 */}
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
<View className='flex justify-between items-center mb-3'>
<Text className='text-sm font-medium text-gray-800'></Text>
<Text
className='text-xs text-green-600'
onClick={toggleAll}
>
{selectedItems.length === order.orderGoods?.length ? '取消全选' : '全选'}
</Text>
</View>
{order.orderGoods?.map((item, idx) => (
<View
key={idx}
className='flex items-center gap-3 py-3 border-b border-gray-50'
style={idx === (order.orderGoods?.length ?? 0) - 1 ? { borderBottom: 'none' } : undefined}
onClick={() => toggleItem(idx)}
>
{/* 选中状态 */}
<View className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
selectedItems.includes(idx)
? 'border-green-500 bg-green-500'
: 'border-gray-300'
}`}>
{selectedItems.includes(idx) && (
<Text className='text-white text-xs'></Text>
)}
</View>
{/* 商品图片 */}
<Image
className='w-16 h-16 rounded-lg'
src={item.image || ''}
mode='aspectFill'
/>
{/* 商品信息 */}
<View className='flex-1'>
<Text className='text-sm text-gray-700 line-clamp-2'>{item.goodsName}</Text>
<View className='flex justify-between items-center mt-1'>
<Price price={item.price || '0'} size='small' />
<Text className='text-xs text-gray-500'>x{item.num || item.quantity || 1}</Text>
</View>
</View>
</View>
))}
</View>
{/* 退款原因 */}
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
<Text className='text-sm font-medium text-gray-800 mb-3 block'>退</Text>
<View className='flex flex-wrap gap-2'>
{REASON_OPTIONS.map(reason => (
<View
key={reason}
className={`px-3 py-2 rounded-full text-sm ${
selectedReason === reason
? 'bg-green-50 text-green-600 border border-green-500'
: 'bg-gray-50 text-gray-600 border border-gray-200'
}`}
onClick={() => setSelectedReason(reason)}
>
<Text>{reason}</Text>
</View>
))}
</View>
{/* 补充说明 */}
<View className='mt-3'>
<Text className='text-xs text-gray-500 mb-1 block'></Text>
<View className='bg-gray-50 rounded-lg p-3'>
<Input
className='w-full text-sm'
placeholder='请详细描述您遇到的问题'
value={reasonDesc}
onInput={e => setReasonDesc(e.detail.value)}
style={{ minHeight: '80px' }}
/>
</View>
</View>
</View>
{/* 上传凭证 */}
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
<Text className='text-sm font-medium text-gray-800 mb-3 block'></Text>
<Uploader
value={images.map((url, idx) => ({ id: String(idx), url }))}
onChange={(files) => {
setImages(files.map(f => f.url || ''))
}}
maxCount={3}
/>
</View>
{/* 退款金额 */}
<View className='bg-white mx-3 mt-3 p-4 rounded-xl mb-4'>
<View className='flex justify-between items-center'>
<Text className='text-sm font-medium text-gray-800'>退</Text>
<Text className='text-lg font-bold text-red-500'>
{'\u00A5'}{refundAmount}
</Text>
</View>
{afterSaleType === 'return' && (
<Text className='text-xs text-gray-400 mt-2 block'>
退退
</Text>
)}
</View>
</ScrollView>
{/* 提交按钮 */}
<View className='bg-white border-t border-gray-100 p-3' style={{ paddingBottom: '20px' }}>
<Button
type='primary'
className='w-full rounded-full'
loading={loading}
style={{ backgroundColor: '#0e932e' }}
onClick={handleSubmit}
>
</Button>
</View>
</View>
)
}
export default AfterSaleApplyPage