campus-errand/miniapp/pages/order/order-detail.vue
2026-03-14 23:38:46 +08:00

686 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="detail-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="nav-back" @click="goBack">
<image class="back-icon" src="/static/ic_back.png" mode="aspectFit"></image>
</view>
<text class="navbar-title">订单详情</text>
<view class="nav-placeholder"></view>
</view>
</view>
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 页面标题 -->
<view class="page-title">
<text>我的{{ getTypeLabel(order.orderType) }}订单详情</text>
</view>
<!-- 订单状态 -->
<view class="status-bar" :class="getStatusClass(order.status)">
<text class="status-text">{{ getStatusLabel(order.status) }}</text>
</view>
<!-- 订单基本信息 -->
<view class="info-section">
<view class="section-title"><text>订单信息</text></view>
<view class="info-row">
<text class="info-label">订单ID</text>
<text class="info-value">{{ order.orderNo || '-' }}</text>
</view>
<view class="info-row" v-if="order.itemName">
<text class="info-label">物品/事项</text>
<text class="info-value">{{ order.itemName }}</text>
</view>
<view class="info-row" v-if="order.pickupLocation">
<text class="info-label">取货地点</text>
<text class="info-value">{{ order.pickupLocation }}</text>
</view>
<view class="info-row" v-if="order.deliveryLocation">
<text class="info-label">送达地点</text>
<text class="info-value">{{ order.deliveryLocation }}</text>
</view>
<view class="info-row" v-if="order.remark">
<text class="info-label">备注</text>
<text class="info-value">{{ order.remark }}</text>
</view>
<view class="info-row" v-if="order.goodsAmount">
<text class="info-label">商品金额</text>
<text class="info-value price">¥{{ order.goodsAmount }}</text>
</view>
<view class="info-row">
<text class="info-label">跑腿佣金</text>
<text class="info-value price">¥{{ order.commission }}</text>
</view>
<view class="info-row">
<text class="info-label">支付总额</text>
<text class="info-value price">¥{{ order.totalAmount }}</text>
</view>
<view class="info-row">
<text class="info-label">下单时间</text>
<text class="info-value">{{ formatTime(order.createdAt) }}</text>
</view>
<!-- 接单后显示 -->
<view class="info-row">
<text class="info-label">接单时间</text>
<text class="info-value">{{ order.acceptedAt ? formatTime(order.acceptedAt) : '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">跑腿昵称</text>
<text class="info-value">{{ order.runnerNickname || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">跑腿UID</text>
<text class="info-value">{{ order.runnerId || '-' }}</text>
</view>
<!-- 完成后显示 -->
<view class="info-row">
<text class="info-label">完成时间</text>
<text class="info-value">{{ order.completedAt ? formatTime(order.completedAt) : '-' }}</text>
</view>
</view>
<!-- 完成凭证 -->
<view class="info-section" v-if="order.completionProof">
<view class="section-title"><text>完成凭证</text></view>
<image
class="proof-image"
:src="order.completionProof"
mode="aspectFit"
@click="previewImage(order.completionProof)"
></image>
</view>
<!-- 美食街订单额外信息 -->
<view class="info-section" v-if="order.orderType === 'Food' && foodItems.length > 0">
<view class="section-title"><text>美食街订单详情</text></view>
<view class="info-row" v-if="order.packingFee">
<text class="info-label">打包费</text>
<text class="info-value price">¥{{ order.packingFee }}</text>
</view>
<!-- 按门店分组展示菜品 -->
<view v-for="(group, idx) in groupedFoodItems" :key="idx" class="shop-group">
<text class="shop-name">{{ group.shopName }}</text>
<view v-for="item in group.items" :key="item.id" class="dish-item">
<image v-if="item.dishPhoto" class="dish-photo" :src="item.dishPhoto" mode="aspectFill"></image>
<view class="dish-info">
<text class="dish-name">{{ item.dishName }}</text>
<text class="dish-price">¥{{ item.unitPrice }} × {{ item.quantity }}份</text>
</view>
</view>
</view>
<view class="info-row">
<text class="info-label">总份数</text>
<text class="info-value">{{ totalQuantity }}份</text>
</view>
<view class="info-row">
<text class="info-label">垫付总金额</text>
<text class="info-value price">¥{{ order.goodsAmount }}</text>
</view>
</view>
<!-- 申诉处理结果 -->
<view class="info-section" v-if="appeals.length > 0">
<view class="section-title"><text>申诉处理结果</text></view>
<view v-for="appeal in appeals" :key="appeal.id" class="appeal-item">
<text class="appeal-result">{{ appeal.result }}</text>
<text class="appeal-time">{{ formatTime(appeal.createdAt) }}</text>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<!-- 待接单:取消订单 -->
<template v-if="order.status === 'Pending'">
<view class="action-btn btn-cancel" @click="onCancelOrder">
<text>取消订单</text>
</view>
</template>
<!-- 进行中:联系跑腿 -->
<template v-else-if="order.status === 'InProgress'">
<view class="action-btn btn-primary" @click="goChat">
<text>联系跑腿</text>
</view>
</template>
<!-- 已完成:评价跑腿 + 联系跑腿 -->
<template v-else-if="order.status === 'Completed'">
<view v-if="!order.isReviewed" class="action-btn btn-primary" @click="openReview">
<text>评价跑腿</text>
</view>
<view class="action-btn btn-secondary" @click="goChat">
<text>联系跑腿</text>
</view>
</template>
<!-- 待确认:确认处理 -->
<template v-else-if="order.status === 'WaitConfirm'">
<view class="action-btn btn-primary" @click="goConfirm">
<text>确认处理</text>
</view>
</template>
</view>
<!-- 评价弹窗 -->
<view class="modal-mask" v-if="showReviewModal" @click="showReviewModal = false">
<view class="modal-content" @click.stop>
<text class="modal-title">评价跑腿</text>
<view class="star-row">
<text
v-for="star in 5"
:key="star"
class="star"
:class="{ active: reviewForm.rating >= star }"
@click="reviewForm.rating = star"
>★</text>
</view>
<textarea
class="review-input"
v-model="reviewForm.content"
placeholder="请输入评价内容(选填)"
maxlength="200"
></textarea>
<view class="modal-actions">
<button class="modal-btn cancel" @click="showReviewModal = false">取消</button>
<button class="modal-btn confirm" @click="submitReviewAction" :loading="reviewSubmitting">提交</button>
</view>
</view>
</view>
</view>
</template>
<script>
import { getOrderDetail, cancelOrder, submitReview } from '../../utils/api'
import { useUserStore } from '../../stores/user'
export default {
data() {
return {
orderId: null,
order: {},
foodItems: [],
appeals: [],
// 评价弹窗
showReviewModal: false,
reviewForm: { rating: 5, content: '' },
reviewSubmitting: false,
statusBarHeight: 0
}
},
computed: {
/** 按门店分组美食街菜品 */
groupedFoodItems() {
const groups = {}
this.foodItems.forEach(item => {
const key = item.shopId || item.shopName || '未知门店'
if (!groups[key]) {
groups[key] = { shopName: item.shopName || '未知门店', items: [] }
}
groups[key].items.push(item)
})
return Object.values(groups)
},
/** 总份数 */
totalQuantity() {
return this.foodItems.reduce((sum, item) => sum + (item.quantity || 0), 0)
}
},
onLoad(options) {
const sysInfo = uni.getSystemInfoSync()
this.statusBarHeight = sysInfo.statusBarHeight || 0
this.orderId = options.id
this.loadDetail()
},
methods: {
goBack() { uni.navigateBack() },
/** 加载订单详情 */
async loadDetail() {
if (!this.orderId) return
try {
const res = await getOrderDetail(this.orderId)
this.order = res || {}
this.foodItems = res?.foodItems || []
this.appeals = res?.appeals || []
// 设置页面标题
uni.setNavigationBarTitle({
title: `我的${this.getTypeLabel(this.order.orderType)}订单详情`
})
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
},
getTypeLabel(type) {
const map = { Pickup: '代取', Delivery: '代送', Help: '万能帮', Purchase: '代购', Food: '美食街' }
return map[type] || ''
},
getStatusLabel(status) {
const map = {
Pending: '待接单', InProgress: '进行中', WaitConfirm: '待确认',
Completed: '已完成', Cancelled: '已取消', Appealing: '申诉中'
}
return map[status] || status
},
getStatusClass(status) {
const map = {
Pending: 'status-pending', InProgress: 'status-progress',
WaitConfirm: 'status-confirm', Completed: 'status-done',
Cancelled: 'status-cancel', Appealing: 'status-appeal'
}
return map[status] || ''
},
formatTime(dateStr) {
if (!dateStr) return '-'
const d = new Date(dateStr)
const pad = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
},
previewImage(url) {
if (!url) return
uni.previewImage({ urls: [url] })
},
/** 取消订单 */
onCancelOrder() {
uni.showModal({
title: '取消订单',
content: '确定取消该订单?',
success: async (res) => {
if (!res.confirm) return
try {
await cancelOrder(this.orderId)
uni.showToast({ title: '订单已取消', icon: 'success' })
this.loadDetail()
} catch (e) {
// 错误已在 request 中处理
}
}
})
},
/** 跳转聊天页 */
goChat() {
uni.navigateTo({ url: `/pages/message/chat?orderId=${this.orderId}` })
},
/** 跳转确认处理页 */
goConfirm() {
uni.navigateTo({ url: `/pages/order/complete-order?id=${this.orderId}&mode=confirm` })
},
/** 打开评价弹窗 */
openReview() {
this.reviewForm = { rating: 5, content: '' }
this.showReviewModal = true
},
/** 提交评价 */
async submitReviewAction() {
if (this.reviewForm.rating < 1) {
uni.showToast({ title: '请选择评分', icon: 'none' })
return
}
this.reviewSubmitting = true
try {
await submitReview(this.orderId, {
rating: this.reviewForm.rating,
content: this.reviewForm.content
})
uni.showToast({ title: '评价成功', icon: 'success' })
this.showReviewModal = false
this.loadDetail()
} catch (e) {
// 错误已在 request 中处理
} finally {
this.reviewSubmitting = false
}
}
}
}
</script>
<style scoped>
/* 自定义导航栏 */
.custom-navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999;
background: #FFB700;
}
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20rpx;
}
.nav-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 40rpx;
height: 40rpx;
}
.navbar-title {
font-size: 34rpx;
font-weight: bold;
color: #363636;
}
.nav-placeholder {
width: 60rpx;
}
.detail-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 0 0 160rpx;
}
/* 页面标题 */
.page-title {
background-color: #ffffff;
padding: 24rpx 30rpx;
}
.page-title text {
font-size: 32rpx;
color: #333333;
font-weight: bold;
}
/* 状态栏 */
.status-bar {
padding: 20rpx 30rpx;
margin-bottom: 16rpx;
}
.status-bar.status-pending { background-color: #fff7e6; }
.status-bar.status-progress { background-color: #e6f7ff; }
.status-bar.status-confirm { background-color: #fff2e8; }
.status-bar.status-done { background-color: #f6ffed; }
.status-bar.status-cancel { background-color: #f5f5f5; }
.status-bar.status-appeal { background-color: #fff1f0; }
.status-text {
font-size: 30rpx;
font-weight: 500;
}
.status-pending .status-text { color: #faad14; }
.status-progress .status-text { color: #007AFF; }
.status-confirm .status-text { color: #ff9900; }
.status-done .status-text { color: #52c41a; }
.status-cancel .status-text { color: #999999; }
.status-appeal .status-text { color: #e64340; }
/* 信息区域 */
.info-section {
background-color: #ffffff;
margin: 0 24rpx 16rpx;
border-radius: 16rpx;
padding: 24rpx 30rpx;
}
.section-title {
padding-bottom: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
margin-bottom: 16rpx;
}
.section-title text {
font-size: 30rpx;
color: #333333;
font-weight: 500;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 10rpx 0;
}
.info-label {
font-size: 26rpx;
color: #999999;
flex-shrink: 0;
margin-right: 20rpx;
}
.info-value {
font-size: 26rpx;
color: #333333;
text-align: right;
flex: 1;
}
.info-value.price {
color: #e64340;
font-weight: bold;
}
/* 完成凭证 */
.proof-image {
width: 100%;
max-height: 400rpx;
border-radius: 8rpx;
}
/* 美食街门店分组 */
.shop-group {
margin: 16rpx 0;
padding: 16rpx;
background-color: #fafafa;
border-radius: 12rpx;
}
.shop-name {
font-size: 28rpx;
color: #333333;
font-weight: 500;
display: block;
margin-bottom: 12rpx;
}
.dish-item {
display: flex;
align-items: center;
padding: 10rpx 0;
}
.dish-photo {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
margin-right: 16rpx;
}
.dish-info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.dish-name {
font-size: 26rpx;
color: #333333;
}
.dish-price {
font-size: 24rpx;
color: #e64340;
}
/* 申诉处理结果 */
.appeal-item {
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.appeal-item:last-child {
border-bottom: none;
}
.appeal-result {
font-size: 26rpx;
color: #333333;
display: block;
margin-bottom: 8rpx;
}
.appeal-time {
font-size: 24rpx;
color: #999999;
}
/* 底部操作按钮 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 20rpx;
padding: 24rpx 30rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
background-color: #ffffff;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.action-btn {
flex: 1;
text-align: center;
padding: 24rpx 0;
border-radius: 12rpx;
font-size: 30rpx;
}
.btn-primary {
background-color: #007AFF;
}
.btn-primary text {
color: #ffffff;
}
.btn-secondary {
border: 1rpx solid #007AFF;
}
.btn-secondary text {
color: #007AFF;
}
.btn-cancel {
border: 1rpx solid #e64340;
}
.btn-cancel text {
color: #e64340;
}
/* 评价弹窗 */
.modal-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal-content {
width: 600rpx;
background-color: #ffffff;
border-radius: 20rpx;
padding: 40rpx;
}
.modal-title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
text-align: center;
display: block;
margin-bottom: 30rpx;
}
.star-row {
display: flex;
justify-content: center;
gap: 16rpx;
margin-bottom: 30rpx;
}
.star {
font-size: 48rpx;
color: #dddddd;
}
.star.active {
color: #faad14;
}
.review-input {
width: 100%;
height: 160rpx;
border: 1rpx solid #e0e0e0;
border-radius: 12rpx;
padding: 16rpx;
font-size: 28rpx;
box-sizing: border-box;
margin-bottom: 30rpx;
}
.modal-actions {
display: flex;
gap: 20rpx;
}
.modal-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
border-radius: 40rpx;
border: none;
}
.modal-btn.cancel {
background-color: #f5f5f5;
color: #666666;
}
.modal-btn.confirm {
background-color: #007AFF;
color: #ffffff;
}
</style>