feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
340
src_bak/pages/order/after-sale-apply.tsx
Normal file
340
src_bak/pages/order/after-sale-apply.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
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
|
||||
257
src_bak/pages/order/after-sale-detail.tsx
Normal file
257
src_bak/pages/order/after-sale-detail.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button } from '@nutui/nutui-react-taro'
|
||||
import { getAfterSaleDetail, cancelAfterSale, formatAfterSaleStatus, AFTER_SALE_TYPE_MAP } from '@/api/shop/shopAfterSale'
|
||||
import type { AfterSaleDetail } from '@/api/shop/shopAfterSale'
|
||||
import Price from '@/components/common/Price'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '售后详情',
|
||||
})
|
||||
|
||||
const AfterSaleDetailPage: React.FC = () => {
|
||||
const { id, orderId } = Taro.getCurrentInstance().router?.params || {}
|
||||
const [detail, setDetail] = useState<AfterSaleDetail | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [cancelling, setCancelling] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadDetail()
|
||||
}, [id, orderId])
|
||||
|
||||
const loadDetail = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getAfterSaleDetail({ afterSaleId: id, orderId })
|
||||
if (res?.data) {
|
||||
setDetail(res.data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载售后详情失败', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消申请
|
||||
const handleCancel = () => {
|
||||
Taro.showModal({
|
||||
title: '确认取消',
|
||||
content: '确定要撤销售后申请吗?取消后不可恢复。',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
setCancelling(true)
|
||||
try {
|
||||
// await cancelAfterSale(id || '')
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
Taro.showToast({ title: '已取消申请', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack()
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
Taro.showToast({ title: '取消失败', icon: 'none' })
|
||||
} finally {
|
||||
setCancelling(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
if (!detail) {
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
|
||||
<Text className='text-gray-400 text-sm'>{loading ? '加载中...' : '加载失败'}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const statusInfo = formatAfterSaleStatus(detail.status)
|
||||
const canCancel = ['pending'].includes(detail.status)
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 pb-20 flex flex-col'>
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
{/* 状态头部 */}
|
||||
<View
|
||||
className='p-5 text-center'
|
||||
style={{ backgroundColor: statusInfo.color }}
|
||||
>
|
||||
<Text className='text-white text-xl font-medium block'>{statusInfo.text}</Text>
|
||||
{detail.status === 'processing' && (
|
||||
<Text className='text-white text-sm opacity-80 mt-1 block'>
|
||||
预计1-3个工作日内处理
|
||||
</Text>
|
||||
)}
|
||||
{detail.status === 'pending' && (
|
||||
<Text className='text-white text-sm opacity-80 mt-1 block'>
|
||||
等待商家审核中
|
||||
</Text>
|
||||
)}
|
||||
{detail.status === 'approved' && (
|
||||
<Text className='text-white text-sm opacity-80 mt-1 block'>
|
||||
商家已同意,请按提示操作
|
||||
</Text>
|
||||
)}
|
||||
{detail.status === 'completed' && (
|
||||
<Text className='text-white text-sm opacity-80 mt-1 block'>
|
||||
退款已原路返回
|
||||
</Text>
|
||||
)}
|
||||
{detail.status === 'rejected' && (
|
||||
<Text className='text-white text-sm opacity-80 mt-1 block'>
|
||||
{detail.rejectReason || '商家拒绝了您的申请'}
|
||||
</Text>
|
||||
)}
|
||||
</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='relative'>
|
||||
{/* 竖线 */}
|
||||
<View className='absolute left-4 top-0 bottom-0 w-px bg-gray-200' />
|
||||
|
||||
{detail.progressRecords.map((record, idx) => (
|
||||
<View key={record.id} className={`relative flex gap-3 pb-4 ${idx === detail.progressRecords.length - 1 ? 'pb-0' : ''}`}>
|
||||
{/* 圆点 */}
|
||||
<View
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center z-10 ${
|
||||
idx === 0 ? 'bg-green-500' : 'bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{idx === 0 ? (
|
||||
<Text className='text-white text-xs'>✓</Text>
|
||||
) : (
|
||||
<View className='w-2 h-2 rounded-full bg-gray-400' />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 内容 */}
|
||||
<View className='flex-1 pt-1'>
|
||||
<View className='flex justify-between items-center'>
|
||||
<Text className={`text-sm font-medium ${
|
||||
idx === 0 ? 'text-gray-800' : 'text-gray-500'
|
||||
}`}>
|
||||
{record.status}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-400'>{formatTime(record.time)}</Text>
|
||||
</View>
|
||||
<Text className={`text-xs mt-1 ${idx === 0 ? 'text-gray-600' : 'text-gray-400'}`}>
|
||||
{record.description}
|
||||
</Text>
|
||||
{record.operator && (
|
||||
<Text className='text-xs text-gray-400 mt-1'>
|
||||
操作人: {record.operator}
|
||||
</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-col" style={{ gap: '8px' }}>
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-sm text-gray-500'>售后类型</Text>
|
||||
<Text className='text-sm text-gray-800'>
|
||||
{AFTER_SALE_TYPE_MAP[detail.type] || detail.type}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-sm text-gray-500'>申请原因</Text>
|
||||
<Text className='text-sm text-gray-800'>{detail.reason}</Text>
|
||||
</View>
|
||||
{detail.description && (
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-sm text-gray-500'>补充说明</Text>
|
||||
<Text className='text-sm text-gray-800 max-w-50 text-right'>{detail.description}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-sm text-gray-500'>申请时间</Text>
|
||||
<Text className='text-sm text-gray-800'>{formatTime(detail.applyTime)}</Text>
|
||||
</View>
|
||||
{detail.contactPhone && (
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-sm text-gray-500'>联系电话</Text>
|
||||
<Text className='text-sm text-gray-800'>{detail.contactPhone}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 退款金额 */}
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||||
<View className='flex justify-between items-center'>
|
||||
<Text className='text-sm font-medium text-gray-800'>退款金额</Text>
|
||||
<Text className='text-xl font-bold text-red-500'>
|
||||
{'\u00A5'}{detail.amount.toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-400 mt-2 block'>
|
||||
退款将原路返回,微信支付退款一般1-3个工作日到账
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 凭证图片 */}
|
||||
{detail.evidenceImages && detail.evidenceImages.length > 0 && (
|
||||
<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-2 flex-wrap'>
|
||||
{detail.evidenceImages.map((url, idx) => (
|
||||
<Image
|
||||
key={idx}
|
||||
className='w-20 h-20 rounded-lg'
|
||||
src={url}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 订单信息 */}
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl mb-4'>
|
||||
<View className='flex justify-between items-center mb-2'>
|
||||
<Text className='text-sm font-medium text-gray-800'>关联订单</Text>
|
||||
<View
|
||||
className='px-3 py-1 rounded-full bg-gray-50'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/order/detail?id=${detail.orderId}` })}
|
||||
>
|
||||
<Text className='text-xs text-gray-500'>查看订单 ›</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className='text-sm text-gray-400'>订单号: {detail.orderNo}</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
{canCancel && (
|
||||
<View className='bg-white border-t border-gray-100 p-3' style={{ paddingBottom: '20px' }}>
|
||||
<Button
|
||||
type='default'
|
||||
className='w-full rounded-full border-gray-200'
|
||||
loading={cancelling}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
撤销售后申请
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default AfterSaleDetailPage
|
||||
174
src_bak/pages/order/after-sale-list.tsx
Normal file
174
src_bak/pages/order/after-sale-list.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Tabs } from '@nutui/nutui-react-taro'
|
||||
import { pageAfterSaleList } from '@/api/shop/shopAfterSale'
|
||||
import type { AfterSaleDetail, AfterSaleStatus } from '@/api/shop/shopAfterSale'
|
||||
import { formatAfterSaleStatus, AFTER_SALE_STATUS_MAP } from '@/api/shop/shopAfterSale'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '售后列表',
|
||||
})
|
||||
|
||||
// Tab 配置
|
||||
const TAB_LIST = [
|
||||
{ title: '全部', status: undefined },
|
||||
{ title: '处理中', status: 'processing' as AfterSaleStatus },
|
||||
{ title: '待收货', status: 'approved' as AfterSaleStatus },
|
||||
{ title: '已完成', status: 'completed' as AfterSaleStatus },
|
||||
{ title: '已拒绝', status: 'rejected' as AfterSaleStatus },
|
||||
]
|
||||
|
||||
// 状态对应的 API 查询值
|
||||
const STATUS_QUERY_MAP: Record<string, AfterSaleStatus | undefined> = {
|
||||
'processing': 'processing',
|
||||
'approved': 'approved',
|
||||
'completed': 'completed',
|
||||
'rejected': 'rejected',
|
||||
}
|
||||
|
||||
const AfterSaleListPage: React.FC = () => {
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
const [list, setList] = useState<AfterSaleDetail[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
setList([])
|
||||
setPage(1)
|
||||
setFinished(false)
|
||||
loadList(1)
|
||||
}, [tabIndex])
|
||||
|
||||
const loadList = async (p: number) => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const status = TAB_LIST[tabIndex]?.status
|
||||
const res = await pageAfterSaleList({
|
||||
page: p,
|
||||
pageSize: 10,
|
||||
status: STATUS_QUERY_MAP[status || ''],
|
||||
})
|
||||
|
||||
const newList = res?.data?.list || []
|
||||
if (p === 1) {
|
||||
setList(newList)
|
||||
} else {
|
||||
setList(prev => [...prev, ...newList])
|
||||
}
|
||||
setFinished(newList.length < 10)
|
||||
setPage(p)
|
||||
} catch (err) {
|
||||
console.error('加载售后列表失败', err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (!finished && !loading) {
|
||||
loadList(page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转详情
|
||||
const goToDetail = (item: AfterSaleDetail) => {
|
||||
Taro.navigateTo({
|
||||
url: `/pages/order/after-sale-detail?id=${item.id}&orderId=${item.orderId}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 售后类型文字
|
||||
const getTypeText = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
refund: '退款',
|
||||
return: '退货退款',
|
||||
exchange: '换货',
|
||||
repair: '维修',
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<Tabs value={tabIndex} onChange={(val) => setTabIndex(val as number)} type='smile'>
|
||||
{TAB_LIST.map((tab) => (
|
||||
<Tabs.TabPane key={tab.title} title={tab.title}>
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='flex-1'
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={100}
|
||||
>
|
||||
<View className='p-3'>
|
||||
{list.length > 0 ? (
|
||||
list.map(item => {
|
||||
const statusInfo = formatAfterSaleStatus(item.status)
|
||||
return (
|
||||
<View
|
||||
key={item.id}
|
||||
className='bg-white rounded-xl p-4 mb-3'
|
||||
onClick={() => goToDetail(item)}
|
||||
>
|
||||
{/* 状态头部 */}
|
||||
<View className='flex justify-between items-center mb-3'>
|
||||
<Text className='text-sm text-gray-400'>订单号: {item.orderNo}</Text>
|
||||
<View className='flex items-center gap-1'>
|
||||
<Text className='text-sm font-medium' style={{ color: statusInfo.color }}>
|
||||
{statusInfo.text}
|
||||
</Text>
|
||||
<Text className='text-gray-400'>›</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 售后类型 */}
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<View className={`px-2 py-1 rounded text-xs ${
|
||||
item.type === 'refund' ? 'bg-blue-50 text-blue-600' : 'bg-orange-50 text-orange-600'
|
||||
}`}>
|
||||
<Text>{getTypeText(item.type)}</Text>
|
||||
</View>
|
||||
<Text className='text-sm text-gray-600'>{item.reason}</Text>
|
||||
</View>
|
||||
|
||||
{/* 金额 */}
|
||||
<View className='flex justify-between items-center pt-2 border-t border-gray-50'>
|
||||
<Text className='text-xs text-gray-400'>申请时间: {formatTime(item.applyTime)}</Text>
|
||||
<View className='flex items-center gap-1'>
|
||||
<Text className='text-xs text-gray-500'>退款金额</Text>
|
||||
<Text className='text-base font-bold text-red-500'>
|
||||
{'\u00A5'}{item.amount.toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
) : !loading ? (
|
||||
<EmptyState text='暂无售后记录' />
|
||||
) : null}
|
||||
|
||||
<LoadMore loading={loading} finished={finished} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default AfterSaleListPage
|
||||
519
src_bak/pages/order/detail.tsx
Normal file
519
src_bak/pages/order/detail.tsx
Normal file
@@ -0,0 +1,519 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button } from '@nutui/nutui-react-taro'
|
||||
import { getShopOrder, prepayShopOrder, updateShopOrder, repairOrder, removeShopOrder } from '@/api/shop/shopOrder'
|
||||
import { listShopOrderGoodsByOrderId } from '@/api/shop/shopOrderGoods'
|
||||
import type { ShopOrder } from '@/api/shop/shopOrder/model'
|
||||
import { OrderStatus, OrderStatusText } from '@/types/order'
|
||||
import Price from '@/components/common/Price'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '订单详情',
|
||||
})
|
||||
|
||||
// 支付方式映射
|
||||
const PAY_TYPE_MAP: Record<number, string> = {
|
||||
0: '货到付款',
|
||||
1: '微信支付',
|
||||
2: '会员卡支付',
|
||||
3: '支付宝',
|
||||
15: '积分支付',
|
||||
}
|
||||
|
||||
// 发票状态映射
|
||||
const INVOICE_STATUS_MAP: Record<number, string> = {
|
||||
0: '未开票',
|
||||
1: '已开票',
|
||||
2: '不可开票',
|
||||
}
|
||||
|
||||
// 发货状态映射
|
||||
const DELIVERY_STATUS_MAP: Record<number, string> = {
|
||||
10: '未发货',
|
||||
20: '已发货',
|
||||
30: '部分发货',
|
||||
}
|
||||
|
||||
/** 根据订单状态计算展示用的状态标签 */
|
||||
const getOrderDisplayStatus = (order: ShopOrder): { title: string; bgColor: string; subtitle: string } => {
|
||||
const { payStatus, deliveryStatus, orderStatus } = order
|
||||
|
||||
// 退款/取消相关状态
|
||||
if (orderStatus === OrderStatus.Cancelled) {
|
||||
return { bgColor: '#999', title: '已取消', subtitle: '订单已取消' }
|
||||
}
|
||||
if (orderStatus === OrderStatus.Cancelling) {
|
||||
return { bgColor: '#ff7d00', title: '取消中', subtitle: '退款处理中' }
|
||||
}
|
||||
if (orderStatus === OrderStatus.RefundSuccess) {
|
||||
return { bgColor: '#999', title: '已退款', subtitle: '退款已原路返回' }
|
||||
}
|
||||
if (orderStatus === OrderStatus.RefundApply || orderStatus === OrderStatus.ClientRefundApply) {
|
||||
return { bgColor: '#ee0a24', title: '退款申请中', subtitle: '商家正在处理退款' }
|
||||
}
|
||||
if (orderStatus === OrderStatus.RefundRejected) {
|
||||
return { bgColor: '#ee0a24', title: '退款被拒绝', subtitle: '退款申请已被拒绝' }
|
||||
}
|
||||
|
||||
// 正常订单流程
|
||||
if (!payStatus) {
|
||||
return { bgColor: '#ff7d00', title: '待付款', subtitle: '请尽快完成支付' }
|
||||
}
|
||||
if (deliveryStatus === 10) {
|
||||
return { bgColor: '#4b9cf5', title: '待发货', subtitle: '商家正在准备商品' }
|
||||
}
|
||||
if (deliveryStatus === 20 || deliveryStatus === 30) {
|
||||
return { bgColor: '#4b9cf5', title: '待收货', subtitle: '商品运输中,请注意查收' }
|
||||
}
|
||||
if (orderStatus === OrderStatus.Completed) {
|
||||
return { bgColor: '#0e932e', title: '已完成', subtitle: '交易已完成,感谢购买' }
|
||||
}
|
||||
if (orderStatus === OrderStatus.Unused) {
|
||||
return { bgColor: '#0e932e', title: '已付款', subtitle: '订单已支付' }
|
||||
}
|
||||
|
||||
// 默认
|
||||
return { bgColor: '#999', title: OrderStatusText[orderStatus ?? 0] || '未知', subtitle: '' }
|
||||
}
|
||||
|
||||
/** 根据订单状态判断当前操作阶段 */
|
||||
type OrderPhase = 'unpaid' | 'unshipped' | 'shipped' | 'completed' | 'cancelled' | 'refund'
|
||||
|
||||
const getOrderPhase = (order: ShopOrder): OrderPhase => {
|
||||
const { payStatus, deliveryStatus, orderStatus } = order
|
||||
|
||||
// 退款/取消
|
||||
if ([OrderStatus.Cancelled, OrderStatus.Cancelling, OrderStatus.RefundSuccess, OrderStatus.RefundApply, OrderStatus.ClientRefundApply, OrderStatus.RefundRejected].includes(orderStatus as OrderStatus)) {
|
||||
return orderStatus === OrderStatus.Cancelled || orderStatus === OrderStatus.RefundSuccess ? 'cancelled' : 'refund'
|
||||
}
|
||||
|
||||
if (!payStatus) return 'unpaid'
|
||||
if (deliveryStatus === 10) return 'unshipped'
|
||||
if (deliveryStatus === 20 || deliveryStatus === 30) return 'shipped'
|
||||
if (orderStatus === OrderStatus.Completed) return 'completed'
|
||||
|
||||
return 'unshipped'
|
||||
}
|
||||
|
||||
const OrderDetailPage: React.FC = () => {
|
||||
const { id } = Taro.getCurrentInstance().router?.params || {}
|
||||
const [order, setOrder] = useState<ShopOrder | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const loadOrder = useCallback(async () => {
|
||||
if (!id) return
|
||||
try {
|
||||
const orderData = await getShopOrder(Number(id))
|
||||
// 如果订单中没有商品清单,则单独加载
|
||||
if (!orderData.orderGoods || orderData.orderGoods.length === 0) {
|
||||
try {
|
||||
const goods = await listShopOrderGoodsByOrderId(Number(id))
|
||||
orderData.orderGoods = goods
|
||||
} catch {
|
||||
// 忽略加载商品失败
|
||||
}
|
||||
}
|
||||
setOrder(orderData)
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
}, [id])
|
||||
|
||||
useEffect(() => {
|
||||
loadOrder()
|
||||
}, [loadOrder])
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string | undefined) => {
|
||||
if (!time) return '-'
|
||||
return time.replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
// 取消订单
|
||||
const handleCancelOrder = () => {
|
||||
Taro.showModal({
|
||||
title: '确认取消',
|
||||
content: '确定要取消该订单吗?',
|
||||
confirmColor: '#ee0a24',
|
||||
success: async (res) => {
|
||||
if (res.confirm && order) {
|
||||
try {
|
||||
await updateShopOrder({ ...order, orderStatus: OrderStatus.Cancelled })
|
||||
Taro.showToast({ title: '订单已取消', icon: 'success' })
|
||||
loadOrder()
|
||||
} catch {
|
||||
Taro.showToast({ title: '取消失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 立即支付
|
||||
const handlePay = async () => {
|
||||
if (!order || loading) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const payResult = await prepayShopOrder({ orderId: order.orderId!, payType: 1 })
|
||||
if (payResult) {
|
||||
await Taro.requestPayment({
|
||||
timeStamp: payResult.timeStamp,
|
||||
nonceStr: payResult.nonceStr,
|
||||
package: payResult.package,
|
||||
signType: payResult.signType,
|
||||
paySign: payResult.paySign,
|
||||
})
|
||||
Taro.showToast({ title: '支付成功', icon: 'success' })
|
||||
loadOrder()
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err?.message !== 'requestPayment:fail cancel') {
|
||||
Taro.showToast({ title: err?.message || '支付失败', icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 确认收货
|
||||
const handleConfirmReceive = () => {
|
||||
Taro.showModal({
|
||||
title: '确认收货',
|
||||
content: '确认已收到商品?',
|
||||
confirmColor: '#0e932e',
|
||||
success: async (res) => {
|
||||
if (res.confirm && order) {
|
||||
try {
|
||||
await updateShopOrder({ ...order, orderStatus: OrderStatus.Completed })
|
||||
Taro.showToast({ title: '已确认收货', icon: 'success' })
|
||||
loadOrder()
|
||||
} catch {
|
||||
Taro.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除订单
|
||||
const handleDeleteOrder = () => {
|
||||
Taro.showModal({
|
||||
title: '删除订单',
|
||||
content: '确定要删除该订单吗?删除后不可恢复',
|
||||
confirmColor: '#ee0a24',
|
||||
success: async (res) => {
|
||||
if (res.confirm && order) {
|
||||
try {
|
||||
await removeShopOrder(order.orderId)
|
||||
Taro.showToast({ title: '订单已删除', icon: 'success' })
|
||||
setTimeout(() => Taro.navigateBack(), 1000)
|
||||
} catch {
|
||||
Taro.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
const displayStatus = getOrderDisplayStatus(order)
|
||||
const phase = getOrderPhase(order)
|
||||
const isExpress = order.deliveryType === 0 || order.deliveryType === undefined
|
||||
const goodsCount = order.orderGoods?.reduce((sum, g) => sum + (g.totalNum || 1), 0) || 0
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex flex-col'>
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
{/* 状态卡片 */}
|
||||
<View
|
||||
className='p-4'
|
||||
style={{ backgroundColor: displayStatus.bgColor }}
|
||||
>
|
||||
<Text className='text-white text-lg font-medium block'>{displayStatus.title}</Text>
|
||||
<Text className='text-white text-sm opacity-90 mt-1 block'>{displayStatus.subtitle}</Text>
|
||||
{phase === 'shipped' && (
|
||||
<View
|
||||
className='mt-2 px-3 py-2 rounded-lg inline-flex items-center'
|
||||
style={{ backgroundColor: 'rgba(255,255,255,0.2)' }}
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/order/logistics?orderId=${order.orderId}` })}
|
||||
>
|
||||
<Text className='text-white text-xs'>查看物流</Text>
|
||||
<Text className='text-white text-xs ml-1'>›</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 收货信息 */}
|
||||
{isExpress ? (
|
||||
<View className='bg-white mx-3 mt-3 p-3 rounded-lg'>
|
||||
<View className='flex items-start gap-2'>
|
||||
<Text className='text-base'>📦</Text>
|
||||
<View className='flex-1'>
|
||||
<View className='flex gap-2 mb-1'>
|
||||
<Text className='text-sm font-medium text-gray-800'>{order.realName || '未知'}</Text>
|
||||
<Text className='text-sm text-gray-600'>{order.mobile || order.phone || '-'}</Text>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-500'>{order.address || '未填写收货地址'}</Text>
|
||||
{(order.sendStartTime || order.sendEndTime) && (
|
||||
<Text className='text-xs text-gray-400 mt-1'>
|
||||
期望配送: {order.sendStartTime?.slice(0, 10)} ~ {order.sendEndTime?.slice(0, 10)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className='bg-white mx-3 mt-3 p-3 rounded-lg'>
|
||||
<View className='flex items-start gap-2'>
|
||||
<Text className='text-base'>🏪</Text>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm font-medium text-gray-800'>{order.selfTakeMerchantName || '自提点'}</Text>
|
||||
<Text className='text-xs text-gray-500 mt-1'>{order.address || '-'}</Text>
|
||||
{order.selfTakeCode && (
|
||||
<View className='mt-2 px-3 py-2 rounded-lg inline-flex items-center' style={{ backgroundColor: '#fff7e6' }}>
|
||||
<Text className='text-xs text-orange-600'>自提码: {order.selfTakeCode}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 商品列表 */}
|
||||
<View className='bg-white mx-3 mt-3 rounded-lg overflow-hidden'>
|
||||
<View className='p-3 border-b border-gray-100'>
|
||||
<Text className='text-sm font-medium text-gray-800'>商品清单 ({goodsCount}件)</Text>
|
||||
</View>
|
||||
{order.orderGoods?.map((item, idx) => (
|
||||
<View
|
||||
key={idx}
|
||||
className='flex gap-3 p-3 border-b border-gray-50'
|
||||
style={idx === (order.orderGoods?.length ?? 0) - 1 ? { borderBottom: 'none' } : undefined}
|
||||
onClick={() => item.goodsId && Taro.navigateTo({ url: `/pages/shop/product-detail?id=${item.goodsId}` })}
|
||||
>
|
||||
<View className='w-16 h-16 rounded-md bg-gray-100 flex-shrink-0 overflow-hidden'>
|
||||
{item.image ? (
|
||||
<Image className='w-full h-full' src={item.image} mode='aspectFill' />
|
||||
) : (
|
||||
<View className='w-full h-full flex items-center justify-center'>
|
||||
<Text className='text-xs text-gray-400'>暂无</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className='flex-1 flex flex-col justify-between h-16'>
|
||||
<Text className='text-sm text-gray-800 line-clamp-1'>{item.goodsName || '未知商品'}</Text>
|
||||
{item.spec ? (
|
||||
<Text className='text-xs text-gray-500 mt-1'>规格: {item.spec}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<View className='flex flex-col items-end justify-between h-16'>
|
||||
<Price price={item.price || '0'} size='small' />
|
||||
<Text className='text-xs text-gray-500'>x{item.totalNum || 1}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 金额明细 */}
|
||||
<View className='bg-white mx-3 mt-3 p-3 rounded-lg'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-3 block'>金额明细</Text>
|
||||
<View className='space-y-2'>
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-sm text-gray-500'>商品总额</Text>
|
||||
<Text className='text-sm text-gray-700'>¥{order.totalPrice || '0.00'}</Text>
|
||||
</View>
|
||||
{order.reducePrice && Number(order.reducePrice) > 0 && (
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-sm text-gray-500'>优惠</Text>
|
||||
<Text className='text-sm text-green-600'>-¥{order.reducePrice}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className='flex justify-between pt-2 border-t border-gray-100'>
|
||||
<Text className='text-sm font-medium text-gray-800'>实付金额</Text>
|
||||
<Price price={order.payPrice || '0'} size='normal' />
|
||||
</View>
|
||||
<View className='flex justify-between pt-2 border-t border-gray-100'>
|
||||
<Text className='text-xs text-gray-400'>支付方式</Text>
|
||||
<Text className='text-xs text-gray-500'>{PAY_TYPE_MAP[order.payType ?? 0] || '未知'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 订单信息 */}
|
||||
<View className='bg-white mx-3 mt-3 p-3 rounded-lg mb-4'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-3 block'>订单信息</Text>
|
||||
<View className='space-y-2'>
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-xs text-gray-400'>订单编号</Text>
|
||||
<View className='flex items-center gap-1'>
|
||||
<Text className='text-xs text-gray-600'>{order.orderNo}</Text>
|
||||
<Text
|
||||
className='text-xs text-blue-500'
|
||||
onClick={() => {
|
||||
Taro.setClipboardData({ data: order.orderNo || '' })
|
||||
Taro.showToast({ title: '已复制', icon: 'none' })
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-xs text-gray-400'>发货状态</Text>
|
||||
<Text className='text-xs text-gray-600'>{DELIVERY_STATUS_MAP[order.deliveryStatus ?? 10]}</Text>
|
||||
</View>
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-xs text-gray-400'>创建时间</Text>
|
||||
<Text className='text-xs text-gray-600'>{formatTime(order.createTime)}</Text>
|
||||
</View>
|
||||
{order.payTime && (
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-xs text-gray-400'>支付时间</Text>
|
||||
<Text className='text-xs text-gray-600'>{formatTime(order.payTime)}</Text>
|
||||
</View>
|
||||
)}
|
||||
{order.deliveryTime && (
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-xs text-gray-400'>发货时间</Text>
|
||||
<Text className='text-xs text-gray-600'>{formatTime(order.deliveryTime)}</Text>
|
||||
</View>
|
||||
)}
|
||||
{order.expirationTime && (
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-xs text-gray-400'>过期时间</Text>
|
||||
<Text className='text-xs text-gray-600'>{formatTime(order.expirationTime)}</Text>
|
||||
</View>
|
||||
)}
|
||||
{order.comments && (
|
||||
<View className='flex justify-start'>
|
||||
<Text className='text-xs text-gray-400 mr-2'>备注</Text>
|
||||
<Text className='text-xs text-gray-600 flex-1'>{order.comments}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-xs text-gray-400'>发票状态</Text>
|
||||
<Text className='text-xs text-gray-600'>{INVOICE_STATUS_MAP[order.isInvoice ?? 0]}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部操作按钮(根据订单阶段动态显示) */}
|
||||
<View
|
||||
className='bg-white border-t border-gray-100 p-3 flex flex-row gap-2'
|
||||
style={{ paddingBottom: '20px' }}
|
||||
>
|
||||
{/* 待付款: 取消 + 支付 */}
|
||||
{phase === 'unpaid' && (
|
||||
<>
|
||||
<Button
|
||||
size='small'
|
||||
className='flex-1'
|
||||
style={{ borderColor: '#ddd', color: '#666' }}
|
||||
onClick={handleCancelOrder}
|
||||
>
|
||||
取消订单
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
className='flex-1'
|
||||
style={{ backgroundColor: '#ee0a24' }}
|
||||
loading={loading}
|
||||
onClick={handlePay}
|
||||
>
|
||||
立即支付
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 待发货: 退款 + 联系客服 */}
|
||||
{phase === 'unshipped' && (
|
||||
<>
|
||||
<Button
|
||||
size='small'
|
||||
className='flex-1'
|
||||
style={{ borderColor: '#ddd', color: '#666' }}
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/order/refund?orderId=${order.orderId}` })}
|
||||
>
|
||||
退款
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
className='flex-1'
|
||||
style={{ backgroundColor: '#4b9cf5' }}
|
||||
onClick={() => Taro.makePhoneCall({ phoneNumber: '400-000-0000' })}
|
||||
>
|
||||
联系客服
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 待收货: 物流 + 确认收货 */}
|
||||
{phase === 'shipped' && (
|
||||
<>
|
||||
<Button
|
||||
size='small'
|
||||
className='flex-1'
|
||||
style={{ borderColor: '#4b9cf5', color: '#4b9cf5' }}
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/order/logistics?orderId=${order.orderId}` })}
|
||||
>
|
||||
查看物流
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
className='flex-1'
|
||||
style={{ backgroundColor: '#0e932e' }}
|
||||
onClick={handleConfirmReceive}
|
||||
>
|
||||
确认收货
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 已完成: 售后 + 删除 */}
|
||||
{phase === 'completed' && (
|
||||
<>
|
||||
<Button
|
||||
size='small'
|
||||
className='flex-1'
|
||||
style={{ borderColor: '#ddd', color: '#666' }}
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/order/after-sale-apply?orderId=${order.orderId}&type=refund` })}
|
||||
>
|
||||
申请售后
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
className='flex-1'
|
||||
style={{ borderColor: '#ee0a24', color: '#ee0a24' }}
|
||||
onClick={handleDeleteOrder}
|
||||
>
|
||||
删除订单
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 已取消/已退款: 删除 */}
|
||||
{phase === 'cancelled' && (
|
||||
<Button
|
||||
size='small'
|
||||
className='flex-1'
|
||||
style={{ borderColor: '#ee0a24', color: '#ee0a24' }}
|
||||
onClick={handleDeleteOrder}
|
||||
>
|
||||
删除订单
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderDetailPage
|
||||
3
src_bak/pages/order/evaluate-detail.config.ts
Normal file
3
src_bak/pages/order/evaluate-detail.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '评价详情',
|
||||
}
|
||||
186
src_bak/pages/order/evaluate-detail.tsx
Normal file
186
src_bak/pages/order/evaluate-detail.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, ScrollView, Swiper, SwiperItem } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '评价详情',
|
||||
})
|
||||
|
||||
const EvaluateDetailPage: React.FC = () => {
|
||||
const [evaluation, setEvaluation] = useState({
|
||||
id: 1,
|
||||
goodsId: 1,
|
||||
goodsName: '高品质保温杯',
|
||||
goodsImage: '',
|
||||
score: 5,
|
||||
content: '质量很好,保温效果非常棒!物流也很快,包装严实,非常满意的一次购物体验。\n\n杯子的外观设计很时尚,颜色也很正,同事们都问我在哪里买的。保温效果真的很好,早上装的开水,到下午还是温热的。\n\n唯一的小缺点就是盖子有点紧,不过用多了应该会好一些。总体来说非常满意,推荐大家购买!',
|
||||
images: ['', '', ''],
|
||||
isAnonymous: false,
|
||||
userName: '张***',
|
||||
createTime: '2026-05-10 14:30:00',
|
||||
likes: 12,
|
||||
isLiked: false,
|
||||
replies: [
|
||||
{
|
||||
id: 1,
|
||||
content: '感谢您的好评!我们会继续努力提供优质产品和服务。',
|
||||
createTime: '2026-05-10 15:00:00',
|
||||
isOfficial: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 渲染星星
|
||||
const renderStars = (score: number) => {
|
||||
const stars = []
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
stars.push(
|
||||
<Text key={i} className={i <= score ? 'text-orange-400' : 'text-gray-300'}>
|
||||
★
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return stars
|
||||
}
|
||||
|
||||
// 处理点赞
|
||||
const handleLike = () => {
|
||||
setEvaluation(prev => ({
|
||||
...prev,
|
||||
isLiked: !prev.isLiked,
|
||||
likes: prev.isLiked ? prev.likes - 1 : prev.likes + 1,
|
||||
}))
|
||||
Taro.showToast({ title: evaluation.isLiked ? '已取消点赞' : '点赞成功', icon: 'success' })
|
||||
}
|
||||
|
||||
// 图片预览
|
||||
const handleImagePreview = (index: number) => {
|
||||
// 这里应该调用 Taro.previewImage
|
||||
Taro.showToast({ title: `查看第${index + 1}张图片`, icon: 'none' })
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='bg-gray-50 flex flex-col' style={{ minHeight: '100vh' }}>
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
{/* 用户信息 */}
|
||||
<View className='bg-white p-4 mb-3'>
|
||||
<View className='flex items-center gap-2 mb-3'>
|
||||
<View className='w-10 h-10 rounded-full flex items-center justify-center' style={{ background: 'linear-gradient(to right, #60a5fa, #c084fc)' }}>
|
||||
<Text className='text-white font-bold'>{evaluation.isAnonymous ? '匿' : evaluation.userName[0]}</Text>
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm text-gray-700 font-medium block'>{evaluation.userName}</Text>
|
||||
<View className='flex items-center gap-1'>
|
||||
{renderStars(evaluation.score)}
|
||||
</View>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-400'>{evaluation.createTime.split(' ')[0]}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 商品信息 */}
|
||||
<View
|
||||
className='bg-white p-4 mb-3'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/shop/product-detail?id=${evaluation.goodsId}` })}
|
||||
>
|
||||
<View className='flex items-center gap-2'>
|
||||
<View className='w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0'>
|
||||
<Text className='text-2xl'>🛍️</Text>
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm text-gray-700 block'>{evaluation.goodsName}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>查看商品详情</Text>
|
||||
</View>
|
||||
<Text className='text-gray-400'>→</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 评价内容 */}
|
||||
<View className='bg-white p-4 mb-3'>
|
||||
<Text className='text-sm text-gray-700 leading-7 block whitespace-pre-wrap'>
|
||||
{evaluation.content}
|
||||
</Text>
|
||||
|
||||
{/* 评价图片 */}
|
||||
{evaluation.images && evaluation.images.length > 0 && (
|
||||
<View className='mt-3'>
|
||||
<Swiper
|
||||
className='h-48 rounded-lg'
|
||||
indicatorColor='#999'
|
||||
indicatorActiveColor='#333'
|
||||
circular
|
||||
autoplay={false}
|
||||
>
|
||||
{evaluation.images.map((_, index) => (
|
||||
<SwiperItem key={index}>
|
||||
<View className='w-full h-48 bg-gray-100 flex items-center justify-center'>
|
||||
<Text className='text-4xl'>🖼️</Text>
|
||||
<Text className='text-xs text-gray-400 mt-2'>评价图片 {index + 1}</Text>
|
||||
</View>
|
||||
</SwiperItem>
|
||||
))}
|
||||
</Swiper>
|
||||
<View className='flex justify-center gap-1 mt-2'>
|
||||
{evaluation.images.map((_, index) => (
|
||||
<View key={index} className='w-1 h-1 rounded-full bg-gray-300' />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 商家回复 */}
|
||||
{evaluation.replies && evaluation.replies.length > 0 && (
|
||||
<View className='bg-white p-4 mb-3'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-2 block'>商家回复</Text>
|
||||
{evaluation.replies.map(reply => (
|
||||
<View key={reply.id} className='bg-orange-50 rounded-lg p-3'>
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
{reply.isOfficial && (
|
||||
<View className='bg-orange-500 px-2 py-1 rounded'>
|
||||
<Text className='text-xs text-white'>官方</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text className='text-xs text-gray-400'>{reply.createTime}</Text>
|
||||
</View>
|
||||
<Text className='text-sm text-gray-700 leading-6'>{reply.content}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部操作栏 */}
|
||||
<View className='bg-white border-t border-gray-100 px-4 py-2 flex items-center justify-around' style={{ paddingBottom: '20px' }}>
|
||||
<View className='flex flex-col items-center' onClick={handleLike}>
|
||||
<Text className={`text-2xl ${evaluation.isLiked ? 'text-red-500' : 'text-gray-400'}`}>
|
||||
{evaluation.isLiked ? '❤️' : '🤍'}
|
||||
</Text>
|
||||
<Text className={`text-xs ${evaluation.isLiked ? 'text-red-500' : 'text-gray-400'}`}>
|
||||
{evaluation.likes}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className='flex flex-col items-center' onClick={() => Taro.showToast({ title: '收藏成功', icon: 'success' })}>
|
||||
<Text className='text-2xl'>⭐</Text>
|
||||
<Text className='text-xs text-gray-400'>收藏</Text>
|
||||
</View>
|
||||
|
||||
<View className='flex flex-col items-center' onClick={() => Taro.showToast({ title: '举报已提交', icon: 'success' })}>
|
||||
<Text className='text-2xl'>⚠️</Text>
|
||||
<Text className='text-xs text-gray-400'>举报</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className='bg-orange-500 text-white px-6 py-2 rounded-full'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/shop/product-detail?id=${evaluation.goodsId}` })}
|
||||
>
|
||||
<Text className='text-sm text-white'>查看商品</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default EvaluateDetailPage
|
||||
3
src_bak/pages/order/evaluate-detail/index.config.ts
Normal file
3
src_bak/pages/order/evaluate-detail/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '评价详情',
|
||||
}
|
||||
189
src_bak/pages/order/evaluate-detail/index.tsx
Normal file
189
src_bak/pages/order/evaluate-detail/index.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { getShopEvaluation, likeShopEvaluation } from '@/api/shop/shopEvaluation'
|
||||
import type { ShopEvaluation } from '@/api/shop/shopEvaluation/model'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '评价详情',
|
||||
})
|
||||
|
||||
const EvaluateDetailPage: React.FC = () => {
|
||||
const { id } = Taro.getCurrentInstance().router?.params || {}
|
||||
const [evaluation, setEvaluation] = useState<ShopEvaluation | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchDetail(Number(id))
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const fetchDetail = async (evalId: number) => {
|
||||
try {
|
||||
const data = await getShopEvaluation(evalId)
|
||||
setEvaluation(data)
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染星星
|
||||
const renderStars = (score: number) => {
|
||||
const stars = []
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
stars.push(
|
||||
<Text key={i} className={i <= score ? 'text-orange-400' : 'text-gray-300'}>★</Text>
|
||||
)
|
||||
}
|
||||
return stars
|
||||
}
|
||||
|
||||
// 处理点赞
|
||||
const handleLike = async () => {
|
||||
if (!evaluation) return
|
||||
try {
|
||||
await likeShopEvaluation({ evaluationId: evaluation.id, isLiked: !evaluation.isLiked })
|
||||
setEvaluation(prev => prev ? {
|
||||
...prev,
|
||||
isLiked: !prev.isLiked,
|
||||
likes: prev.isLiked ? prev.likes - 1 : prev.likes + 1,
|
||||
} : prev)
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 图片预览
|
||||
const handleImagePreview = (index: number) => {
|
||||
if (!evaluation?.images?.length) return
|
||||
Taro.previewImage({
|
||||
current: evaluation.images[index],
|
||||
urls: evaluation.images,
|
||||
})
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
|
||||
<Text className='text-sm text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!evaluation) {
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
|
||||
<Text className='text-sm text-gray-400'>评价不存在</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='bg-gray-50 flex flex-col' style={{ minHeight: '100vh' }}>
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
{/* 用户信息 */}
|
||||
<View className='bg-white p-4 mb-3'>
|
||||
<View className='flex items-center gap-2 mb-3'>
|
||||
<View className='w-10 h-10 rounded-full flex items-center justify-center overflow-hidden' style={{ background: 'linear-gradient(to right, #60a5fa, #c084fc)' }}>
|
||||
{evaluation.userAvatar ? (
|
||||
<Image src={evaluation.userAvatar} className='w-10 h-10' mode='aspectFill' />
|
||||
) : (
|
||||
<Text className='text-white font-bold'>{evaluation.isAnonymous ? '匿' : evaluation.userName[0]}</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm text-gray-700 font-medium block'>{evaluation.userName}</Text>
|
||||
<View className='flex items-center gap-1'>{renderStars(evaluation.score)}</View>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-400'>{evaluation.createTime.split(' ')[0]}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 商品信息 */}
|
||||
<View
|
||||
className='bg-white p-4 mb-3'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/shop/product-detail?id=${evaluation.goodsId}` })}
|
||||
>
|
||||
<View className='flex items-center gap-2'>
|
||||
<View className='w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden'>
|
||||
{evaluation.goodsImage ? (
|
||||
<Image src={evaluation.goodsImage} className='w-12 h-12' mode='aspectFill' />
|
||||
) : (
|
||||
<Text className='text-2xl'>🛍️</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm text-gray-700 block'>{evaluation.goodsName}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>查看商品详情</Text>
|
||||
</View>
|
||||
<Text className='text-gray-400'>→</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 评价内容 */}
|
||||
<View className='bg-white p-4 mb-3'>
|
||||
<Text className='text-sm text-gray-700 leading-7 block whitespace-pre-wrap'>
|
||||
{evaluation.content}
|
||||
</Text>
|
||||
|
||||
{/* 评价图片 */}
|
||||
{evaluation.images && evaluation.images.length > 0 && (
|
||||
<View className='flex flex-wrap gap-2 mt-3'>
|
||||
{evaluation.images.map((img, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className='w-24 h-24 bg-gray-100 rounded-lg overflow-hidden'
|
||||
onClick={() => handleImagePreview(index)}
|
||||
>
|
||||
<Image src={img} className='w-24 h-24' mode='aspectFill' />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 商家回复 */}
|
||||
{evaluation.reply && (
|
||||
<View className='bg-white p-4 mb-3'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-2 block'>商家回复</Text>
|
||||
<View className='bg-orange-50 rounded-lg p-3'>
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<View className='bg-orange-500 px-2 py-1 rounded'>
|
||||
<Text className='text-xs text-white'>官方</Text>
|
||||
</View>
|
||||
{evaluation.replyTime && (
|
||||
<Text className='text-xs text-gray-400'>{evaluation.replyTime}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className='text-sm text-gray-700 leading-6'>{evaluation.reply}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部操作栏 */}
|
||||
<View className='bg-white border-t border-gray-100 px-4 py-2 flex items-center justify-around' style={{ paddingBottom: '20px' }}>
|
||||
<View className='flex flex-col items-center' onClick={handleLike}>
|
||||
<Text className={`text-2xl ${evaluation.isLiked ? 'text-red-500' : 'text-gray-400'}`}>
|
||||
{evaluation.isLiked ? '❤️' : '🤍'}
|
||||
</Text>
|
||||
<Text className={`text-xs ${evaluation.isLiked ? 'text-red-500' : 'text-gray-400'}`}>
|
||||
{evaluation.likes}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className='bg-orange-500 text-white px-6 py-2 rounded-full'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/shop/product-detail?id=${evaluation.goodsId}` })}
|
||||
>
|
||||
<Text className='text-sm text-white'>查看商品</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default EvaluateDetailPage
|
||||
3
src_bak/pages/order/evaluate-list.config.ts
Normal file
3
src_bak/pages/order/evaluate-list.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '商品评价',
|
||||
}
|
||||
238
src_bak/pages/order/evaluate-list.tsx
Normal file
238
src_bak/pages/order/evaluate-list.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { listShopEvaluation, likeShopEvaluation } from '@/api/shop/shopEvaluation'
|
||||
import type { ShopEvaluation } from '@/api/shop/shopEvaluation'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '商品评价',
|
||||
})
|
||||
|
||||
const EvaluateListPage: React.FC = () => {
|
||||
const [evaluations, setEvaluations] = useState<ShopEvaluation[]>([])
|
||||
const [sortBy, setSortBy] = useState('newest') // newest, mostLiked
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
setEvaluations([])
|
||||
setPage(1)
|
||||
setFinished(false)
|
||||
loadList(1)
|
||||
}, [sortBy])
|
||||
|
||||
const loadList = async (p: number) => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const res = await listShopEvaluation({
|
||||
page: p,
|
||||
limit: 10,
|
||||
sortBy: sortBy as 'newest' | 'mostLiked',
|
||||
})
|
||||
|
||||
if (res?.list) {
|
||||
if (p === 1) {
|
||||
setEvaluations(res.list)
|
||||
} else {
|
||||
setEvaluations(prev => [...prev, ...res.list])
|
||||
}
|
||||
setFinished(res.list.length < 10)
|
||||
setPage(p)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载评价列表失败', err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 点赞/取消点赞
|
||||
const handleLike = async (id: number) => {
|
||||
const evaluation = evaluations.find(ev => ev.id === id)
|
||||
if (!evaluation) return
|
||||
|
||||
try {
|
||||
await likeShopEvaluation({
|
||||
evaluationId: id,
|
||||
isLiked: !evaluation.isLiked,
|
||||
})
|
||||
|
||||
setEvaluations(prev =>
|
||||
prev.map(ev =>
|
||||
ev.id === id
|
||||
? {
|
||||
...ev,
|
||||
isLiked: !ev.isLiked,
|
||||
likes: ev.isLiked ? ev.likes - 1 : ev.likes + 1,
|
||||
}
|
||||
: ev
|
||||
)
|
||||
)
|
||||
Taro.showToast({ title: '操作成功', icon: 'success' })
|
||||
} catch (err) {
|
||||
console.error('点赞操作失败', err)
|
||||
Taro.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 查看评价详情
|
||||
const handleDetail = (id: number) => {
|
||||
Taro.navigateTo({ url: `/pages/order/evaluate-detail?id=${id}` })
|
||||
}
|
||||
|
||||
// 渲染星星
|
||||
const renderStars = (score: number) => {
|
||||
const stars = []
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
stars.push(
|
||||
<Text key={i} className={i <= score ? 'text-orange-400' : 'text-gray-300'}>
|
||||
★
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return stars
|
||||
}
|
||||
|
||||
// 排序
|
||||
const sortedEvaluations = [...evaluations].sort((a, b) => {
|
||||
if (sortBy === 'newest') {
|
||||
return new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||
} else if (sortBy === 'mostLiked') {
|
||||
return b.likes - a.likes
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
// 加载更多
|
||||
const handleLoadMore = () => {
|
||||
if (!finished && !loading) {
|
||||
loadList(page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
{/* 排序栏 */}
|
||||
<View className='bg-white flex justify-between items-center px-3 py-2'>
|
||||
<Text className='text-sm text-gray-500'>
|
||||
共 {evaluations.length} 条评价
|
||||
</Text>
|
||||
<View className='flex gap-3'>
|
||||
<View
|
||||
className={`px-3 py-1 rounded-full text-xs ${
|
||||
sortBy === 'newest' ? 'bg-orange-500 text-white' : 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
onClick={() => setSortBy('newest')}
|
||||
>
|
||||
<Text className={sortBy === 'newest' ? 'text-white' : 'text-gray-600'}>最新</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`px-3 py-1 rounded-full text-xs ${
|
||||
sortBy === 'mostLiked' ? 'bg-orange-500 text-white' : 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
onClick={() => setSortBy('mostLiked')}
|
||||
>
|
||||
<Text className={sortBy === 'mostLiked' ? 'text-white' : 'text-gray-600'}>最热</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 评价列表 */}
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='flex-1'
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={100}
|
||||
>
|
||||
<View className='p-3'>
|
||||
{evaluations.length === 0 && !loading ? (
|
||||
<EmptyState text='暂无评价' />
|
||||
) : (
|
||||
sortedEvaluations.map(ev => (
|
||||
<View key={ev.id} className='bg-white rounded-lg p-4 mb-3 shadow-sm'>
|
||||
{/* 用户信息 */}
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<View className='w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center'>
|
||||
<Text className='text-sm text-gray-500'>{ev.isAnonymous ? '匿' : ev.userName[0]}</Text>
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm text-gray-700'>{ev.userName}</Text>
|
||||
<View className='flex items-center gap-1'>
|
||||
{renderStars(ev.score)}
|
||||
</View>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
{ev.createTime.split(' ')[0]}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 商品信息 */}
|
||||
<View
|
||||
className='flex items-center gap-2 bg-gray-50 rounded-lg p-2 mb-2'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/shop/product-detail?id=${ev.goodsId}` })}
|
||||
>
|
||||
<View className='w-10 h-10 bg-gray-200 rounded flex items-center justify-center flex-shrink-0'>
|
||||
<Text className='text-lg'>🛍️</Text>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-600 flex-1'>{ev.goodsName}</Text>
|
||||
<Text className='text-gray-400 text-xs'>→</Text>
|
||||
</View>
|
||||
|
||||
{/* 评价内容 */}
|
||||
<Text className='text-sm text-gray-700 mb-2 block leading-6'>
|
||||
{ev.content}
|
||||
</Text>
|
||||
|
||||
{/* 评价图片 */}
|
||||
{ev.images && ev.images.length > 0 && (
|
||||
<View className='flex gap-2 mb-2'>
|
||||
{ev.images.map((img, idx) => (
|
||||
<View key={idx} className='w-16 h-16 bg-gray-100 rounded'>
|
||||
<Text className='text-xs text-gray-400'>图片{idx + 1}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 操作栏 */}
|
||||
<View className='flex items-center justify-between pt-2 border-t border-gray-50'>
|
||||
<View
|
||||
className={`flex items-center gap-1 ${
|
||||
ev.isLiked ? 'opacity-100' : 'opacity-50'
|
||||
}`}
|
||||
onClick={() => handleLike(ev.id)}
|
||||
>
|
||||
<Text className={ev.isLiked ? 'text-red-500' : 'text-gray-400'}>
|
||||
{ev.isLiked ? '❤️' : '🤍'}
|
||||
</Text>
|
||||
<Text className={`text-xs ${ev.isLiked ? 'text-red-500' : 'text-gray-400'}`}>
|
||||
{ev.likes}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className='flex items-center gap-1'
|
||||
onClick={() => handleDetail(ev.id)}
|
||||
>
|
||||
<Text className='text-xs text-gray-400'>查看详情</Text>
|
||||
<Text className='text-gray-400 text-xs'>→</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
|
||||
<LoadMore loading={loading} finished={finished} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default EvaluateListPage
|
||||
3
src_bak/pages/order/evaluate-list/index.config.ts
Normal file
3
src_bak/pages/order/evaluate-list/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '商品评价',
|
||||
}
|
||||
238
src_bak/pages/order/evaluate-list/index.tsx
Normal file
238
src_bak/pages/order/evaluate-list/index.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { listShopEvaluation, likeShopEvaluation } from '@/api/shop/shopEvaluation'
|
||||
import type { ShopEvaluation } from '@/api/shop/shopEvaluation'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '商品评价',
|
||||
})
|
||||
|
||||
const EvaluateListPage: React.FC = () => {
|
||||
const [evaluations, setEvaluations] = useState<ShopEvaluation[]>([])
|
||||
const [sortBy, setSortBy] = useState('newest') // newest, mostLiked
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
setEvaluations([])
|
||||
setPage(1)
|
||||
setFinished(false)
|
||||
loadList(1)
|
||||
}, [sortBy])
|
||||
|
||||
const loadList = async (p: number) => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const res = await listShopEvaluation({
|
||||
page: p,
|
||||
limit: 10,
|
||||
sortBy: sortBy as 'newest' | 'mostLiked',
|
||||
})
|
||||
|
||||
if (res?.list) {
|
||||
if (p === 1) {
|
||||
setEvaluations(res.list)
|
||||
} else {
|
||||
setEvaluations(prev => [...prev, ...res.list])
|
||||
}
|
||||
setFinished(res.list.length < 10)
|
||||
setPage(p)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载评价列表失败', err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 点赞/取消点赞
|
||||
const handleLike = async (id: number) => {
|
||||
const evaluation = evaluations.find(ev => ev.id === id)
|
||||
if (!evaluation) return
|
||||
|
||||
try {
|
||||
await likeShopEvaluation({
|
||||
evaluationId: id,
|
||||
isLiked: !evaluation.isLiked,
|
||||
})
|
||||
|
||||
setEvaluations(prev =>
|
||||
prev.map(ev =>
|
||||
ev.id === id
|
||||
? {
|
||||
...ev,
|
||||
isLiked: !ev.isLiked,
|
||||
likes: ev.isLiked ? ev.likes - 1 : ev.likes + 1,
|
||||
}
|
||||
: ev
|
||||
)
|
||||
)
|
||||
Taro.showToast({ title: '操作成功', icon: 'success' })
|
||||
} catch (err) {
|
||||
console.error('点赞操作失败', err)
|
||||
Taro.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 查看评价详情
|
||||
const handleDetail = (id: number) => {
|
||||
Taro.navigateTo({ url: `/pages/order/evaluate-detail/index?id=${id}` })
|
||||
}
|
||||
|
||||
// 渲染星星
|
||||
const renderStars = (score: number) => {
|
||||
const stars = []
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
stars.push(
|
||||
<Text key={i} className={i <= score ? 'text-orange-400' : 'text-gray-300'}>
|
||||
★
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return stars
|
||||
}
|
||||
|
||||
// 排序
|
||||
const sortedEvaluations = [...evaluations].sort((a, b) => {
|
||||
if (sortBy === 'newest') {
|
||||
return new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||
} else if (sortBy === 'mostLiked') {
|
||||
return b.likes - a.likes
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
// 加载更多
|
||||
const handleLoadMore = () => {
|
||||
if (!finished && !loading) {
|
||||
loadList(page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
{/* 排序栏 */}
|
||||
<View className='bg-white flex justify-between items-center px-3 py-2'>
|
||||
<Text className='text-sm text-gray-500'>
|
||||
共 {evaluations.length} 条评价
|
||||
</Text>
|
||||
<View className='flex gap-3'>
|
||||
<View
|
||||
className={`px-3 py-1 rounded-full text-xs ${
|
||||
sortBy === 'newest' ? 'bg-orange-500 text-white' : 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
onClick={() => setSortBy('newest')}
|
||||
>
|
||||
<Text className={sortBy === 'newest' ? 'text-white' : 'text-gray-600'}>最新</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`px-3 py-1 rounded-full text-xs ${
|
||||
sortBy === 'mostLiked' ? 'bg-orange-500 text-white' : 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
onClick={() => setSortBy('mostLiked')}
|
||||
>
|
||||
<Text className={sortBy === 'mostLiked' ? 'text-white' : 'text-gray-600'}>最热</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 评价列表 */}
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='flex-1'
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={100}
|
||||
>
|
||||
<View className='p-3'>
|
||||
{evaluations.length === 0 && !loading ? (
|
||||
<EmptyState text='暂无评价' />
|
||||
) : (
|
||||
sortedEvaluations.map(ev => (
|
||||
<View key={ev.id} className='bg-white rounded-lg p-4 mb-3 shadow-sm'>
|
||||
{/* 用户信息 */}
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<View className='w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center'>
|
||||
<Text className='text-sm text-gray-500'>{ev.isAnonymous ? '匿' : ev.userName[0]}</Text>
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm text-gray-700'>{ev.userName}</Text>
|
||||
<View className='flex items-center gap-1'>
|
||||
{renderStars(ev.score)}
|
||||
</View>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
{ev.createTime.split(' ')[0]}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 商品信息 */}
|
||||
<View
|
||||
className='flex items-center gap-2 bg-gray-50 rounded-lg p-2 mb-2'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/shop/product-detail?id=${ev.goodsId}` })}
|
||||
>
|
||||
<View className='w-10 h-10 bg-gray-200 rounded flex items-center justify-center flex-shrink-0'>
|
||||
<Text className='text-lg'>🛍️</Text>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-600 flex-1'>{ev.goodsName}</Text>
|
||||
<Text className='text-gray-400 text-xs'>→</Text>
|
||||
</View>
|
||||
|
||||
{/* 评价内容 */}
|
||||
<Text className='text-sm text-gray-700 mb-2 block leading-6'>
|
||||
{ev.content}
|
||||
</Text>
|
||||
|
||||
{/* 评价图片 */}
|
||||
{ev.images && ev.images.length > 0 && (
|
||||
<View className='flex gap-2 mb-2'>
|
||||
{ev.images.map((img, idx) => (
|
||||
<View key={idx} className='w-16 h-16 bg-gray-100 rounded'>
|
||||
<Text className='text-xs text-gray-400'>图片{idx + 1}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 操作栏 */}
|
||||
<View className='flex items-center justify-between pt-2 border-t border-gray-50'>
|
||||
<View
|
||||
className={`flex items-center gap-1 ${
|
||||
ev.isLiked ? 'opacity-100' : 'opacity-50'
|
||||
}`}
|
||||
onClick={() => handleLike(ev.id)}
|
||||
>
|
||||
<Text className={ev.isLiked ? 'text-red-500' : 'text-gray-400'}>
|
||||
{ev.isLiked ? '❤️' : '🤍'}
|
||||
</Text>
|
||||
<Text className={`text-xs ${ev.isLiked ? 'text-red-500' : 'text-gray-400'}`}>
|
||||
{ev.likes}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className='flex items-center gap-1'
|
||||
onClick={() => handleDetail(ev.id)}
|
||||
>
|
||||
<Text className='text-xs text-gray-400'>查看详情</Text>
|
||||
<Text className='text-gray-400 text-xs'>→</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
|
||||
<LoadMore loading={loading} finished={finished} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default EvaluateListPage
|
||||
3
src_bak/pages/order/evaluate.config.ts
Normal file
3
src_bak/pages/order/evaluate.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '商品评价',
|
||||
}
|
||||
185
src_bak/pages/order/evaluate.tsx
Normal file
185
src_bak/pages/order/evaluate.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Image, Input } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button } from '@nutui/nutui-react-taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { submitGoodsComment } from '@/api/shop/shopGoodsComment'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '商品评价',
|
||||
})
|
||||
|
||||
const EvaluatePage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const { orderId, orderGoodsId, goodsId } = Taro.getCurrentInstance().router?.params || {}
|
||||
|
||||
const [rating, setRating] = useState(5)
|
||||
const [content, setContent] = useState('')
|
||||
const [images, setImages] = useState<string[]>([])
|
||||
const [isAnonymous, setIsAnonymous] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
}
|
||||
}, [isLoggedIn])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!content.trim()) {
|
||||
Taro.showToast({ title: '请输入评价内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!orderId || !orderGoodsId || !goodsId) {
|
||||
Taro.showToast({ title: '参数错误', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
Taro.showModal({
|
||||
title: '提交评价',
|
||||
content: '确定要提交评价吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const submitRes = await submitGoodsComment({
|
||||
oid: Number(orderId),
|
||||
goodsId: Number(goodsId),
|
||||
goodsScore: rating,
|
||||
serviceScore: rating,
|
||||
comment: content,
|
||||
pics: images.join(','),
|
||||
})
|
||||
|
||||
if (submitRes.code === 0) {
|
||||
Taro.showToast({ title: '评价成功', icon: 'success' })
|
||||
setTimeout(() => Taro.navigateBack(), 1500)
|
||||
} else {
|
||||
Taro.showToast({ title: submitRes.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleChooseImage = () => {
|
||||
Taro.chooseImage({
|
||||
count: 9 - images.length,
|
||||
success: (res) => {
|
||||
setImages([...images, ...res.tempFilePaths])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemoveImage = (index: number) => {
|
||||
const newImages = [...images]
|
||||
newImages.splice(index, 1)
|
||||
setImages(newImages)
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return null
|
||||
}
|
||||
|
||||
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-lg'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-3 block'>商品评分</Text>
|
||||
<View className='flex gap-2'>
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<View
|
||||
key={star}
|
||||
onClick={() => setRating(star)}
|
||||
>
|
||||
<Text className='text-2xl'>
|
||||
{star <= rating ? '⭐' : '☆'}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 评价内容 */}
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-lg'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-2 block'>评价内容</Text>
|
||||
<View className='bg-gray-50 rounded-lg p-3'>
|
||||
<Input
|
||||
value={content}
|
||||
onInput={(e) => setContent(e.detail.value)}
|
||||
placeholder='请输入评价内容'
|
||||
className='w-full text-sm'
|
||||
style={{ minHeight: '120px' }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 上传图片 */}
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-lg'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-2 block'>上传图片</Text>
|
||||
<View className='flex flex-wrap gap-2'>
|
||||
{images.map((img, idx) => (
|
||||
<View key={idx} className='relative'>
|
||||
<Image
|
||||
src={img}
|
||||
className='w-20 h-20 rounded-lg'
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View
|
||||
className='absolute top-0 right-0 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center'
|
||||
onClick={() => handleRemoveImage(idx)}
|
||||
>
|
||||
<Text className='text-white text-xs'>×</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
{images.length < 9 && (
|
||||
<View
|
||||
className='w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center'
|
||||
onClick={handleChooseImage}
|
||||
>
|
||||
<Text className='text-2xl text-gray-400'>+</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 匿名评价 */}
|
||||
<View
|
||||
className='bg-white mx-3 mt-3 p-4 rounded-lg flex items-center justify-between'
|
||||
onClick={() => setIsAnonymous(!isAnonymous)}
|
||||
>
|
||||
<Text className='text-sm text-gray-700'>匿名评价</Text>
|
||||
<View
|
||||
className={`w-12 h-6 rounded-full relative ${
|
||||
isAnonymous ? 'bg-green-500' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<View
|
||||
className={`absolute top-1 w-4 h-4 bg-white rounded-full ${
|
||||
isAnonymous ? 'right-1' : 'left-1'
|
||||
}`}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<View className='bg-white border-t border-gray-100 p-3' style={{ paddingBottom: '20px' }}>
|
||||
<Button
|
||||
type='primary'
|
||||
className='w-full rounded-full'
|
||||
style={{ backgroundColor: '#0e932e' }}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
提交评价
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default EvaluatePage
|
||||
3
src_bak/pages/order/evaluate/index.config.ts
Normal file
3
src_bak/pages/order/evaluate/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '商品评价',
|
||||
}
|
||||
185
src_bak/pages/order/evaluate/index.tsx
Normal file
185
src_bak/pages/order/evaluate/index.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Image, Input } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button } from '@nutui/nutui-react-taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { submitGoodsComment } from '@/api/shop/shopGoodsComment'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '商品评价',
|
||||
})
|
||||
|
||||
const EvaluatePage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const { orderId, orderGoodsId, goodsId } = Taro.getCurrentInstance().router?.params || {}
|
||||
|
||||
const [rating, setRating] = useState(5)
|
||||
const [content, setContent] = useState('')
|
||||
const [images, setImages] = useState<string[]>([])
|
||||
const [isAnonymous, setIsAnonymous] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
}
|
||||
}, [isLoggedIn])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!content.trim()) {
|
||||
Taro.showToast({ title: '请输入评价内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!orderId || !orderGoodsId || !goodsId) {
|
||||
Taro.showToast({ title: '参数错误', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
Taro.showModal({
|
||||
title: '提交评价',
|
||||
content: '确定要提交评价吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const submitRes = await submitGoodsComment({
|
||||
oid: Number(orderId),
|
||||
goodsId: Number(goodsId),
|
||||
goodsScore: rating,
|
||||
serviceScore: rating,
|
||||
comment: content,
|
||||
pics: images.join(','),
|
||||
})
|
||||
|
||||
if (submitRes.code === 0) {
|
||||
Taro.showToast({ title: '评价成功', icon: 'success' })
|
||||
setTimeout(() => Taro.navigateBack(), 1500)
|
||||
} else {
|
||||
Taro.showToast({ title: submitRes.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleChooseImage = () => {
|
||||
Taro.chooseImage({
|
||||
count: 9 - images.length,
|
||||
success: (res) => {
|
||||
setImages([...images, ...res.tempFilePaths])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemoveImage = (index: number) => {
|
||||
const newImages = [...images]
|
||||
newImages.splice(index, 1)
|
||||
setImages(newImages)
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return null
|
||||
}
|
||||
|
||||
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-lg'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-3 block'>商品评分</Text>
|
||||
<View className='flex gap-2'>
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<View
|
||||
key={star}
|
||||
onClick={() => setRating(star)}
|
||||
>
|
||||
<Text className='text-2xl'>
|
||||
{star <= rating ? '⭐' : '☆'}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 评价内容 */}
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-lg'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-2 block'>评价内容</Text>
|
||||
<View className='bg-gray-50 rounded-lg p-3'>
|
||||
<Input
|
||||
value={content}
|
||||
onInput={(e) => setContent(e.detail.value)}
|
||||
placeholder='请输入评价内容'
|
||||
className='w-full text-sm'
|
||||
style={{ minHeight: '120px' }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 上传图片 */}
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-lg'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-2 block'>上传图片</Text>
|
||||
<View className='flex flex-wrap gap-2'>
|
||||
{images.map((img, idx) => (
|
||||
<View key={idx} className='relative'>
|
||||
<Image
|
||||
src={img}
|
||||
className='w-20 h-20 rounded-lg'
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View
|
||||
className='absolute top-0 right-0 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center'
|
||||
onClick={() => handleRemoveImage(idx)}
|
||||
>
|
||||
<Text className='text-white text-xs'>×</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
{images.length < 9 && (
|
||||
<View
|
||||
className='w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center'
|
||||
onClick={handleChooseImage}
|
||||
>
|
||||
<Text className='text-2xl text-gray-400'>+</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 匿名评价 */}
|
||||
<View
|
||||
className='bg-white mx-3 mt-3 p-4 rounded-lg flex items-center justify-between'
|
||||
onClick={() => setIsAnonymous(!isAnonymous)}
|
||||
>
|
||||
<Text className='text-sm text-gray-700'>匿名评价</Text>
|
||||
<View
|
||||
className={`w-12 h-6 rounded-full relative ${
|
||||
isAnonymous ? 'bg-green-500' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<View
|
||||
className={`absolute top-1 w-4 h-4 bg-white rounded-full ${
|
||||
isAnonymous ? 'right-1' : 'left-1'
|
||||
}`}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<View className='bg-white border-t border-gray-100 p-3' style={{ paddingBottom: '20px' }}>
|
||||
<Button
|
||||
type='primary'
|
||||
className='w-full rounded-full'
|
||||
style={{ backgroundColor: '#0e932e' }}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
提交评价
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default EvaluatePage
|
||||
187
src_bak/pages/order/list.tsx
Normal file
187
src_bak/pages/order/list.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { pageShopOrder } from '@/api/shop/shopOrder'
|
||||
import type { ShopOrder, ShopOrderParam } from '@/api/shop/shopOrder/model'
|
||||
import { OrderListStatus, OrderListTabText } from '@/types/order'
|
||||
import OrderCard from '@/components/common/OrderCard'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '我的订单',
|
||||
})
|
||||
|
||||
// Tab 配置(使用后端 statusFilter 字段)
|
||||
const tabList = [
|
||||
{ title: OrderListTabText[OrderListStatus.All], status: OrderListStatus.All },
|
||||
{ title: OrderListTabText[OrderListStatus.Unpaid], status: OrderListStatus.Unpaid },
|
||||
{ title: OrderListTabText[OrderListStatus.Unshipped], status: OrderListStatus.Unshipped },
|
||||
{ title: OrderListTabText[OrderListStatus.Shipped], status: OrderListStatus.Shipped },
|
||||
{ title: OrderListTabText[OrderListStatus.Completed], status: OrderListStatus.Completed },
|
||||
]
|
||||
|
||||
interface TabState {
|
||||
orders: ShopOrder[]
|
||||
page: number
|
||||
finished: boolean
|
||||
loading: boolean
|
||||
initialized: boolean
|
||||
}
|
||||
|
||||
const OrderListPage: React.FC = () => {
|
||||
const { tab: initTab } = Taro.getCurrentInstance().router?.params || {}
|
||||
const [tabIndex, setTabIndex] = useState(Number(initTab) || 0)
|
||||
|
||||
// 每个 tab 独立维护状态
|
||||
const [tabStates, setTabStates] = useState<TabState[]>(
|
||||
tabList.map(() => ({
|
||||
orders: [],
|
||||
page: 1,
|
||||
finished: false,
|
||||
loading: false,
|
||||
initialized: false,
|
||||
}))
|
||||
)
|
||||
|
||||
const currentState = tabStates[tabIndex]
|
||||
|
||||
// 用 ref 避免 loadOrders 闭包中捕获过期状态
|
||||
const tabStatesRef = useRef(tabStates)
|
||||
tabStatesRef.current = tabStates
|
||||
|
||||
const updateTabState = useCallback(
|
||||
(index: number, partial: Partial<TabState>) => {
|
||||
setTabStates(prev => {
|
||||
const next = [...prev]
|
||||
next[index] = { ...next[index], ...partial }
|
||||
return next
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const loadOrders = useCallback(
|
||||
async (targetIndex: number, p: number) => {
|
||||
const state = tabStatesRef.current[targetIndex]
|
||||
if (state.loading) return
|
||||
|
||||
updateTabState(targetIndex, { loading: true })
|
||||
|
||||
try {
|
||||
const params: ShopOrderParam = { page: p, limit: 10 }
|
||||
const status = tabList[targetIndex]?.status
|
||||
|
||||
// 使用后端 statusFilter 字段进行筛选
|
||||
// -1=全部, 0=待支付, 1=待发货, 3=待收货, 5=已完成
|
||||
if (status !== undefined && status !== OrderListStatus.All) {
|
||||
params.statusFilter = status
|
||||
}
|
||||
|
||||
const res = await pageShopOrder(params)
|
||||
const list = res?.list || []
|
||||
|
||||
updateTabState(targetIndex, {
|
||||
orders: p === 1 ? list : [...state.orders, ...list],
|
||||
page: p,
|
||||
finished: list.length < 10,
|
||||
loading: false,
|
||||
initialized: true,
|
||||
})
|
||||
} catch {
|
||||
updateTabState(targetIndex, { loading: false, initialized: true })
|
||||
}
|
||||
},
|
||||
[updateTabState]
|
||||
)
|
||||
|
||||
// 切换 tab 时加载数据
|
||||
useEffect(() => {
|
||||
if (!tabStates[tabIndex].initialized) {
|
||||
loadOrders(tabIndex, 1)
|
||||
}
|
||||
}, [tabIndex])
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (!currentState.finished && !currentState.loading) {
|
||||
loadOrders(tabIndex, currentState.page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabChange = (index: number) => {
|
||||
if (index !== tabIndex) {
|
||||
setTabIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex flex-col'>
|
||||
{/* 自定义 Tabs 标题栏 */}
|
||||
<View className='bg-white flex flex-row items-center border-b border-gray-100'>
|
||||
{tabList.map((tab, index) => (
|
||||
<View
|
||||
key={tab.title}
|
||||
className='flex-1 flex flex-col items-center justify-center py-3'
|
||||
onClick={() => handleTabChange(index)}
|
||||
>
|
||||
<Text
|
||||
className='text-sm font-medium'
|
||||
style={{
|
||||
color: index === tabIndex ? '#ee0a24' : '#666',
|
||||
}}
|
||||
>
|
||||
{tab.title}
|
||||
</Text>
|
||||
{index === tabIndex && (
|
||||
<View
|
||||
className='mt-1 rounded-full'
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '3px',
|
||||
backgroundColor: '#ee0a24',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 内容区域 - 每个 tab 独立渲染 */}
|
||||
<View className='flex-1 relative'>
|
||||
{tabList.map((tab, idx) => (
|
||||
<View
|
||||
key={tab.title}
|
||||
className='absolute inset-0'
|
||||
style={{
|
||||
display: idx === tabIndex ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='flex-1'
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={100}
|
||||
>
|
||||
<View className='p-3'>
|
||||
{tabStates[idx].orders.length > 0 ? (
|
||||
tabStates[idx].orders.map(order => (
|
||||
<OrderCard key={order.orderId} order={order} />
|
||||
))
|
||||
) : tabStates[idx].initialized && !tabStates[idx].loading ? (
|
||||
<EmptyState text='暂无订单' />
|
||||
) : null}
|
||||
<LoadMore
|
||||
loading={tabStates[idx].loading}
|
||||
finished={tabStates[idx].finished}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderListPage
|
||||
250
src_bak/pages/order/logistics.tsx
Normal file
250
src_bak/pages/order/logistics.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button } from '@nutui/nutui-react-taro'
|
||||
import { queryLogistics, formatLogisticsStatus, EXPRESS_COMPANIES } from '@/api/shop/shopLogistics'
|
||||
import type { LogisticsInfo, LogisticsTrack } from '@/api/shop/shopLogistics'
|
||||
import Loading from '@/components/common/Loading'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '物流详情',
|
||||
})
|
||||
|
||||
const LogisticsPage: React.FC = () => {
|
||||
const { orderId, expressNo, expressCompany } = Taro.getCurrentInstance().router?.params || {}
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [logisticsInfo, setLogisticsInfo] = useState<LogisticsInfo | null>(null)
|
||||
const [trackList, setTrackList] = useState<LogisticsTrack[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
loadLogistics()
|
||||
}, [])
|
||||
|
||||
const loadLogistics = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 必须传入参数
|
||||
if (!expressNo || !expressCompany) {
|
||||
Taro.showToast({ title: '缺少物流参数', icon: 'none' })
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const res = await queryLogistics({
|
||||
orderId,
|
||||
expressNo,
|
||||
expressCompany,
|
||||
})
|
||||
if (res?.data) {
|
||||
setLogisticsInfo(res.data.logisticsInfo)
|
||||
setTrackList(res.data.trackList)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载物流信息失败', err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制单号
|
||||
const copyExpressNo = () => {
|
||||
if (logisticsInfo?.expressNo) {
|
||||
Taro.setClipboardData({
|
||||
data: logisticsInfo.expressNo,
|
||||
success: () => {
|
||||
Taro.showToast({ title: '单号已复制', icon: 'success' })
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 联系快递员(模拟)
|
||||
const contactRider = () => {
|
||||
Taro.makePhoneCall({
|
||||
phoneNumber: '400-811-1111',
|
||||
fail: () => {
|
||||
Taro.showToast({ title: '拨打失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (time: string) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading fullscreen />
|
||||
}
|
||||
|
||||
if (!logisticsInfo) {
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
|
||||
<EmptyState text='暂无物流信息' />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const statusInfo = formatLogisticsStatus(logisticsInfo.status)
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 pb-4 flex flex-col'>
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
{/* 快递信息卡片 */}
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||||
<View className='flex items-center gap-3'>
|
||||
{/* 快递logo */}
|
||||
<View className='w-12 h-12 rounded-lg bg-gray-100 flex items-center justify-center'>
|
||||
<Text className='text-xl'>📦</Text>
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<View className='flex items-center gap-2'>
|
||||
<Text className='text-sm font-medium text-gray-800'>{logisticsInfo.expressCompanyName}</Text>
|
||||
<Text className='text-xs text-gray-400'>•</Text>
|
||||
<Text className='text-sm text-gray-500'>{statusInfo.text}</Text>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-400 mt-1'>
|
||||
运单号: {logisticsInfo.expressNo}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
className='px-3 py-1 rounded-full bg-gray-50'
|
||||
onClick={copyExpressNo}
|
||||
>
|
||||
<Text className='text-xs text-gray-500'>复制</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 预计送达 */}
|
||||
{logisticsInfo.estimatedTime && (
|
||||
<View className='mt-3 pt-3 border-t border-gray-100'>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
预计送达: {formatDate(logisticsInfo.estimatedTime)} 24:00 前
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 当前状态 */}
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||||
<View className='flex items-start gap-3'>
|
||||
<View className={`w-10 h-10 rounded-full flex items-center justify-center ${statusInfo.icon === '🚚' ? 'bg-green-50' : 'bg-gray-50'}`}>
|
||||
<Text>{statusInfo.icon}</Text>
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm font-medium text-gray-800'>{logisticsInfo.status}</Text>
|
||||
{logisticsInfo.currentLocation && (
|
||||
<Text className='text-xs text-gray-400 mt-1'>
|
||||
当前位于: {logisticsInfo.currentLocation}
|
||||
</Text>
|
||||
)}
|
||||
<Text className='text-xs text-gray-400 mt-1'>
|
||||
更新时间: {formatTime(logisticsInfo.updateTime)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 收货人信息 */}
|
||||
{logisticsInfo.receiverInfo && (
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-2 block'>收货信息</Text>
|
||||
<View className='flex items-center gap-2'>
|
||||
<Text className='text-sm text-gray-600'>{logisticsInfo.receiverInfo.name}</Text>
|
||||
<Text className='text-sm text-gray-500'>{logisticsInfo.receiverInfo.phone}</Text>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-400 mt-1'>{logisticsInfo.receiverInfo.address}</Text>
|
||||
</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='relative'>
|
||||
{/* 竖线 */}
|
||||
<View className='absolute left-4 top-0 bottom-0 w-px bg-gray-200' />
|
||||
|
||||
{trackList.map((item, idx) => (
|
||||
<View key={idx} className={`relative flex gap-3 pb-4 ${idx === trackList.length - 1 ? 'pb-0' : ''}`}>
|
||||
{/* 圆点 */}
|
||||
<View
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center z-10 ${
|
||||
item.isCompleted ? 'bg-green-500' : 'bg-white border-2 border-green-500'
|
||||
}`}
|
||||
>
|
||||
{item.isCompleted && idx !== 0 ? (
|
||||
<Text className='text-white text-xs'>✓</Text>
|
||||
) : !item.isCompleted ? (
|
||||
<View className='w-2 h-2 rounded-full bg-green-500' />
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* 内容 */}
|
||||
<View className='flex-1 pt-1'>
|
||||
<View className='flex justify-between items-start'>
|
||||
<Text className={`text-sm ${item.isCompleted ? 'text-gray-600' : 'text-gray-800 font-medium'}`}>
|
||||
{item.status}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-400'>{formatTime(item.time)}</Text>
|
||||
</View>
|
||||
<Text className={`text-xs mt-1 ${item.isCompleted ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
{item.description}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1'>{item.location}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 温馨提示 */}
|
||||
<View className='bg-orange-50 mx-3 mt-3 p-4 rounded-xl'>
|
||||
<Text className='text-sm text-orange-600 font-medium'>温馨提示</Text>
|
||||
<Text className='text-xs text-orange-500 mt-1 leading-5'>
|
||||
1. 快递在运输过程中可能存在延迟,请耐心等待{'\n'}
|
||||
2. 如有疑问可联系快递公司客服{'\n'}
|
||||
3. 签收前请检查包裹是否完好
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部操作 */}
|
||||
<View className='bg-white border-t border-gray-100 p-3' style={{ paddingBottom: '20px' }}>
|
||||
<View className='flex gap-3'>
|
||||
<Button
|
||||
size='small'
|
||||
className='flex-1 rounded-full border-gray-300 text-gray-600'
|
||||
onClick={contactRider}
|
||||
>
|
||||
联系客服
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
className='flex-1 rounded-full'
|
||||
style={{ backgroundColor: '#0e932e' }}
|
||||
onClick={() => Taro.showToast({ title: '功能开发中', icon: 'none' })}
|
||||
>
|
||||
催单
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogisticsPage
|
||||
6
src_bak/pages/order/order.config.ts
Normal file
6
src_bak/pages/order/order.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
navigationBarTitleText: '我的',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f8f8f8'
|
||||
}
|
||||
9
src_bak/pages/order/order.scss
Normal file
9
src_bak/pages/order/order.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
padding: 32px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.nut-avatar {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
65
src_bak/pages/order/order.tsx
Normal file
65
src_bak/pages/order/order.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { Cell, CellGroup, Avatar, Tag } from '@nutui/nutui-react-taro'
|
||||
import { useAppContext } from '@/hooks/useAppContext'
|
||||
import './order.scss'
|
||||
|
||||
export default function Order() {
|
||||
const { theme, toggleTheme } = useAppContext()
|
||||
|
||||
const techStack = [
|
||||
{ name: 'Taro', version: '4.0.8', color: 'primary' },
|
||||
{ name: 'React', version: '18.3.1', color: 'success' },
|
||||
{ name: 'TypeScript', version: '5.7.2', color: 'warning' },
|
||||
{ name: 'NutUI', version: '2.7.4', color: 'danger' },
|
||||
{ name: 'TailwindCSS', version: '3.4.17', color: 'primary' }
|
||||
]
|
||||
|
||||
return (
|
||||
<View className='home-page p-4'>
|
||||
<View className='flex-center mb-6'>
|
||||
<Avatar
|
||||
size='large'
|
||||
src='https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/7362/5fc1f425E224AFA46/a13f0a3e12a比6b4.png'
|
||||
/>
|
||||
</View>
|
||||
|
||||
<CellGroup className='mb-4' title='项目信息'>
|
||||
<Cell title='项目名称' description='Paopao Taro' />
|
||||
<Cell title='当前主题' description={theme === 'light' ? '浅色模式 🌞' : '深色模式 🌙'} />
|
||||
<Cell
|
||||
title='切换主题'
|
||||
extra={
|
||||
<Tag type={theme === 'light' ? 'primary' : 'dark'}>
|
||||
点击切换
|
||||
</Tag>
|
||||
}
|
||||
onClick={toggleTheme}
|
||||
/>
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup className='mb-4' title='技术栈'>
|
||||
{techStack.map((tech, index) => (
|
||||
<Cell
|
||||
key={index}
|
||||
title={tech.name}
|
||||
description={`v${tech.version}`}
|
||||
extra={
|
||||
<Tag type={tech.color as any}>{tech.name}</Tag>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</CellGroup>
|
||||
|
||||
<View className='mt-4 p-4 bg-gray-50 rounded-lg'>
|
||||
<Text className='text-sm text-gray-500'>
|
||||
📦 已集成工具库:
|
||||
</Text>
|
||||
<View className='mt-2 flex flex-wrap gap-2'>
|
||||
<Tag type='primary' className='m-1'>Day.js</Tag>
|
||||
<Tag type='success' className='m-1'>Crypto-js</Tag>
|
||||
<Tag type='warning' className='m-1'>React Router</Tag>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
93
src_bak/pages/order/refund-detail.tsx
Normal file
93
src_bak/pages/order/refund-detail.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { getRefundDetail, type RefundDetail } from '@/api/shop/shopOrderRefund'
|
||||
import Loading from '@/components/common/Loading'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '退款详情',
|
||||
})
|
||||
|
||||
const RefundDetailPage: React.FC = () => {
|
||||
const { orderId } = Taro.getCurrentInstance().router?.params || {}
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refundDetail, setRefundDetail] = useState<RefundDetail | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadRefundDetail()
|
||||
}, [orderId])
|
||||
|
||||
const loadRefundDetail = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const data = await getRefundDetail(orderId || '')
|
||||
setRefundDetail(data)
|
||||
} catch (err) {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
console.error('加载退款详情失败', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading fullscreen />
|
||||
}
|
||||
|
||||
if (!refundDetail) {
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
|
||||
<EmptyState text='未找到退款信息' />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex flex-col'>
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
{/* 退款状态 */}
|
||||
<View className='p-4 text-center bg-white'>
|
||||
<Text className='text-lg font-bold text-orange-500 block'>{refundDetail.statusText}</Text>
|
||||
<Text className='text-sm text-gray-400 mt-1 block'>
|
||||
退款金额: {'\u00A5'}{refundDetail.refundMoney}
|
||||
</Text>
|
||||
</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>
|
||||
{refundDetail.steps.map((step, idx) => (
|
||||
<View key={idx} className='flex gap-3 pb-4 border-b border-gray-50' style={idx === refundDetail.steps.length - 1 ? { borderBottom: 'none', paddingBottom: '0' } : undefined}>
|
||||
<View className='flex flex-col items-center'>
|
||||
<View className={`w-3 h-3 rounded-full ${step.done ? 'bg-green-500' : 'bg-gray-300'}`} />
|
||||
{idx < refundDetail.steps.length - 1 && (
|
||||
<View className={`w-1 flex-1 min-h-4 ${step.done ? 'bg-green-500' : 'bg-gray-200'}`} />
|
||||
)}
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className={`text-sm ${step.done ? 'text-gray-800' : 'text-gray-400'}`}>{step.title}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-0 block'>{step.desc}</Text>
|
||||
{step.time && (
|
||||
<Text className='text-xs text-gray-400 mt-0 block'>{step.time}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 退款信息 */}
|
||||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl mb-4'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-2 block'>退款信息</Text>
|
||||
<View className='text-xs text-gray-500 leading-6'>
|
||||
<Text>订单编号: {refundDetail.orderNo}</Text>
|
||||
<Text>退款原因: {refundDetail.reason}</Text>
|
||||
<Text>申请时间: {refundDetail.time}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default RefundDetailPage
|
||||
113
src_bak/pages/order/refund.tsx
Normal file
113
src_bak/pages/order/refund.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button } from '@nutui/nutui-react-taro'
|
||||
import { applyAfterSale } from '@/api/shop/shopAfterSale'
|
||||
import Price from '@/components/common/Price'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '申请退款',
|
||||
})
|
||||
|
||||
const REASON_OPTIONS = ['不想要了', '商品描述不符', '商品质量问题', '发货太慢', '其他原因']
|
||||
|
||||
const RefundPage: React.FC = () => {
|
||||
const { id } = Taro.getCurrentInstance().router?.params || {}
|
||||
const [reason, setReason] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [selectedReason, setSelectedReason] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedReason) {
|
||||
Taro.showToast({ title: '请选择退款原因', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
Taro.showModal({
|
||||
title: '确认',
|
||||
content: '确定要提交退款申请吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
setLoading(true)
|
||||
try {
|
||||
Taro.showToast({ title: '提交中...', icon: 'loading' })
|
||||
const result = await applyAfterSale({
|
||||
orderId: id || '',
|
||||
type: 'refund',
|
||||
reason: selectedReason,
|
||||
description: description,
|
||||
amount: 0 // 金额由后端计算
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
Taro.showToast({ title: '申请已提交', icon: 'success' })
|
||||
setTimeout(() => Taro.navigateBack(), 1500)
|
||||
} else {
|
||||
Taro.showToast({ title: result.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('申请退款失败:', err)
|
||||
Taro.showToast({ title: '提交失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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 flex-wrap gap-2'>
|
||||
{REASON_OPTIONS.map(r => (
|
||||
<View
|
||||
key={r}
|
||||
className={`px-3 py-1 rounded-full text-sm ${
|
||||
selectedReason === r
|
||||
? 'bg-green-50 text-green-600 border border-green-500'
|
||||
: 'bg-gray-50 text-gray-600 border border-gray-200'
|
||||
}`}
|
||||
onClick={() => setSelectedReason(r)}
|
||||
>
|
||||
<Text>{r}</Text>
|
||||
</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-2 block'>退款说明</Text>
|
||||
<View className='w-full bg-gray-50 rounded-lg p-3 text-sm'>
|
||||
<Input
|
||||
className='w-full'
|
||||
placeholder='选填,请详细描述退款原因'
|
||||
value={description}
|
||||
onInput={e => setDescription(e.detail.value)}
|
||||
style={{ minHeight: '120px' }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<View className='bg-white border-t border-gray-100 p-3' style={{ paddingBottom: '20px' }}>
|
||||
<Button
|
||||
type='primary'
|
||||
className='w-full rounded-full'
|
||||
style={{ backgroundColor: '#ee0a24' }}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
提交申请
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default RefundPage
|
||||
Reference in New Issue
Block a user