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,3 @@
export default definePageConfig({
navigationBarTitleText: ''
})

View File

@@ -0,0 +1,191 @@
.evaluate-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 80px;
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 50vh;
.loading-text {
margin-top: 16px;
color: #666;
}
}
.order-info {
background: white;
padding: 16px 20px;
margin-bottom: 12px;
.order-no {
display: block;
color: #666;
margin-bottom: 4px;
}
.order-tip {
display: block;
font-weight: 500;
color: #333;
}
}
.goods-list {
.goods-item {
background: white;
margin-bottom: 12px;
padding: 20px;
.goods-info {
display: flex;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
.goods-image {
width: 80px;
height: 80px;
border-radius: 8px;
margin-right: 12px;
flex-shrink: 0;
}
.goods-detail {
flex: 1;
.goods-name {
display: block;
font-weight: 500;
color: #333;
line-height: 1.4;
margin-bottom: 8px;
}
.goods-sku {
display: block;
color: #999;
margin-bottom: 8px;
}
.goods-price {
display: block;
font-weight: 600;
color: #ff6b35;
}
}
}
.rating-section {
margin-bottom: 20px;
.rating-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.rating-label {
font-weight: 500;
color: #333;
}
.rating-text {
color: #ff6b35;
font-weight: 500;
}
}
}
.content-section {
margin-bottom: 20px;
.content-label {
display: block;
font-weight: 500;
color: #333;
margin-bottom: 12px;
}
}
.image-section {
.image-label {
display: block;
font-weight: 500;
color: #333;
margin-bottom: 12px;
}
}
}
}
.submit-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 12px 16px;
border-top: 1px solid #f0f0f0;
z-index: 100;
}
}
/* NutUI 组件样式覆盖 */
.evaluate-page {
.nut-rate {
.nut-rate-item {
margin-right: 8px;
}
}
.nut-textarea {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 12px;
background: #fafafa;
&:focus {
border-color: #1890ff;
background: white;
}
}
.nut-uploader {
.nut-uploader-slot {
border: 2px dashed #e8e8e8;
border-radius: 8px;
background: #fafafa;
&:hover {
border-color: #1890ff;
}
}
.nut-uploader-preview {
border-radius: 8px;
overflow: hidden;
}
}
}
/* 适配不同屏幕尺寸 */
@media (max-width: 375px) {
.evaluate-page {
.goods-list {
.goods-item {
padding: 16px;
.goods-info {
.goods-image {
width: 70px;
height: 70px;
}
}
}
}
}
}

View File

@@ -0,0 +1,304 @@
import React, { useState, useEffect } from 'react'
import Taro, { useRouter } from '@tarojs/taro'
import { View, Text, Image } from '@tarojs/components'
import {
Rate,
TextArea,
Button,
Uploader,
Loading,
Empty
} from '@nutui/nutui-react-taro'
import './index.scss'
// 订单商品信息
interface OrderGoods {
goodsId: string
goodsName: string
goodsImage: string
goodsPrice: number
goodsNum: number
skuInfo?: string
}
// 评价信息
interface EvaluateInfo {
goodsId: string
rating: number // 评分 1-5
content: string // 评价内容
images: string[] // 评价图片
isAnonymous: boolean // 是否匿名
}
const EvaluatePage: 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 [evaluates, setEvaluates] = useState<Map<string, EvaluateInfo>>(new Map())
useEffect(() => {
if (orderId) {
loadOrderGoods()
}
}, [orderId])
// 加载订单商品信息
const loadOrderGoods = async () => {
try {
setLoading(true)
// 模拟API调用 - 实际项目中替换为真实API
const mockOrderGoods: OrderGoods[] = [
{
goodsId: '1',
goodsName: 'iPhone 15 Pro Max 256GB 深空黑色',
goodsImage: 'https://via.placeholder.com/100x100',
goodsPrice: 9999,
goodsNum: 1,
skuInfo: '颜色深空黑色容量256GB'
},
{
goodsId: '2',
goodsName: 'AirPods Pro 第三代',
goodsImage: 'https://via.placeholder.com/100x100',
goodsPrice: 1999,
goodsNum: 1,
skuInfo: '颜色:白色'
}
]
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000))
setOrderGoods(mockOrderGoods)
// 初始化评价信息
const initialEvaluates = new Map<string, EvaluateInfo>()
mockOrderGoods.forEach(goods => {
initialEvaluates.set(goods.goodsId, {
goodsId: goods.goodsId,
rating: 5,
content: '',
images: [],
isAnonymous: false
})
})
setEvaluates(initialEvaluates)
} catch (error) {
console.error('加载订单商品失败:', error)
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
setLoading(false)
}
}
// 更新评价信息
const updateEvaluate = (goodsId: string, field: keyof EvaluateInfo, value: any) => {
setEvaluates(prev => {
const newEvaluates = new Map(prev)
const evaluate = newEvaluates.get(goodsId)
if (evaluate) {
newEvaluates.set(goodsId, {
...evaluate,
[field]: value
})
}
return newEvaluates
})
}
// 处理图片上传
const handleImageUpload = async (goodsId: string, files: any) => {
try {
// 模拟图片上传
const uploadedImages: string[] = []
for (const file of files) {
if (file.url) {
uploadedImages.push(file.url)
}
}
updateEvaluate(goodsId, 'images', uploadedImages)
} catch (error) {
console.error('图片上传失败:', error)
Taro.showToast({
title: '图片上传失败',
icon: 'none'
})
}
}
// 提交评价
const submitEvaluate = async () => {
try {
// 验证评价内容
const evaluateList = Array.from(evaluates.values())
const invalidEvaluate = evaluateList.find(evaluate =>
evaluate.rating < 1 || evaluate.rating > 5
)
if (invalidEvaluate) {
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)
}
}
// 获取评分文字描述
const getRatingText = (rating: number) => {
const texts = ['', '很差', '一般', '满意', '很好', '非常满意']
return texts[rating] || ''
}
if (loading) {
return (
<View className="evaluate-page">
<View className="loading-container">
<Loading type="spinner" />
<Text className="loading-text">...</Text>
</View>
</View>
)
}
if (orderGoods.length === 0) {
return (
<View className="evaluate-page">
<Empty
description="暂无商品信息"
imageSize={80}
>
<Button type="primary" size="small" onClick={() => Taro.navigateBack()}>
</Button>
</Empty>
</View>
)
}
return (
<View className="evaluate-page">
{/* 订单信息 */}
<View className="order-info">
<Text className="order-no">{orderNo}</Text>
<Text className="order-tip"></Text>
</View>
{/* 商品评价列表 */}
<View className="goods-list">
{orderGoods.map(goods => {
const evaluate = evaluates.get(goods.goodsId)
if (!evaluate) return null
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="rating-section">
<View className="rating-header">
<Text className="rating-label"></Text>
<Text className="rating-text">{getRatingText(evaluate.rating)}</Text>
</View>
<Rate
value={evaluate.rating}
onChange={(value) => updateEvaluate(goods.goodsId, 'rating', value)}
allowHalf={false}
/>
</View>
{/* 评价内容 */}
<View className="content-section">
<Text className="content-label"></Text>
<TextArea
placeholder="请描述您对商品的使用感受..."
value={evaluate.content}
onChange={(value) => updateEvaluate(goods.goodsId, 'content', value)}
maxLength={500}
showCount
rows={6}
/>
</View>
{/* 图片上传 */}
<View className="image-section">
<Text className="image-label"></Text>
<Uploader
value={evaluate.images.map(url => ({ url }))}
onChange={(files) => handleImageUpload(goods.goodsId, files)}
multiple
maxCount={6}
previewType="picture"
deletable
/>
</View>
</View>
)
})}
</View>
{/* 提交按钮 */}
<View className="submit-section">
<Button
type="primary"
block
loading={submitting}
onClick={submitEvaluate}
>
{submitting ? '提交中...' : '提交评价'}
</Button>
</View>
</View>
)
}
export default EvaluatePage