598 lines
15 KiB
Vue
598 lines
15 KiB
Vue
<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>
|