Files
xinlong-shop-taro/src/pages/order/after-sale-apply.tsx
赵忠林 082646a613 feat(image): 全项目应用图片压缩优化
- 将图片显示统一替换为 getCompressedImageUrl 函数处理压缩
- 覆盖 35 个文件,包括 4 个组件和 31 个页面
- 主要组件:LazyImage、ProductCard、OrderCard、SkuSelector 内部自动压缩图片
- 主要页面:购物车、订单、积分、活动、拼团、秒杀、收藏、浏览历史等
- 跳过用户头像、二维码、商品详情大图及所有 Taro.previewImage 调用
- 调整压缩默认宽度为 300,保持质量90,启用压缩功能
2026-07-06 12:21:39 +08:00

342 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'
import { getCompressedImageUrl } from '@/utils/image'
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={getCompressedImageUrl(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