campus-errand/miniapp/pages/order-hall/order-hall.vue
2026-03-12 18:12:10 +08:00

598 lines
15 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="hall-page">
<!-- 分类标签切换 -->
<scroll-view class="tab-bar" scroll-x>
<view
class="tab-item"
v-for="tab in tabs"
:key="tab.value"
:class="{ active: currentTab === tab.value }"
@click="switchTab(tab.value)"
>
{{ tab.label }}
</view>
</scroll-view>
<!-- 排序切换 + 刷新按钮 -->
<view class="toolbar">
<view class="sort-group">
<view
class="sort-btn"
:class="{ active: sortMode === 'commission' }"
@click="switchSort('commission')"
>佣金优先</view>
<view
class="sort-btn"
:class="{ active: sortMode === 'distance' }"
@click="switchSort('distance')"
>距离优先</view>
</view>
<view class="refresh-btn" @click="onRefresh">
<text class="refresh-icon">⟳</text>
</view>
</view>
<!-- 订单列表 -->
<scroll-view class="order-list" scroll-y @scrolltolower="loadMore">
<view v-if="loading && orders.length === 0" class="loading-tip">加载中...</view>
<view v-else-if="orders.length === 0" class="empty-tip">暂无订单</view>
<view
v-for="order in orders"
:key="order.id"
class="order-card"
>
<!-- 订单类型标签 -->
<view class="card-header">
<text class="order-type-tag">{{ getTypeLabel(order.orderType) }}</text>
<text class="order-time">{{ formatTime(order.createdAt) }}</text>
</view>
<!-- 代取订单展示 -->
<view v-if="order.orderType === 'Pickup'" class="card-body">
<view class="info-row"><text class="info-label">代取物品</text><text class="info-value">{{ order.itemName }}</text></view>
<view class="info-row"><text class="info-label">取货地点</text><text class="info-value">{{ order.pickupLocation }}</text></view>
<view class="info-row"><text class="info-label">送达地点</text><text class="info-value">{{ order.deliveryLocation }}</text></view>
<view v-if="order.remark" class="info-row"><text class="info-label">备注</text><text class="info-value">{{ order.remark }}</text></view>
</view>
<!-- 代送订单展示 -->
<view v-else-if="order.orderType === 'Delivery'" class="card-body">
<view class="info-row"><text class="info-label">送货物品</text><text class="info-value">{{ order.itemName }}</text></view>
<view class="info-row"><text class="info-label">取货地点</text><text class="info-value">{{ order.pickupLocation }}</text></view>
<view class="info-row"><text class="info-label">送达地点</text><text class="info-value">{{ order.deliveryLocation }}</text></view>
<view v-if="order.remark" class="info-row"><text class="info-label">备注</text><text class="info-value">{{ order.remark }}</text></view>
</view>
<!-- 万能帮订单展示 -->
<view v-else-if="order.orderType === 'Help'" class="card-body">
<view v-if="order.remark" class="info-row"><text class="info-label">备注</text><text class="info-value">{{ order.remark }}</text></view>
</view>
<!-- 代购订单展示 -->
<view v-else-if="order.orderType === 'Purchase'" class="card-body">
<view class="info-row"><text class="info-label">代购物品</text><text class="info-value">{{ order.itemName }}</text></view>
<view class="info-row"><text class="info-label">买货地点</text><text class="info-value">{{ order.pickupLocation }}</text></view>
<view class="info-row"><text class="info-label">送达地点</text><text class="info-value">{{ order.deliveryLocation }}</text></view>
<view v-if="order.remark" class="info-row"><text class="info-label">备注</text><text class="info-value">{{ order.remark }}</text></view>
<view class="info-row"><text class="info-label">垫付金额</text><text class="info-value price">¥{{ order.goodsAmount }}</text></view>
</view>
<!-- 美食街订单展示 -->
<view v-else-if="order.orderType === 'Food'" class="card-body">
<view class="info-row"><text class="info-label">门店数量</text><text class="info-value">{{ order.shopCount }}家</text></view>
<view class="info-row"><text class="info-label">菜品数量</text><text class="info-value">{{ order.dishItemCount }}份</text></view>
<view class="info-row"><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.goodsAmount }}</text></view>
</view>
<!-- 底部:佣金 + 操作按钮 -->
<view class="card-footer">
<text class="commission">跑腿费 ¥{{ order.commission }}</text>
<!-- 美食街显示查看详情,其他显示接单 -->
<button
v-if="order.orderType === 'Food'"
class="action-btn detail-btn"
size="mini"
@click="viewFoodDetail(order)"
>查看详情</button>
<button
v-else
class="action-btn accept-btn"
size="mini"
@click="onAcceptClick(order)"
>接单</button>
</view>
</view>
</scroll-view>
<!-- 接单确认弹窗 -->
<view class="modal-mask" v-if="showAcceptModal" @click="showAcceptModal = false">
<view class="modal-content" @click.stop>
<text class="modal-title">确认接单</text>
<text class="modal-desc">确认接取该订单?接单后请尽快完成。</text>
<view class="modal-actions">
<button class="modal-btn cancel" @click="showAcceptModal = false">取消</button>
<button class="modal-btn confirm" @click="confirmAccept" :loading="accepting">接单</button>
</view>
</view>
</view>
<!-- 跑腿认证弹窗 -->
<view class="modal-mask" v-if="showCertModal" @click="showCertModal = false">
<view class="modal-content" @click.stop>
<text class="modal-title">跑腿认证</text>
<text class="modal-desc">接单前需完成跑腿认证</text>
<view class="cert-form">
<view class="form-item">
<text class="form-label">姓名</text>
<input class="form-input" v-model="certForm.realName" placeholder="请输入真实姓名" />
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<input class="form-input" v-model="certForm.phone" type="number" placeholder="请输入手机号" maxlength="11" />
</view>
</view>
<view class="modal-actions">
<button class="modal-btn cancel" @click="showCertModal = false">取消</button>
<button class="modal-btn confirm" @click="submitCert" :loading="certSubmitting">提交</button>
</view>
</view>
</view>
</view>
</template>
<script>
import { getOrderHall, acceptOrder, getCertificationStatus, submitCertification } from '../../utils/api'
export default {
data() {
return {
// 分类标签
tabs: [
{ label: '代取', value: 'Pickup' },
{ label: '代送', value: 'Delivery' },
{ label: '万能帮', value: 'Help' },
{ label: '代购', value: 'Purchase' },
{ label: '美食街', value: 'Food' }
],
currentTab: 'Pickup',
// 排序方式,默认佣金优先
sortMode: 'commission',
// 订单列表
orders: [],
loading: false,
// 接单弹窗
showAcceptModal: false,
acceptingOrder: null,
accepting: false,
// 跑腿认证弹窗
showCertModal: false,
certForm: { realName: '', phone: '' },
certSubmitting: false,
// 认证状态缓存
certStatus: null,
// 定位信息
userLocation: null
}
},
onShow() {
this.requestLocation()
this.loadOrders()
},
methods: {
/** 申请定位权限 */
requestLocation() {
uni.getLocation({
type: 'gcj02',
success: (res) => {
this.userLocation = { latitude: res.latitude, longitude: res.longitude }
},
fail: () => {
// 定位失败不阻塞,距离排序不可用
}
})
},
/** 加载订单列表 */
async loadOrders() {
this.loading = true
try {
const params = {
orderType: this.currentTab,
sort: this.sortMode
}
// 距离排序时传入用户位置
if (this.sortMode === 'distance' && this.userLocation) {
params.latitude = this.userLocation.latitude
params.longitude = this.userLocation.longitude
}
const res = await getOrderHall(params)
this.orders = res || []
} catch (e) {
this.orders = []
} finally {
this.loading = false
}
},
/** 切换分类标签 */
switchTab(value) {
this.currentTab = value
this.loadOrders()
},
/** 切换排序方式 */
switchSort(mode) {
if (this.sortMode === mode) return
this.sortMode = mode
this.loadOrders()
},
/** 刷新列表 */
onRefresh() {
this.loadOrders()
},
/** 加载更多(预留分页) */
loadMore() {
// 预留分页逻辑
},
/** 获取订单类型中文标签 */
getTypeLabel(type) {
const map = { Pickup: '代取', Delivery: '代送', Help: '万能帮', Purchase: '代购', Food: '美食街' }
return map[type] || type
},
/** 格式化时间 */
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())}`
},
/** 查看美食街订单详情 */
viewFoodDetail(order) {
uni.navigateTo({
url: `/pages/food/food-order-detail?id=${order.id}`
})
},
/** 点击接单按钮 */
async onAcceptClick(order) {
// 先检查跑腿认证状态
try {
if (this.certStatus === null) {
const res = await getCertificationStatus()
this.certStatus = res?.status || null
}
if (this.certStatus === 'Approved') {
// 已认证,弹出接单确认弹窗
this.acceptingOrder = order
this.showAcceptModal = true
} else if (this.certStatus === 'Pending') {
// 审核中
uni.showToast({ title: '平台审核中', icon: 'none' })
} else {
// 未认证,弹出认证弹窗
this.certForm = { realName: '', phone: '' }
this.showCertModal = true
}
} catch (e) {
// 未认证,弹出认证弹窗
this.certForm = { realName: '', phone: '' }
this.showCertModal = true
}
},
/** 确认接单 */
async confirmAccept() {
if (!this.acceptingOrder) return
this.accepting = true
try {
await acceptOrder(this.acceptingOrder.id)
uni.showToast({ title: '接单成功', icon: 'success' })
this.showAcceptModal = false
this.acceptingOrder = null
// 刷新列表
this.loadOrders()
} catch (e) {
// 并发接单提示
// 错误已在 request 中处理409 会显示"该订单已被接取"
} finally {
this.accepting = false
}
},
/** 提交跑腿认证 */
async submitCert() {
if (!this.certForm.realName.trim()) {
uni.showToast({ title: '请输入姓名', icon: 'none' })
return
}
if (!this.certForm.phone.trim()) {
uni.showToast({ title: '请输入手机号', icon: 'none' })
return
}
this.certSubmitting = true
try {
await submitCertification({
realName: this.certForm.realName.trim(),
phone: this.certForm.phone.trim()
})
this.certStatus = 'Pending'
this.showCertModal = false
uni.showToast({ title: '认证已提交,等待审核', icon: 'none' })
} catch (e) {
// 错误已在 request 中处理
} finally {
this.certSubmitting = false
}
}
}
}
</script>
<style scoped>
.hall-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 分类标签栏 */
.tab-bar {
white-space: nowrap;
background-color: #ffffff;
padding: 16rpx 24rpx;
flex-shrink: 0;
}
.tab-item {
display: inline-block;
padding: 12rpx 32rpx;
margin-right: 16rpx;
font-size: 28rpx;
color: #666666;
border-radius: 32rpx;
background-color: #f5f5f5;
}
.tab-item.active {
background-color: #007AFF;
color: #ffffff;
}
/* 工具栏 */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 24rpx;
background-color: #ffffff;
border-top: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.sort-group {
display: flex;
gap: 16rpx;
}
.sort-btn {
font-size: 26rpx;
color: #999999;
padding: 8rpx 20rpx;
border-radius: 24rpx;
border: 1rpx solid #e0e0e0;
}
.sort-btn.active {
color: #007AFF;
border-color: #007AFF;
background-color: rgba(0, 122, 255, 0.05);
}
.refresh-btn {
padding: 8rpx 16rpx;
}
.refresh-icon {
font-size: 40rpx;
color: #007AFF;
}
/* 订单列表 */
.order-list {
flex: 1;
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: #007AFF;
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
.order-time {
font-size: 24rpx;
color: #999999;
}
.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: space-between;
align-items: center;
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
}
.commission {
font-size: 30rpx;
color: #e64340;
font-weight: bold;
}
.action-btn {
font-size: 26rpx;
border-radius: 32rpx;
padding: 0 32rpx;
height: 60rpx;
line-height: 60rpx;
border: none;
}
.accept-btn {
background-color: #007AFF;
color: #ffffff;
}
.detail-btn {
background-color: #ffffff;
color: #007AFF;
border: 1rpx solid #007AFF;
}
/* 弹窗 */
.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: 20rpx;
}
.modal-desc {
font-size: 28rpx;
color: #666666;
text-align: center;
display: block;
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;
}
/* 认证表单 */
.cert-form {
margin-bottom: 20rpx;
}
.cert-form .form-item {
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.cert-form .form-label {
font-size: 28rpx;
color: #333333;
margin-bottom: 8rpx;
display: block;
}
.cert-form .form-input {
font-size: 28rpx;
color: #333333;
height: 64rpx;
}
</style>