feat(user/order): 新增订单评价功能

- 添加订单评价页面组件
- 实现订单信息和商品列表展示
- 添加评价功能相关的样式和布局
- 优化订单列表页面,增加申请退款和查看物流等功能
This commit is contained in:
2025-08-23 16:22:01 +08:00
parent 7708968f53
commit 0a6f21d182
16 changed files with 3050 additions and 93 deletions

View File

@@ -0,0 +1,423 @@
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,
Uploader,
Loading,
Empty,
InputNumber
} from '@nutui/nutui-react-taro'
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 [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
})
useEffect(() => {
if (orderId) {
loadOrderInfo()
}
}, [orderId])
// 加载订单信息
const loadOrderInfo = async () => {
try {
setLoading(true)
// 模拟API调用
const mockOrderGoods: OrderGoods[] = [
{
goodsId: '1',
goodsName: 'iPhone 15 Pro Max 256GB 深空黑色',
goodsImage: 'https://via.placeholder.com/100x100',
goodsPrice: 9999,
goodsNum: 1,
canRefundNum: 1,
skuInfo: '颜色深空黑色容量256GB'
},
{
goodsId: '2',
goodsName: 'AirPods Pro 第三代',
goodsImage: 'https://via.placeholder.com/100x100',
goodsPrice: 1999,
goodsNum: 2,
canRefundNum: 2,
skuInfo: '颜色:白色'
}
]
const totalAmount = mockOrderGoods.reduce((sum, goods) =>
sum + goods.goodsPrice * goods.goodsNum, 0
)
await new Promise(resolve => setTimeout(resolve, 1000))
setOrderGoods(mockOrderGoods)
setOrderAmount(totalAmount)
// 初始化退款申请信息
setRefundApp(prev => ({
...prev,
refundAmount: totalAmount,
refundGoods: mockOrderGoods.map(goods => ({
goodsId: goods.goodsId,
refundNum: goods.goodsNum
}))
}))
} 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 handleImageUpload = async (files: any) => {
try {
const uploadedImages: string[] = []
for (const file of files) {
if (file.url) {
uploadedImages.push(file.url)
}
}
updateRefundApp('evidenceImages', uploadedImages)
} catch (error) {
console.error('图片上传失败:', error)
Taro.showToast({
title: '图片上传失败',
icon: 'none'
})
}
}
// 提交退款申请
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)
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 2000))
Taro.showToast({
title: '退款申请提交成功',
icon: 'success'
})
// 延迟返回上一页
setTimeout(() => {
Taro.navigateBack()
}, 1500)
} catch (error) {
console.error('提交退款申请失败:', error)
Taro.showToast({
title: '提交失败,请重试',
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>
)
}
if (orderGoods.length === 0) {
return (
<View className="refund-page">
<Empty
description="暂无订单信息"
imageSize={80}
>
<Button type="primary" size="small" onClick={() => Taro.navigateBack()}>
</Button>
</Empty>
</View>
)
}
return (
<View className="refund-page">
{/* 订单信息 */}
<View className="order-info">
<Text className="order-no">{orderNo}</Text>
<Text className="order-amount">¥{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, value)}
/>
<Text className="max-num">{goods.canRefundNum}</Text>
</View>
</View>
)
})}
</View>
)}
{/* 退款金额 */}
<CellGroup title="退款金额">
<Cell>
<Text className="refund-amount">¥{refundApp.refundAmount}</Text>
</Cell>
</CellGroup>
{/* 退款原因 */}
<CellGroup title="退款原因">
<RadioGroup
value={refundApp.refundReason}
onChange={(value) => updateRefundApp('refundReason', value)}
>
{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