campus-errand/miniapp/pages/order-hall/order-hall.vue
18631081161 1fd330b233
Some checks reported errors
continuous-integration/drone/push Build encountered an error
下单优化
2026-04-03 15:40:09 +08:00

793 lines
18 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">
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<text class="navbar-title">接单</text>
</view>
</view>
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 顶部大图 -->
<image v-if="bannerUrl" class="top-banner" :src="bannerUrl" mode="aspectFill"></image>
<!-- 分类标签 + 排序 -->
<view class="tab-sort-bar" :style="{ top: (statusBarHeight + 44) + 'px' }">
<scroll-view class="tab-scroll" scroll-x>
<view class="tab-item" v-for="tab in tabs" :key="tab.value"
:class="{ active: currentTab === tab.value }" @click="switchTab(tab.value)">
<text class="tab-text">{{ tab.label }}</text>
<view v-if="currentTab === tab.value" class="tab-line"></view>
</view>
</scroll-view>
<view class="sort-area">
<picker :range="sortOptions" :range-key="'label'" :value="sortIndex" @change="onSortChange">
<view class="sort-picker">
<text class="sort-text">{{ sortOptions[sortIndex].label }}</text>
<text class="sort-arrow">▾</text>
</view>
</picker>
<image class="refresh-icon" src="/static/ic_refresh.png" mode="aspectFit" @click="onRefresh"></image>
</view>
</view>
<!-- 订单列表 -->
<view class="order-list">
<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">
<!-- 万能帮:特殊卡片布局 -->
<template v-if="order.orderType === 'Help'">
<view class="info-row remark-row">
<image class="remark-icon" src="/static/ic_modify.png" mode="aspectFit"></image>
<text class="info-text">备注信息:{{ order.itemName || '无' }}</text>
</view>
<view class="card-footer" style="margin-top: 20rpx;">
<view class="help-commission">
<text class="help-commission-label">跑腿费(含其他费用)</text>
<text class="commission-value">{{ getHelpTotalAmount(order) }}元</text>
</view>
<view class="footer-spacer"></view>
<button class="accept-btn" size="mini" @click="onAcceptClick(order)">接单</button>
</view>
</template>
<!-- 美食街:专属卡片布局 -->
<template v-else-if="order.orderType === 'Food'">
<!-- 标题行:门店数量 + 跑腿费 -->
<view class="card-title-row">
<text class="card-item-name">门店数量:{{ order.shopCount || 0 }}个</text>
<view class="card-commission">
<text class="commission-label">跑腿费:</text>
<text class="commission-value">{{ formatMoney(order.commission) }}元</text>
</view>
</view>
<!-- 美食数量 -->
<view class="food-info-row">
<text class="food-info-label">美食数量:</text>
<text class="food-info-value">{{ order.dishItemCount || 0 }}份</text>
</view>
<!-- 垫付商品金额 -->
<view class="food-info-row" v-if="order.goodsAmount">
<text class="food-info-label">垫付商品金额:</text>
<text class="food-info-price">¥{{ order.goodsAmount }}</text>
</view>
<!-- 送达地址 -->
<view class="info-row" v-if="order.deliveryLocation">
<view class="dot orange"></view>
<text class="info-text">送达地址:{{ order.deliveryLocation }}</text>
</view>
<!-- 查看详情按钮 -->
<view class="card-footer" style="margin-top: 16rpx;">
<view class="footer-spacer"></view>
<button class="accept-btn" size="mini" @click="viewFoodDetail(order)">查看详情</button>
</view>
</template>
<!-- 其他类型:默认卡片布局 -->
<template v-else>
<!-- 标题行:物品名 + 跑腿费 -->
<view class="card-title-row">
<text class="card-item-name">{{ getItemTitle(order) }}</text>
<view class="card-commission">
<text class="commission-label">跑腿费:</text>
<text class="commission-value">{{ formatMoney(order.commission) }}元</text>
</view>
</view>
<!-- 代购:垫付商品金额 -->
<view class="goods-amount-row" v-if="order.orderType === 'Purchase' && order.goodsAmount">
<text class="goods-amount-label">垫付商品金额:</text>
<text class="goods-amount-value">¥{{ order.goodsAmount }}</text>
</view>
<!-- 信息行 -->
<view class="card-body">
<view class="info-row" v-if="order.pickupLocation">
<view class="dot green"></view>
<text class="info-text">取货地址:{{ order.pickupLocation }}</text>
</view>
<view class="info-row" v-if="order.deliveryLocation">
<view class="dot orange"></view>
<text class="info-text">送达地址:{{ order.deliveryLocation }}</text>
</view>
<view class="info-row remark-row">
<image class="remark-icon" src="/static/ic_modify.png" mode="aspectFit"></image>
<text class="info-text">备注信息:{{ order.remark || '无' }}</text>
</view>
</view>
<!-- 接单按钮 -->
<view class="card-footer">
<view class="footer-spacer"></view>
<button class="accept-btn" size="mini" @click="onAcceptClick(order)">接单</button>
</view>
</template>
</view>
</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,
getPageBanner,
getServiceEntries
} from '../../utils/api'
export default {
data() {
return {
statusBarHeight: 0,
bannerUrl: '',
tabs: [{
label: '代取',
value: 'Pickup'
},
{
label: '代送',
value: 'Delivery'
},
{
label: '万能帮',
value: 'Help'
},
{
label: '代购',
value: 'Purchase'
},
{
label: '美食街',
value: 'Food'
}
],
currentTab: '',
sortOptions: [{
label: '按时间排序',
value: 'time'
},
{
label: '佣金优先',
value: 'commission'
}
],
sortIndex: 0,
orders: [],
loading: false,
showAcceptModal: false,
acceptingOrder: null,
accepting: false,
showCertModal: false,
certForm: {
realName: '',
phone: ''
},
certSubmitting: false,
certStatus: null
}
},
async onShow() {
const sysInfo = uni.getSystemInfoSync()
this.statusBarHeight = sysInfo.statusBarHeight || 0
this.certStatus = null
this.loadBanner()
await this.loadTabs()
this.loadOrders()
},
methods: {
/** 从服务入口获取标签排序 */
async loadTabs() {
try {
const entries = await getServiceEntries()
if (entries && entries.length > 0) {
const nameMap = { '代取': 'Pickup', '代送': 'Delivery', '万能帮': 'Help', '代购': 'Purchase', '美食街': 'Food' }
const sorted = entries
.filter(e => nameMap[e.name])
.map(e => ({ label: e.name, value: nameMap[e.name] }))
if (sorted.length > 0) {
this.tabs = sorted
// 只在首次或当前tab不在列表中时重置
if (!this.currentTab || !sorted.find(t => t.value === this.currentTab)) {
this.currentTab = sorted[0].value
}
}
}
} catch (e) {}
},
/** 加载顶部大图 */
async loadBanner() {
try {
const res = await getPageBanner('order-hall')
if (res?.value) this.bannerUrl = res.value
} catch (e) {}
},
/** 加载订单列表 */
async loadOrders() {
this.loading = true
try {
const sortValue = this.sortOptions[this.sortIndex].value
const params = {
type: this.currentTab,
sort: sortValue
}
const res = await getOrderHall(params)
this.orders = res || []
} catch (e) {
this.orders = []
} finally {
this.loading = false
}
},
switchTab(value) {
this.currentTab = value
this.loadOrders()
},
onSortChange(e) {
this.sortIndex = e.detail.value
this.loadOrders()
},
onRefresh() {
this.loadOrders()
},
loadMore() {},
/** 获取万能帮总费用(佣金+商品金额) */
getHelpTotalAmount(order) {
const commission = parseFloat(order.commission) || 0
const goodsAmount = parseFloat(order.goodsAmount) || 0
return (commission + goodsAmount).toFixed(2)
},
/** 格式化金额为2位小数 */
formatMoney(val) {
return (parseFloat(val) || 0).toFixed(2)
},
/** 获取卡片标题 */
getItemTitle(order) {
const typeMap = {
Pickup: '代取物品',
Delivery: '代送物品',
Help: '万能帮',
Purchase: '代购物品',
Food: '美食街'
}
const prefix = typeMap[order.orderType] || ''
if (order.orderType === 'Food') return `美食街订单(${order.shopCount || 0}家)`
if (order.orderType === 'Help') return `万能帮:${order.itemName || ''}`
return `${prefix}${order.itemName || ''}`
},
viewFoodDetail(order) {
uni.navigateTo({
url: `/pages/food/food-order-detail?id=${order.id}`
})
},
/** 点击接单 */
async onAcceptClick(order) {
// 未登录跳转登录页
const token = uni.getStorageSync('token')
if (!token) {
uni.navigateTo({ url: '/pages/login/login' })
return
}
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) {} 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) {} finally {
this.certSubmitting = false
}
}
}
}
</script>
<style scoped>
.hall-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 40rpx;
}
/* 自定义导航栏 */
.custom-navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999;
background: linear-gradient(to right, #FFB700, #FFD59B);
}
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.navbar-title {
font-size: 34rpx;
font-weight: bold;
color: #363636;
}
/* 顶部大图 */
.top-banner {
width: 100%;
height: 374rpx;
}
/* 分类标签 + 排序栏 */
.tab-sort-bar {
display: flex;
align-items: center;
padding: 0 20rpx;
position: sticky;
top: 0;
z-index: 99;
background-color: #f5f5f5;
}
.tab-scroll {
flex: 1;
white-space: nowrap;
min-width: 0;
}
.tab-item {
display: inline-flex;
flex-direction: column;
align-items: center;
padding: 24rpx 20rpx 16rpx;
position: relative;
}
.tab-text {
font-size: 35rpx;
font-weight: 600;
color: #585858;
}
.tab-item.active .tab-text {
color: #FF8C00;
font-weight: bold;
}
.tab-line {
width: 40rpx;
height: 6rpx;
border-radius: 3rpx;
background: #FF8C00;
margin-top: 8rpx;
}
.sort-area {
display: flex;
align-items: center;
flex-shrink: 0;
margin-left: 12rpx;
gap: 16rpx;
}
.sort-picker {
display: flex;
align-items: center;
border-radius: 10rpx;
background-color: #FFFFFF;
padding: 8rpx 20rpx;
white-space: nowrap;
}
.sort-text {
font-size: 24rpx;
color: #666;
}
.sort-arrow {
font-size: 20rpx;
color: #999;
margin-left: 4rpx;
}
.refresh-icon {
width: 40rpx;
height: 40rpx;
flex-shrink: 0;
}
/* 订单列表 */
.order-list {
padding: 16rpx 0;
}
.loading-tip,
.empty-tip {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 80rpx 0;
}
/* 订单卡片 */
.order-card {
background: #fff;
border-radius: 20rpx;
padding: 26rpx;
width: 694rpx;
margin: 0 auto 20rpx;
box-sizing: border-box;
}
.card-title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
}
/* 代购垫付金额 */
.goods-amount-row {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.goods-amount-label {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.goods-amount-value {
font-size: 30rpx;
color: #FF6B00;
font-weight: bold;
}
.card-item-name {
font-size: 30rpx;
color: #333;
font-weight: bold;
flex: 1;
margin-right: 16rpx;
}
.card-commission {
display: flex;
align-items: center;
flex-shrink: 0;
}
.commission-label {
font-size: 24rpx;
color: #FF6B00;
}
.commission-value {
font-size: 34rpx;
color: #FF6B00;
font-weight: bold;
}
/* 万能帮跑腿费 */
.help-commission {
display: flex;
align-items: center;
}
.help-commission-label {
font-size: 26rpx;
color: #333;
}
/* 美食街卡片信息行 */
.food-info-row {
display: flex;
align-items: center;
padding: 6rpx 0;
}
.food-info-label {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.food-info-value {
font-size: 28rpx;
color: #333;
}
.food-info-price {
font-size: 30rpx;
color: #FF6B00;
font-weight: bold;
}
/* 信息行 */
.card-body {
margin-bottom: 20rpx;
}
.info-row {
display: flex;
align-items: center;
padding: 8rpx 0;
}
.dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin-right: 16rpx;
flex-shrink: 0;
}
.dot.green {
background: #4CAF50;
}
.dot.orange {
background: #FFB700;
}
.remark-row {
margin-top: 4rpx;
}
.remark-icon {
width: 28rpx;
height: 28rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.info-text {
font-size: 28rpx;
color: #363636;
}
/* 卡片底部 */
.card-footer {
display: flex;
justify-content: flex-end;
}
.footer-spacer {
flex: 1;
}
.accept-btn {
background: linear-gradient(135deg, #FFB700, #FF9500);
color: #fff;
font-size: 28rpx;
font-weight: bold;
border-radius: 36rpx;
padding: 0 48rpx;
height: 68rpx;
line-height: 68rpx;
border: none;
}
.accept-btn::after {
border: none;
}
/* 弹窗 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal-content {
width: 600rpx;
background: #fff;
border-radius: 20rpx;
padding: 40rpx;
}
.modal-title {
font-size: 34rpx;
font-weight: bold;
color: #333;
text-align: center;
display: block;
margin-bottom: 20rpx;
}
.modal-desc {
font-size: 28rpx;
color: #666;
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: #f5f5f5;
color: #666;
}
.modal-btn.cancel::after {
border: none;
}
.modal-btn.confirm::after {
border: none;
}
.modal-btn.confirm {
background: linear-gradient(135deg, #FFB700, #FF9500);
color: #fff;
}
.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: #333;
margin-bottom: 8rpx;
display: block;
}
.cert-form .form-input {
font-size: 28rpx;
color: #333;
height: 64rpx;
}
</style>