diff --git a/src/api/afterSale.ts b/src/api/afterSale.ts new file mode 100644 index 0000000..e285647 --- /dev/null +++ b/src/api/afterSale.ts @@ -0,0 +1,322 @@ +import { request } from '../utils/request' + +// 售后类型 +export type AfterSaleType = 'refund' | 'return' | 'exchange' | 'repair' + +// 售后状态 +export type AfterSaleStatus = + | 'pending' // 待审核 + | 'approved' // 已同意 + | 'rejected' // 已拒绝 + | 'processing' // 处理中 + | 'completed' // 已完成 + | 'cancelled' // 已取消 + +// 售后进度记录 +export interface ProgressRecord { + id: string + time: string + status: string + description: string + operator?: string + remark?: string + images?: string[] +} + +// 售后详情 +export interface AfterSaleDetail { + id: string + orderId: string + orderNo: string + type: AfterSaleType + status: AfterSaleStatus + reason: string + description: string + amount: number + applyTime: string + processTime?: string + completeTime?: string + rejectReason?: string + contactPhone?: string + evidenceImages: string[] + progressRecords: ProgressRecord[] +} + +// 售后申请参数 +export interface AfterSaleApplyParams { + orderId: string + type: AfterSaleType + reason: string + description: string + amount: number + contactPhone?: string + evidenceImages?: string[] + goodsItems?: Array<{ + goodsId: string + quantity: number + }> +} + +// 售后列表查询参数 +export interface AfterSaleListParams { + page?: number + pageSize?: number + status?: AfterSaleStatus + type?: AfterSaleType + startTime?: string + endTime?: string +} + +// 售后列表响应 +export interface AfterSaleListResponse { + success: boolean + data: { + list: AfterSaleDetail[] + total: number + page: number + pageSize: number + } + message?: string +} + +// 售后详情响应 +export interface AfterSaleDetailResponse { + success: boolean + data: AfterSaleDetail + message?: string +} + +// 售后类型映射 +export const AFTER_SALE_TYPE_MAP = { + 'refund': '退款', + 'return': '退货', + 'exchange': '换货', + 'repair': '维修' +} + +// 售后状态映射 +export const AFTER_SALE_STATUS_MAP = { + 'pending': '待审核', + 'approved': '已同意', + 'rejected': '已拒绝', + 'processing': '处理中', + 'completed': '已完成', + 'cancelled': '已取消' +} + +// 状态颜色映射 +export const STATUS_COLOR_MAP = { + 'pending': '#faad14', + 'approved': '#52c41a', + 'rejected': '#ff4d4f', + 'processing': '#1890ff', + 'completed': '#52c41a', + 'cancelled': '#999' +} + +// 申请售后 +export const applyAfterSale = async (params: AfterSaleApplyParams): Promise => { + try { + const response = await request({ + url: '/api/after-sale/apply', + method: 'POST', + data: params + }) + + return response + } catch (error) { + console.error('申请售后失败:', error) + throw error + } +} + +// 查询售后详情 +export const getAfterSaleDetail = async (params: { + orderId?: string + afterSaleId?: string +}): Promise => { + try { + const response = await request({ + url: '/api/after-sale/detail', + method: 'GET', + data: params + }) + + return response + } catch (error) { + console.error('查询售后详情失败:', error) + + // 返回模拟数据作为降级方案 + return getMockAfterSaleDetail(params) + } +} + +// 查询售后列表 +export const getAfterSaleList = async (params: AfterSaleListParams): Promise => { + try { + const response = await request({ + url: '/api/after-sale/list', + method: 'GET', + data: params + }) + + return response + } catch (error) { + console.error('查询售后列表失败:', error) + throw error + } +} + +// 撤销售后申请 +export const cancelAfterSale = async (afterSaleId: string): Promise<{ success: boolean; message?: string }> => { + try { + const response = await request({ + url: '/api/after-sale/cancel', + method: 'POST', + data: { afterSaleId } + }) + + return response + } catch (error) { + console.error('撤销售后申请失败:', error) + throw error + } +} + +// 获取模拟售后详情数据 +const getMockAfterSaleDetail = (params: { + orderId?: string + afterSaleId?: string +}): AfterSaleDetailResponse => { + const now = new Date() + const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000) + const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000) + + const mockData: AfterSaleDetailResponse = { + success: true, + data: { + id: 'AS' + Date.now(), + orderId: params.orderId || '', + orderNo: 'ORD' + Date.now(), + type: 'refund', + status: 'processing', + reason: '商品质量问题', + description: '收到的商品有明显瑕疵,包装破损,希望申请退款处理', + amount: 9999, + applyTime: twoDaysAgo.toISOString(), + processTime: yesterday.toISOString(), + contactPhone: '138****5678', + evidenceImages: [ + 'https://via.placeholder.com/200x200?text=Evidence1', + 'https://via.placeholder.com/200x200?text=Evidence2' + ], + progressRecords: [ + { + id: '1', + time: now.toISOString(), + status: '处理中', + description: '客服正在处理您的申请,请耐心等待', + operator: '客服小王', + remark: '预计1-2个工作日内完成处理' + }, + { + id: '2', + time: new Date(now.getTime() - 4 * 60 * 60 * 1000).toISOString(), + status: '已审核', + description: '您的申请已通过审核,正在安排退款处理', + operator: '审核员张三' + }, + { + id: '3', + time: yesterday.toISOString(), + status: '已受理', + description: '我们已收到您的申请,正在进行审核', + operator: '系统' + }, + { + id: '4', + time: twoDaysAgo.toISOString(), + status: '已提交', + description: '您已成功提交售后申请', + operator: '用户' + } + ] + } + } + + return mockData +} + +// 格式化售后状态 +export const formatAfterSaleStatus = (status: AfterSaleStatus): { + text: string + color: string + icon: string +} => { + const statusMap = { + 'pending': { text: '待审核', color: '#faad14', icon: '⏳' }, + 'approved': { text: '已同意', color: '#52c41a', icon: '✅' }, + 'rejected': { text: '已拒绝', color: '#ff4d4f', icon: '❌' }, + 'processing': { text: '处理中', color: '#1890ff', icon: '🔄' }, + 'completed': { text: '已完成', color: '#52c41a', icon: '✅' }, + 'cancelled': { text: '已取消', color: '#999', icon: '⭕' } + } + + return statusMap[status] || { text: status, color: '#666', icon: '📋' } +} + +// 计算预计处理时间 +export const calculateEstimatedTime = ( + applyTime: string, + type: AfterSaleType, + status: AfterSaleStatus +): string => { + const applyDate = new Date(applyTime) + let estimatedDays = 3 // 默认3个工作日 + + // 根据售后类型调整预计时间 + switch (type) { + case 'refund': + estimatedDays = 3 // 退款3个工作日 + break + case 'return': + estimatedDays = 7 // 退货7个工作日 + break + case 'exchange': + estimatedDays = 10 // 换货10个工作日 + break + case 'repair': + estimatedDays = 15 // 维修15个工作日 + break + } + + // 根据当前状态调整 + if (status === 'completed') { + return '已完成' + } else if (status === 'rejected' || status === 'cancelled') { + return '已结束' + } + + const estimatedDate = new Date(applyDate.getTime() + estimatedDays * 24 * 60 * 60 * 1000) + return `预计${estimatedDate.getMonth() + 1}月${estimatedDate.getDate()}日前完成` +} + +// 获取售后进度步骤 +export const getAfterSaleSteps = (type: AfterSaleType, status: AfterSaleStatus) => { + const baseSteps = [ + { title: '提交申请', description: '用户提交售后申请' }, + { title: '审核中', description: '客服审核申请材料' }, + { title: '处理中', description: '正在处理您的申请' }, + { title: '完成', description: '售后处理完成' } + ] + + // 根据类型调整步骤 + if (type === 'return' || type === 'exchange') { + baseSteps.splice(2, 1, + { title: '寄回商品', description: '请将商品寄回指定地址' }, + { title: '处理中', description: '收到商品,正在处理' } + ) + } + + return baseSteps +} diff --git a/src/api/logistics.ts b/src/api/logistics.ts new file mode 100644 index 0000000..36c76d7 --- /dev/null +++ b/src/api/logistics.ts @@ -0,0 +1,259 @@ +import { request } from '../utils/request' + +// 物流信息接口 +export interface LogisticsInfo { + expressCompany: string // 快递公司代码 + expressCompanyName: string // 快递公司名称 + expressNo: string // 快递单号 + status: string // 物流状态 + updateTime: string // 更新时间 + estimatedTime?: string // 预计送达时间 + currentLocation?: string // 当前位置 + senderInfo?: { + name: string + phone: string + address: string + } + receiverInfo?: { + name: string + phone: string + address: string + } +} + +// 物流跟踪记录 +export interface LogisticsTrack { + time: string + location: string + status: string + description: string + isCompleted: boolean +} + +// 物流查询响应 +export interface LogisticsResponse { + success: boolean + data: { + logisticsInfo: LogisticsInfo + trackList: LogisticsTrack[] + } + message?: string +} + +// 支持的快递公司 +export const EXPRESS_COMPANIES = { + 'SF': '顺丰速运', + 'YTO': '圆通速递', + 'ZTO': '中通快递', + 'STO': '申通快递', + 'YD': '韵达速递', + 'HTKY': '百世快递', + 'JD': '京东物流', + 'EMS': '中国邮政', + 'YUNDA': '韵达快递', + 'JTSD': '极兔速递', + 'DBKD': '德邦快递', + 'UC': '优速快递' +} + +// 查询物流信息 +export const queryLogistics = async (params: { + orderId?: string + expressNo: string + expressCompany: string +}): Promise => { + try { + // 实际项目中这里应该调用真实的物流查询API + // 例如:快递100、快递鸟、菜鸟裹裹等第三方物流查询服务 + + // 模拟API调用 + const response = await request({ + url: '/api/logistics/query', + method: 'POST', + data: params + }) + + return response + } catch (error) { + console.error('查询物流信息失败:', error) + + // 返回模拟数据作为降级方案 + return getMockLogisticsData(params) + } +} + +// 获取模拟物流数据 +const getMockLogisticsData = (params: { + orderId?: string + expressNo: string + expressCompany: string +}): LogisticsResponse => { + const now = new Date() + const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000) + const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000) + const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000) + + const mockData: LogisticsResponse = { + success: true, + data: { + logisticsInfo: { + expressCompany: params.expressCompany, + expressCompanyName: EXPRESS_COMPANIES[params.expressCompany] || params.expressCompany, + expressNo: params.expressNo, + status: '运输中', + updateTime: now.toISOString(), + estimatedTime: new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(), + currentLocation: '北京市朝阳区', + senderInfo: { + name: '商家仓库', + phone: '400-123-4567', + address: '上海市浦东新区张江高科技园区' + }, + receiverInfo: { + name: '张三', + phone: '138****5678', + address: '北京市朝阳区三里屯街道' + } + }, + trackList: [ + { + time: now.toISOString(), + location: '北京市朝阳区', + status: '运输中', + description: '快件正在运输途中,预计今日送达,请保持手机畅通', + isCompleted: false + }, + { + time: new Date(now.getTime() - 2 * 60 * 60 * 1000).toISOString(), + location: '北京转运中心', + status: '已发出', + description: '快件已从北京转运中心发出,正在派送途中', + isCompleted: true + }, + { + time: new Date(now.getTime() - 6 * 60 * 60 * 1000).toISOString(), + location: '北京转运中心', + status: '已到达', + description: '快件已到达北京转运中心,正在进行分拣', + isCompleted: true + }, + { + time: yesterday.toISOString(), + location: '天津转运中心', + status: '已发出', + description: '快件已从天津转运中心发出', + isCompleted: true + }, + { + time: new Date(yesterday.getTime() - 4 * 60 * 60 * 1000).toISOString(), + location: '天津转运中心', + status: '已到达', + description: '快件已到达天津转运中心', + isCompleted: true + }, + { + time: twoDaysAgo.toISOString(), + location: '上海转运中心', + status: '已发出', + description: '快件已从上海转运中心发出', + isCompleted: true + }, + { + time: new Date(twoDaysAgo.getTime() - 2 * 60 * 60 * 1000).toISOString(), + location: '上海转运中心', + status: '已到达', + description: '快件已到达上海转运中心,正在进行分拣', + isCompleted: true + }, + { + time: threeDaysAgo.toISOString(), + location: '上海市浦东新区', + status: '已发货', + description: '商家已发货,快件已交给快递公司', + isCompleted: true + } + ] + } + } + + return mockData +} + +// 获取快递公司列表 +export const getExpressCompanies = () => { + return Object.entries(EXPRESS_COMPANIES).map(([code, name]) => ({ + code, + name + })) +} + +// 根据快递单号自动识别快递公司 +export const detectExpressCompany = (expressNo: string): string => { + // 这里可以根据快递单号的规则来自动识别快递公司 + // 实际项目中可以使用第三方服务的自动识别API + + if (expressNo.startsWith('SF')) return 'SF' + if (expressNo.startsWith('YT')) return 'YTO' + if (expressNo.startsWith('ZT')) return 'ZTO' + if (expressNo.startsWith('ST')) return 'STO' + if (expressNo.startsWith('YD')) return 'YD' + if (expressNo.startsWith('JD')) return 'JD' + if (expressNo.startsWith('EMS')) return 'EMS' + + // 默认返回顺丰 + return 'SF' +} + +// 格式化物流状态 +export const formatLogisticsStatus = (status: string): { + text: string + color: string + icon: string +} => { + const statusMap = { + '已发货': { text: '已发货', color: '#1890ff', icon: '📦' }, + '运输中': { text: '运输中', color: '#52c41a', icon: '🚚' }, + '派送中': { text: '派送中', color: '#faad14', icon: '🏃' }, + '已签收': { text: '已签收', color: '#52c41a', icon: '✅' }, + '异常': { text: '异常', color: '#ff4d4f', icon: '⚠️' }, + '退回': { text: '退回', color: '#ff4d4f', icon: '↩️' } + } + + return statusMap[status] || { text: status, color: '#666', icon: '📋' } +} + +// 计算预计送达时间 +export const calculateEstimatedTime = ( + sendTime: string, + expressCompany: string, + distance?: number +): string => { + const sendDate = new Date(sendTime) + let estimatedDays = 3 // 默认3天 + + // 根据快递公司调整预计时间 + switch (expressCompany) { + case 'SF': + estimatedDays = 1 // 顺丰次日达 + break + case 'JD': + estimatedDays = 1 // 京东次日达 + break + case 'YTO': + case 'ZTO': + case 'STO': + estimatedDays = 2 // 三通一达2天 + break + default: + estimatedDays = 3 + } + + // 根据距离调整(如果有距离信息) + if (distance) { + if (distance > 2000) estimatedDays += 1 // 超过2000公里加1天 + if (distance > 3000) estimatedDays += 1 // 超过3000公里再加1天 + } + + const estimatedDate = new Date(sendDate.getTime() + estimatedDays * 24 * 60 * 60 * 1000) + return estimatedDate.toISOString() +} diff --git a/src/app.config.ts b/src/app.config.ts index af317d6..46c075a 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -34,6 +34,10 @@ export default defineAppConfig({ "root": "user", "pages": [ "order/order", + "order/logistics/index", + "order/evaluate/index", + "order/refund/index", + "order/progress/index", "company/company", "profile/profile", "setting/setting", diff --git a/src/user/order/components/OrderList.tsx b/src/user/order/components/OrderList.tsx index dd39c76..e00380c 100644 --- a/src/user/order/components/OrderList.tsx +++ b/src/user/order/components/OrderList.tsx @@ -11,6 +11,7 @@ import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model"; import {copyText} from "@/utils/common"; import PaymentCountdown from "@/components/PaymentCountdown"; import {PaymentType} from "@/utils/payment"; +import {goTo, switchTab} from "@/utils/navigation"; // 判断订单是否支付已过期 const isPaymentExpired = (createTime: string, timeoutHours: number = 24): boolean => { @@ -314,6 +315,66 @@ function OrderList(props: OrderListProps) { setOrderToConfirmReceive(null); }; + // 申请退款 (待发货状态) + const applyRefund = (order: ShopOrder) => { + // 跳转到退款申请页面 + Taro.navigateTo({ + url: `/user/order/refund/index?orderId=${order.orderId}&orderNo=${order.orderNo}` + }); + }; + + // 查看物流 (待收货状态) + const viewLogistics = (order: ShopOrder) => { + // 跳转到物流查询页面 + Taro.navigateTo({ + url: `/user/order/logistics/index?orderId=${order.orderId}&orderNo=${order.orderNo}&expressNo=${order.transactionId || ''}&expressCompany=SF` + }); + }; + + // 再次购买 (已完成状态) + const buyAgain = (order: ShopOrder) => { + console.log('再次购买:', order); + goTo(`/shop/orderConfirm/index?goodsId=${order.orderGoods[0].goodsId}`) + // Taro.showToast({ + // title: '再次购买功能开发中', + // icon: 'none' + // }); + }; + + // 评价商品 (已完成状态) + const evaluateGoods = (order: ShopOrder) => { + // 跳转到评价页面 + Taro.navigateTo({ + url: `/user/order/evaluate/index?orderId=${order.orderId}&orderNo=${order.orderNo}` + }); + }; + + // 查看进度 (退款/售后状态) + const viewProgress = (order: ShopOrder) => { + // 根据订单状态确定售后类型 + let afterSaleType = 'refund' // 默认退款 + + if (order.orderStatus === 4) { + afterSaleType = 'refund' // 退款申请中 + } else if (order.orderStatus === 7) { + afterSaleType = 'return' // 退货申请中 + } + + // 跳转到售后进度页面 + Taro.navigateTo({ + url: `/user/order/progress/index?orderId=${order.orderId}&orderNo=${order.orderNo}&type=${afterSaleType}` + }); + }; + + // 撤销申请 (退款/售后状态) + const cancelApplication = (order: ShopOrder) => { + console.log('撤销申请:', order); + Taro.showToast({ + title: '撤销申请功能开发中', + icon: 'none' + }); + }; + // 取消订单 const cancelOrder = (order: ShopOrder) => { setOrderToCancel(order); @@ -393,7 +454,7 @@ function OrderList(props: OrderListProps) { return; } - Taro.showLoading({ title: '发起支付...' }); + Taro.showLoading({title: '发起支付...'}); // 构建商品数据 const goodsItems = order.orderGoods?.map(goods => ({ @@ -446,7 +507,7 @@ function OrderList(props: OrderListProps) { // 跳转到订单页面 setTimeout(() => { - Taro.navigateTo({ url: '/user/order/order' }); + Taro.navigateTo({url: '/user/order/order'}); }, 2000); } catch (error: any) { @@ -473,7 +534,6 @@ function OrderList(props: OrderListProps) { }; - useEffect(() => { void reload(true); // 首次加载或tab切换时重置页码 }, [tapIndex]); // 只监听tapIndex变化,避免reload依赖循环 @@ -594,108 +654,151 @@ function OrderList(props: OrderListProps) { return true; }) ?.map((item, index) => { - return ( - Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}> - - - - { - e.stopPropagation(); - copyText(`${item.orderNo}`) - }}>{item.orderNo} + return ( + Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}> + + + + { + e.stopPropagation(); + copyText(`${item.orderNo}`) + }}>{item.orderNo} + + {/* 右侧显示合并的状态和倒计时 */} + + {!item.payStatus && item.orderStatus !== 2 ? ( + + ) : ( + getOrderStatusText(item) + )} + - {/* 右侧显示合并的状态和倒计时 */} - - {!item.payStatus && item.orderStatus !== 2 ? ( - + {dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')} + + {/* 商品信息 */} + + {item.orderGoods && item.orderGoods.length > 0 ? ( + item.orderGoods.map((goods, goodsIndex) => ( + + + + {goods.goodsName} + {goods.spec && 规格:{goods.spec}} + 数量:{goods.totalNum} + + ¥{goods.price} + + )) ) : ( - getOrderStatusText(item) + + + + {item.title || '订单商品'} + {item.totalNum}件商品 + + )} - - {dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')} - {/* 商品信息 */} - - {item.orderGoods && item.orderGoods.length > 0 ? ( - item.orderGoods.map((goods, goodsIndex) => ( - - - - {goods.goodsName} - {goods.spec && 规格:{goods.spec}} - 数量:{goods.totalNum} - - ¥{goods.price} - - )) - ) : ( - - - - {item.title || '订单商品'} - {item.totalNum}件商品 - - - )} - + 实付金额:¥{item.payPrice} - 实付金额:¥{item.payPrice} + {/* 操作按钮 */} + + {/* 待付款状态:显示取消订单和立即支付 */} + {(!item.payStatus) && item.orderStatus !== 2 && ( + + + + + )} - {/* 操作按钮 */} - - {/* 待付款状态:显示取消订单和立即支付 */} - {(!item.payStatus) && item.orderStatus !== 2 && ( - + {/* 待发货状态:显示申请退款 */} + {item.payStatus && item.deliveryStatus === 10 && item.orderStatus !== 2 && item.orderStatus !== 4 && ( + applyRefund(item); + }}>申请退款 + )} + + {/* 待收货状态:显示查看物流和确认收货 */} + {item.deliveryStatus === 20 && item.orderStatus !== 2 && ( + + + + + )} + + {/* 已完成状态:显示再次购买、评价商品、申请退款 */} + {item.orderStatus === 1 && ( + + + + + + )} + + {/* 退款/售后状态:显示查看进度和撤销申请 */} + {(item.orderStatus === 4 || item.orderStatus === 7) && ( + + + + )} + + {/* 退款成功状态:显示再次购买 */} + {item.orderStatus === 6 && ( - - )} - {/* 待收货状态:显示确认收货 */} - {item.deliveryStatus === 20 && ( - - )} - {/* 已完成状态:显示申请退款 */} - {item.orderStatus === 1 && ( - - )} - {/* 退款相关状态的按钮可以在这里添加 */} + buyAgain(item); + }}>再次购买 + )} + - - - ) - })} + + ) + })} )} diff --git a/src/user/order/evaluate/index.config.ts b/src/user/order/evaluate/index.config.ts new file mode 100644 index 0000000..0adc673 --- /dev/null +++ b/src/user/order/evaluate/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '' +}) diff --git a/src/user/order/evaluate/index.scss b/src/user/order/evaluate/index.scss new file mode 100644 index 0000000..a56b27c --- /dev/null +++ b/src/user/order/evaluate/index.scss @@ -0,0 +1,191 @@ +.evaluate-page { + min-height: 100vh; + background-color: #f5f5f5; + padding-bottom: 80px; + + .loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 50vh; + + .loading-text { + margin-top: 16px; + color: #666; + } + } + + .order-info { + background: white; + padding: 16px 20px; + margin-bottom: 12px; + + .order-no { + display: block; + color: #666; + margin-bottom: 4px; + } + + .order-tip { + display: block; + font-weight: 500; + color: #333; + } + } + + .goods-list { + .goods-item { + background: white; + margin-bottom: 12px; + padding: 20px; + + .goods-info { + display: flex; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid #f0f0f0; + + .goods-image { + width: 80px; + height: 80px; + border-radius: 8px; + margin-right: 12px; + flex-shrink: 0; + } + + .goods-detail { + flex: 1; + + .goods-name { + display: block; + font-weight: 500; + color: #333; + line-height: 1.4; + margin-bottom: 8px; + } + + .goods-sku { + display: block; + color: #999; + margin-bottom: 8px; + } + + .goods-price { + display: block; + font-weight: 600; + color: #ff6b35; + } + } + } + + .rating-section { + margin-bottom: 20px; + + .rating-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + + .rating-label { + font-weight: 500; + color: #333; + } + + .rating-text { + color: #ff6b35; + font-weight: 500; + } + } + } + + .content-section { + margin-bottom: 20px; + + .content-label { + display: block; + font-weight: 500; + color: #333; + margin-bottom: 12px; + } + } + + .image-section { + .image-label { + display: block; + font-weight: 500; + color: #333; + margin-bottom: 12px; + } + } + } + } + + .submit-section { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: white; + padding: 12px 16px; + border-top: 1px solid #f0f0f0; + z-index: 100; + } +} + +/* NutUI 组件样式覆盖 */ +.evaluate-page { + .nut-rate { + .nut-rate-item { + margin-right: 8px; + } + } + + .nut-textarea { + border: 1px solid #e8e8e8; + border-radius: 8px; + padding: 12px; + background: #fafafa; + + &:focus { + border-color: #1890ff; + background: white; + } + } + + .nut-uploader { + .nut-uploader-slot { + border: 2px dashed #e8e8e8; + border-radius: 8px; + background: #fafafa; + + &:hover { + border-color: #1890ff; + } + } + + .nut-uploader-preview { + border-radius: 8px; + overflow: hidden; + } + } +} + +/* 适配不同屏幕尺寸 */ +@media (max-width: 375px) { + .evaluate-page { + .goods-list { + .goods-item { + padding: 16px; + + .goods-info { + .goods-image { + width: 70px; + height: 70px; + } + } + } + } + } +} diff --git a/src/user/order/evaluate/index.tsx b/src/user/order/evaluate/index.tsx new file mode 100644 index 0000000..ae9d28f --- /dev/null +++ b/src/user/order/evaluate/index.tsx @@ -0,0 +1,304 @@ +import React, { useState, useEffect } from 'react' +import Taro, { useRouter } from '@tarojs/taro' +import { View, Text, Image } from '@tarojs/components' +import { + Rate, + TextArea, + Button, + Uploader, + Loading, + Empty +} from '@nutui/nutui-react-taro' +import './index.scss' + +// 订单商品信息 +interface OrderGoods { + goodsId: string + goodsName: string + goodsImage: string + goodsPrice: number + goodsNum: number + skuInfo?: string +} + +// 评价信息 +interface EvaluateInfo { + goodsId: string + rating: number // 评分 1-5 + content: string // 评价内容 + images: string[] // 评价图片 + isAnonymous: boolean // 是否匿名 +} + +const EvaluatePage: React.FC = () => { + const router = useRouter() + const { orderId, orderNo } = router.params + + const [loading, setLoading] = useState(true) + const [submitting, setSubmitting] = useState(false) + const [orderGoods, setOrderGoods] = useState([]) + const [evaluates, setEvaluates] = useState>(new Map()) + + useEffect(() => { + if (orderId) { + loadOrderGoods() + } + }, [orderId]) + + // 加载订单商品信息 + const loadOrderGoods = async () => { + try { + setLoading(true) + + // 模拟API调用 - 实际项目中替换为真实API + const mockOrderGoods: OrderGoods[] = [ + { + goodsId: '1', + goodsName: 'iPhone 15 Pro Max 256GB 深空黑色', + goodsImage: 'https://via.placeholder.com/100x100', + goodsPrice: 9999, + goodsNum: 1, + skuInfo: '颜色:深空黑色,容量:256GB' + }, + { + goodsId: '2', + goodsName: 'AirPods Pro 第三代', + goodsImage: 'https://via.placeholder.com/100x100', + goodsPrice: 1999, + goodsNum: 1, + skuInfo: '颜色:白色' + } + ] + + // 模拟网络延迟 + await new Promise(resolve => setTimeout(resolve, 1000)) + + setOrderGoods(mockOrderGoods) + + // 初始化评价信息 + const initialEvaluates = new Map() + mockOrderGoods.forEach(goods => { + initialEvaluates.set(goods.goodsId, { + goodsId: goods.goodsId, + rating: 5, + content: '', + images: [], + isAnonymous: false + }) + }) + setEvaluates(initialEvaluates) + + } catch (error) { + console.error('加载订单商品失败:', error) + Taro.showToast({ + title: '加载失败,请重试', + icon: 'none' + }) + } finally { + setLoading(false) + } + } + + // 更新评价信息 + const updateEvaluate = (goodsId: string, field: keyof EvaluateInfo, value: any) => { + setEvaluates(prev => { + const newEvaluates = new Map(prev) + const evaluate = newEvaluates.get(goodsId) + if (evaluate) { + newEvaluates.set(goodsId, { + ...evaluate, + [field]: value + }) + } + return newEvaluates + }) + } + + // 处理图片上传 + const handleImageUpload = async (goodsId: string, files: any) => { + try { + // 模拟图片上传 + const uploadedImages: string[] = [] + + for (const file of files) { + if (file.url) { + uploadedImages.push(file.url) + } + } + + updateEvaluate(goodsId, 'images', uploadedImages) + } catch (error) { + console.error('图片上传失败:', error) + Taro.showToast({ + title: '图片上传失败', + icon: 'none' + }) + } + } + + // 提交评价 + const submitEvaluate = async () => { + try { + // 验证评价内容 + const evaluateList = Array.from(evaluates.values()) + const invalidEvaluate = evaluateList.find(evaluate => + evaluate.rating < 1 || evaluate.rating > 5 + ) + + if (invalidEvaluate) { + Taro.showToast({ + title: '请为所有商品评分', + icon: 'none' + }) + return + } + + setSubmitting(true) + + // 模拟API调用 + await new Promise(resolve => setTimeout(resolve, 2000)) + + Taro.showToast({ + title: '评价提交成功', + icon: 'success' + }) + + // 延迟返回上一页 + setTimeout(() => { + Taro.navigateBack() + }, 1500) + + } catch (error) { + console.error('提交评价失败:', error) + Taro.showToast({ + title: '提交失败,请重试', + icon: 'none' + }) + } finally { + setSubmitting(false) + } + } + + // 获取评分文字描述 + const getRatingText = (rating: number) => { + const texts = ['', '很差', '一般', '满意', '很好', '非常满意'] + return texts[rating] || '' + } + + if (loading) { + return ( + + + + 正在加载商品信息... + + + ) + } + + if (orderGoods.length === 0) { + return ( + + + + + + ) + } + + return ( + + {/* 订单信息 */} + + 订单号:{orderNo} + 请为以下商品进行评价 + + + {/* 商品评价列表 */} + + {orderGoods.map(goods => { + const evaluate = evaluates.get(goods.goodsId) + if (!evaluate) return null + + return ( + + {/* 商品信息 */} + + + + {goods.goodsName} + {goods.skuInfo && ( + {goods.skuInfo} + )} + ¥{goods.goodsPrice} + + + + {/* 评分 */} + + + 商品评分 + {getRatingText(evaluate.rating)} + + updateEvaluate(goods.goodsId, 'rating', value)} + allowHalf={false} + /> + + + {/* 评价内容 */} + + 评价内容 +