campus-errand/miniapp/pages/order/my-orders.vue
18631081161 681d2b5fe8
All checks were successful
continuous-integration/drone/push Build is passing
提现
2026-04-02 16:55:18 +08:00

700 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="my-orders-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="filter-sticky" :style="{ top: (statusBarHeight + 44) + 'px' }">
<!-- 状态筛选标签 -->
<view class="tab-bar">
<scroll-view class="tab-scroll" scroll-x>
<view
class="tab-item"
v-for="tab in statusTabs"
:key="tab.value"
:class="{ active: currentStatus === tab.value }"
@click="switchStatus(tab.value)"
>{{ tab.label }}</view>
</scroll-view>
</view>
<!-- 类型筛选标签 -->
<view class="type-bar">
<scroll-view class="type-scroll" scroll-x>
<view
class="type-item"
v-for="tab in typeTabs"
:key="tab.value"
:class="{ active: currentType === tab.value }"
@click="switchType(tab.value)"
>{{ tab.label }}</view>
</scroll-view>
</view>
</view>
<!-- 订单列表 -->
<view class="order-list">
<view v-if="loading" class="loading-tip">加载中...</view>
<view v-else-if="filteredOrders.length === 0" class="empty-tip">暂无订单</view>
<view
v-for="order in filteredOrders"
:key="order.id"
class="order-card"
>
<view class="card-header">
<text class="order-type-tag">{{ getTypeLabel(order.orderType) }}</text>
<text class="order-status" :class="getStatusClass(order.status)">{{ getStatusLabel(order.status) }}</text>
</view>
<view class="card-body">
<view class="info-row">
<text class="info-label">订单编号</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.deliveryLocation">
<text class="info-label">送达地点</text>
<text class="info-value">{{ order.deliveryLocation }}</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">{{ formatTime(order.createdAt) }}</text>
</view>
</view>
<!-- 操作按钮区域 -->
<view class="card-footer">
<!-- 待接单:取消订单 + 查看详情 -->
<template v-if="order.status === 'Pending'">
<view class="btn btn-cancel" @click="onCancelOrder(order)">
<text>取消订单</text>
</view>
<view class="btn btn-detail" @click="goDetail(order)">
<text>查看详情</text>
</view>
</template>
<!-- 进行中:联系跑腿 + 查看详情 -->
<template v-else-if="order.status === 'InProgress'">
<view class="btn btn-contact" @click="goChat(order)">
<text>联系跑腿</text>
</view>
<view class="btn btn-detail" @click="goDetail(order)">
<text>查看详情</text>
</view>
</template>
<!-- 待确认:确认完成 + 订单未完成 + 查看详情 -->
<template v-else-if="order.status === 'WaitConfirm'">
<view class="btn btn-cancel" @click="onRejectComplete(order)">
<text>订单未完成</text>
</view>
<view class="btn btn-primary" @click="onConfirmComplete(order)">
<text>确认订单完成</text>
</view>
<view class="btn btn-detail" @click="goDetail(order)">
<text>查看详情</text>
</view>
</template>
<!-- 已完成:评价跑腿 + 查看详情 + 联系跑腿 -->
<template v-else-if="order.status === 'Completed'">
<view v-if="!order.isReviewed" class="btn btn-primary" @click="openReview(order)">
<text>评价跑腿</text>
</view>
<view class="btn btn-contact" @click="goChat(order)">
<text>联系跑腿</text>
</view>
<view class="btn btn-detail" @click="goDetail(order)">
<text>查看详情</text>
</view>
</template>
<!-- 已取消/申诉中:查看详情 -->
<template v-else>
<view class="btn btn-detail" @click="goDetail(order)">
<text>查看详情</text>
</view>
</template>
</view>
</view>
</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 { getMyOrders, cancelOrder, submitReview, confirmOrder, rejectOrder } from '../../utils/api'
import { initIM, sendCustomMessage } from '../../utils/im'
export default {
data() {
return {
statusTabs: [
{ label: '全部', value: '' },
{ label: '待接单', value: 'Pending' },
{ label: '进行中', value: 'InProgress' },
{ label: '待确认', value: 'WaitConfirm' },
{ label: '已完成', value: 'Completed' },
{ label: '已取消', value: 'Cancelled' },
{ label: '申诉中', value: 'Appealing' }
],
typeTabs: [
{ label: '全部', value: '' },
{ label: '代取', value: 'Pickup' },
{ label: '代送', value: 'Delivery' },
{ label: '万能帮', value: 'Help' },
{ label: '代购', value: 'Purchase' },
{ label: '美食街', value: 'Food' }
],
currentStatus: '',
currentType: '',
orders: [],
loading: false,
// 评价弹窗
showReviewModal: false,
reviewingOrder: null,
reviewForm: { rating: 5, content: '' },
reviewSubmitting: false,
statusBarHeight: 0
}
},
computed: {
/** 按状态和类型过滤订单 */
filteredOrders() {
return this.orders.filter(o => {
if (this.currentStatus) {
const statuses = this.currentStatus.split(',')
if (!statuses.includes(o.status)) return false
}
if (this.currentType && o.orderType !== this.currentType) return false
return true
})
}
},
onLoad(options) {
const sysInfo = uni.getSystemInfoSync()
this.statusBarHeight = sysInfo.statusBarHeight || 0
if (options.status) {
this.currentStatus = options.status
}
},
onShow() {
this.loadOrders()
},
methods: {
goBack() { uni.navigateBack() },
/** 加载我的订单 */
async loadOrders() {
this.loading = true
try {
const res = await getMyOrders({})
this.orders = res?.items || res || []
} catch (e) {
this.orders = []
} finally {
this.loading = false
}
},
switchStatus(value) {
this.currentStatus = value
},
switchType(value) {
this.currentType = value
},
getTypeLabel(type) {
const map = { Pickup: '代取', Delivery: '代送', Help: '万能帮', Purchase: '代购', Food: '美食街' }
return map[type] || type
},
getStatusLabel(status) {
const map = {
Unpaid: '待支付', 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(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : 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())}`
},
/** 取消订单 */
onCancelOrder(order) {
uni.showModal({
title: '取消订单',
content: '确定取消该订单?',
success: async (res) => {
if (!res.confirm) return
try {
await cancelOrder(order.id)
uni.showToast({ title: '订单已取消', icon: 'success' })
this.loadOrders()
} catch (e) {
// 错误已在 request 中处理
}
}
})
},
/** 跳转订单详情 */
goDetail(order) {
uni.navigateTo({ url: `/pages/order/order-detail?id=${order.id}` })
},
/** 跳转聊天页 */
goChat(order) {
uni.navigateTo({ url: `/pages/message/chat?orderId=${order.id}` })
},
/** 单主确认订单完成 */
onConfirmComplete(order) {
uni.showModal({
title: '确认完成',
content: '确认该订单已完成?',
success: async (res) => {
if (!res.confirm) return
try {
await confirmOrder(order.id)
uni.showToast({ title: '订单已完成', icon: 'success' })
this.loadOrders()
this.sendOrderStatusIM(order.runnerId, order.id, '单主已确认,订单已完成')
} catch (e) {}
}
})
},
/** 单主拒绝订单完成 */
onRejectComplete(order) {
uni.showModal({
title: '拒绝完成',
content: '确认该订单未完成?订单将继续进行。',
success: async (res) => {
if (!res.confirm) return
try {
await rejectOrder(order.id)
uni.showToast({ title: '已拒绝,订单继续进行', icon: 'none' })
this.loadOrders()
this.sendOrderStatusIM(order.runnerId, order.id, '单主已拒绝完成,订单继续进行')
} catch (e) {}
}
})
},
/** 发送订单状态变更 IM 通知(异步,不阻塞主流程) */
async sendOrderStatusIM(targetUserId, orderId, description) {
if (!targetUserId) return
try {
await initIM()
await sendCustomMessage(`user_${targetUserId}`, {
bizType: 'order-status',
description
}, orderId)
} catch (e) {
console.error('[IM] 发送状态通知失败:', e)
}
},
/** 打开评价弹窗 */
openReview(order) {
this.reviewingOrder = order
this.reviewForm = { rating: 5, content: '' }
this.showReviewModal = true
},
/** 提交评价 */
async submitReviewAction() {
if (!this.reviewingOrder) return
if (this.reviewForm.rating < 1) {
uni.showToast({ title: '请选择评分', icon: 'none' })
return
}
this.reviewSubmitting = true
try {
await submitReview(this.reviewingOrder.id, {
rating: this.reviewForm.rating,
content: this.reviewForm.content
})
uni.showToast({ title: '评价成功', icon: 'success' })
this.showReviewModal = false
// 刷新列表,隐藏评价按钮
this.loadOrders()
} 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;
}
.my-orders-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 40rpx;
}
/* 筛选栏吸顶容器 */
.filter-sticky {
position: sticky;
top: 0;
z-index: 99;
background-color: #ffffff;
}
/* 状态筛选栏 */
.tab-bar {
padding: 16rpx 24rpx;
}
.tab-scroll {
white-space: nowrap;
}
.tab-item {
display: inline-block;
padding: 10rpx 24rpx;
margin-right: 12rpx;
font-size: 26rpx;
color: #666666;
border-radius: 28rpx;
background-color: #f5f5f5;
}
.tab-item.active {
background-color: #FFB700;
color: #ffffff;
}
/* 类型筛选栏 */
.type-bar {
padding: 12rpx 24rpx;
border-top: 1rpx solid #f0f0f0;
}
.type-scroll {
white-space: nowrap;
}
.type-item {
display: inline-block;
padding: 8rpx 20rpx;
margin-right: 12rpx;
font-size: 24rpx;
color: #999999;
border-radius: 24rpx;
border: 1rpx solid #e0e0e0;
}
.type-item.active {
color: #FFB700;
border-color: #FFB700;
}
/* 订单列表 */
.order-list {
padding: 16rpx 24rpx;
}
.loading-tip, .empty-tip {
text-align: center;
color: #999999;
font-size: 28rpx;
padding: 80rpx 0;
}
/* 订单卡片 */
.order-card {
background-color: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.order-type-tag {
font-size: 24rpx;
color: #ffffff;
background-color: #FFB700;
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
.order-status {
font-size: 24rpx;
font-weight: 500;
}
.status-pending { color: #faad14; }
.status-progress { color: #FFB700; }
.status-confirm { color: #ff9900; }
.status-done { color: #52c41a; }
.status-cancel { color: #999999; }
.status-appeal { color: #e64340; }
.card-body {
margin-bottom: 16rpx;
}
.info-row {
display: flex;
padding: 6rpx 0;
}
.info-label {
font-size: 26rpx;
color: #999999;
width: 140rpx;
flex-shrink: 0;
}
.info-value {
font-size: 26rpx;
color: #333333;
flex: 1;
}
.info-value.price {
color: #e64340;
font-weight: bold;
}
/* 操作按钮 */
.card-footer {
display: flex;
justify-content: flex-end;
gap: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
flex-wrap: wrap;
}
.btn {
padding: 12rpx 28rpx;
border-radius: 32rpx;
font-size: 26rpx;
}
.btn text {
font-size: 26rpx;
}
.btn-primary {
background-color: #FAD146;
}
.btn-primary text {
color: #333333;
}
.btn-detail {
border: 1rpx solid #FFB700;
}
.btn-detail text {
color: #FFB700;
}
.btn-cancel {
border: 1rpx solid #e64340;
}
.btn-cancel text {
color: #e64340;
}
.btn-contact {
border: 1rpx solid #52c41a;
}
.btn-contact text {
color: #52c41a;
}
/* 评价弹窗 */
.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::after {
border: none;
}
.modal-btn.cancel {
background-color: #f5f5f5;
color: #666666;
}
.modal-btn.confirm {
background-color: #FAD146;
color: #333333;
}
</style>