Files
delivery-uniapp/pages/index/index.vue
2026-01-06 21:22:12 +08:00

481 lines
12 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.
<!-- 接单页将原首页替换为接单页面含订单卡片顶部 tab底部批量操作栏 -->
<template>
<view class="receive-page">
<!-- 顶部区域状态 + tabs -->
<view class="top-area" :style="headerStyle">
<view class="top-bg"></view>
<view class="top-inner">
<view class="user-info">
<image class="user-avatar" :src="driverInfo.avatar || defaultAvatar" mode="cover" />
<view class="user-meta">
<!-- <text class="user-name">{{ driverInfo.nickName || '骑手姓名' }}</text> -->
<text class="user-status" @click="toggleOnline">{{ driverInfo.isOnline ? '在线中' : '离线' }}</text>
</view>
</view>
<view class="tabs">
<view :class="['tab', activeTab === 'pickup' ? 'active' : '']" @click="switchTab('pickup')">
<text>待取货</text>
<text class="count">({{ pickupCount }})</text>
</view>
<view :class="['tab', activeTab === 'delivering' ? 'active' : '']" @click="switchTab('delivering')">
<text>配送中</text>
<text class="count">({{ deliveringCount }})</text>
</view>
</view>
</view>
</view>
<!-- 订单列表 -->
<scroll-view class="order-list" scroll-y="true" :style="{ height: listHeight + 'px' }">
<view v-for="order in filteredOrders" :key="order.id" class="order-card">
<!-- 头部编号 -->
<view class="order-header">
<view class="order-badge">{{ order.type === 'pickup' ? '取' : '送' }}</view>
<view class="order-title">
<text class="shop-name">{{ order.shopName }}</text>
<text class="order-id">#{{ order.id }}</text>
</view>
<view class="order-status">{{ order.statusText }}</view>
</view>
<!-- 地址信息 -->
<view class="order-info">
<view class="address-row">
<view class="icon pickup"></view>
<view class="address-content">
<text class="address-title">{{ order.pickupAddress }}</text>
<text class="address-sub">商家已出餐 · {{ order.pickupNote || '' }}</text>
</view>
<view class="nav-icon" @click="openMap(order.pickupLat, order.pickupLng, order.pickupAddress)">导航</view>
</view>
<view class="address-row">
<view class="icon deliver"></view>
<view class="address-content">
<text class="address-title">{{ order.deliveryAddress }}</text>
<text class="address-sub">收货人{{ order.receiverName }} {{ order.receiverPhone ? ('尾号' + (order.receiverPhone + '').slice(-4)) : ''}}</text>
</view>
<view class="nav-icon" @click="openMap(order.deliveryLat, order.deliveryLng, order.deliveryAddress)">导航</view>
</view>
</view>
<!-- 备注 -->
<view class="order-note" v-if="order.note">
<text>顾客{{ order.note }}</text>
</view>
<!-- 操作区 -->
<view class="order-actions">
<view class="contact" @click="callPhone(order.receiverPhone)">
<text>联系</text>
</view>
<view class="confirm" @click="confirmArrive(order.id)">
<text>确认到店</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部批量操作栏 -->
<view class="bottom-bar">
<view class="batch-item" @click="scanQr">扫一扫取单</view>
<view class="batch-item" @click="openManualInput">输入用户单编码</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import sheep from '@/sheep';
// 驿站/骑手信息(从 store 获取或 mock
const driverInfo = ref({
isOnline: true,
nickName: '骑手张三',
avatar: ''
});
const defaultAvatar = 'https://huichibao.oss-cn-guangzhou.aliyuncs.com/1/material/348b8223-8d03-46aa-8836-6757e8beebd2.png';
// 页面状态
const activeTab = ref('pickup'); // 'pickup' | 'delivering'
const listHeight = ref(600);
// Mock 订单数据(真实项目应从后端接口拉取 / store
const orders = ref([
{
id: 1001,
type: 'pickup',
statusText: '待取货',
shopName: '取货点店铺名称',
pickupAddress: '广东省广州市天河区学院站荷光路118-121号',
pickupLat: 23.1,
pickupLng: 113.3,
pickupNote: '商家已出餐',
deliveryAddress: '广东省广州市天河区华景新城软件园区',
deliveryLat: 23.12,
deliveryLng: 113.31,
receiverName: '张先生',
receiverPhone: '13900001234',
note: '根据餐量提供餐具'
},
{
id: 1002,
type: 'pickup',
statusText: '待取货',
shopName: '乐易购(学院店)',
pickupAddress: '广东省广州市天河区学院站荷光路118--121号',
pickupLat: 23.11,
pickupLng: 113.32,
pickupNote: '',
deliveryAddress: '广东省广州市天河区某小区',
deliveryLat: 23.13,
deliveryLng: 113.33,
receiverName: '李女士',
receiverPhone: '13900005678',
note: ''
}
]);
// 计算各 tab 数量与过滤列表
const pickupCount = computed(() => orders.value.filter(o => o.type === 'pickup').length);
const deliveringCount = computed(() => orders.value.filter(o => o.type === 'delivering').length);
const filteredOrders = computed(() => {
if (activeTab.value === 'pickup') {
return orders.value.filter(o => o.type === 'pickup');
}
return orders.value.filter(o => o.type === 'delivering');
});
// 切换 tab
function switchTab(tab) {
activeTab.value = tab;
}
// 切换上线/下线(简单 UI 切换,建议接入后端)
function toggleOnline() {
driverInfo.value.isOnline = !driverInfo.value.isOnline;
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast(driverInfo.value.isOnline ? '已上线' : '已下线');
}
// 确认到店(演示:改变订单状态)
function confirmArrive(orderId) {
const order = orders.value.find(o => o.id === orderId);
if (!order) return;
// 示例逻辑:到店后将类型改为 delivering
if (order.type === 'pickup') {
order.type = 'delivering';
order.statusText = '配送中';
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('已确认到店,开始配送');
}
}
// 拨打电话
function callPhone(phone) {
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到联系电话');
return;
}
uni.makePhoneCall({
phoneNumber: phone
});
}
// 打开地图导航(使用 openLocation 打开经纬度或直接跳转小程序地图)
function openMap(lat, lng, name) {
if (!lat || !lng) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('无法获取坐标');
return;
}
uni.openLocation({
latitude: Number(lat),
longitude: Number(lng),
name: name || '',
scale: 18
});
}
// 底部操作(扫码、手动输入)
function scanQr() {
uni.scanCode({
onlyFromCamera: false,
success(res) {
const code = res.result || res.path || '';
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('已识别:' + code);
},
fail() {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('扫码失败');
}
});
}
function openManualInput() {
uni.navigateTo({
url: '/pages/index/user' // 示例跳转,按需替换为真实手动输入页面
});
}
// 入口:计算列表高度适配底部栏
onMounted(() => {
try {
const sys = uni.getSystemInfoSync();
const windowHeight = sys.windowHeight || 667;
// 留出顶部和底部空间
listHeight.value = windowHeight - 200;
} catch (e) {
// ignore
}
});
// 顶部安全区处理:参考 pages/index/user.vue 的实现
import { onShow } from '@dcloudio/uni-app';
const headerStyle = ref({});
function setHeaderSafeArea() {
try {
const sys = uni.getSystemInfoSync();
const statusBarHeightPx = sys?.statusBarHeight || 0;
const windowWidth = sys?.windowWidth || 375;
// 将 px 转为 rpx rpx = px / windowWidth * 750
const statusBarHeightRpx = Math.round((statusBarHeightPx / windowWidth) * 750);
headerStyle.value = {
paddingTop: statusBarHeightPx + 'px',
'--statusbar': statusBarHeightRpx + 'rpx'
};
} catch (e) {
// ignore
}
}
onMounted(() => {
setHeaderSafeArea();
// 计算列表高度适配底部栏
try {
const sys = uni.getSystemInfoSync();
const windowHeight = sys.windowHeight || 667;
// 留出顶部和底部空间
listHeight.value = windowHeight - 200;
} catch (e) {
// ignore
}
});
onShow(() => {
// 每次页面显示时重新计算(兼容热更或状态变化)
setHeaderSafeArea();
});
</script>
<style scoped>
.receive-page {
background: #fff;
min-height: 100vh;
position: relative;
}
.top-area {
position: relative;
/* 兼容刘海屏安全区处理 */
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
height: 220rpx;
}
.top-bg {
position: absolute;
left: 0;
right: 0;
top: 0;
/* 背景高度需要包含安全区高度 */
height: 270rpx;
height: calc(270rpx + constant(safe-area-inset-top));
height: calc(270rpx + env(safe-area-inset-top));
height: calc(270rpx + var(--statusbar, 0rpx));
background: #c292ee;
border-bottom-left-radius: 12rpx;
border-bottom-right-radius: 12rpx;
z-index: 0;
}
.top-inner {
position: absolute;
left: 0;
right: 0;
top: calc(var(--statusbar, 0rpx) + 30rpx);
z-index: 1;
padding: 0 30rpx;
display: flex;
flex-direction: column;
}
.user-info {
display: flex;
flex-direction: row;
align-items: center;
}
.user-avatar {
width: 110rpx;
height: 110rpx;
border-radius: 55rpx;
border: 4rpx solid rgba(255,255,255,0.6);
}
.user-meta {
margin-left: 20rpx;
}
.user-name {
font-size: 32rpx;
color: #fff;
font-weight: 700;
}
.user-status {
margin-top: 8rpx;
color: rgba(255,255,255,0.9);
font-size: 26rpx;
}
.tabs {
margin-top: 18rpx;
display: flex;
flex-direction: row;
gap: 20rpx;
}
.tab {
padding: 10rpx 20rpx;
background: rgba(255,255,255,0.12);
border-radius: 40rpx;
color: #fff;
display: flex;
align-items: center;
}
.tab.active {
background: #fff;
color: #6b3aa6;
}
.tab .count {
margin-left: 8rpx;
}
.order-list {
padding: 20rpx;
padding-right: 20rpx;
box-sizing: border-box;
background: #f7f7f7;
}
.order-card {
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 18rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
overflow: hidden;
box-sizing: border-box;
max-width: 100%;
}
.order-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.order-badge {
width: 54rpx;
height: 54rpx;
border-radius: 27rpx;
background: #e6f7ff;
color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
}
.order-title {
flex: 1;
margin-left: 12rpx;
}
.shop-name {
font-size: 28rpx;
font-weight: 700;
}
.order-id {
color: #999;
margin-left: 8rpx;
}
.order-status {
color: #ff7a45;
}
.order-info {
margin-top: 8rpx;
}
.address-row {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 10rpx;
}
.icon {
width: 38rpx;
height: 38rpx;
border-radius: 19rpx;
background: #f2f2f2;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10rpx;
font-weight: 700;
}
.icon.pickup { background: #87d6ff; color: #fff; }
.icon.deliver { background: #ffd591; color: #fff; }
.address-content { flex: 1; min-width: 0; }
.address-title {
display: block;
font-size: 26rpx;
font-weight: 600;
/* 支持长地址换行,防止撑开布局 */
white-space: normal;
word-break: break-word;
}
.address-sub { display: block; font-size: 22rpx; color: #888; margin-top: 6rpx; }
.nav-icon { color: #1e9fff; padding: 6rpx 10rpx; }
.order-note { background: #f6f6f6; padding: 12rpx; border-radius: 8rpx; margin-top: 12rpx; color: #666; }
.order-actions {
display: flex;
flex-direction: row;
margin-top: 12rpx;
gap: 12rpx;
}
.contact {
flex: 1;
background: #fff;
border: 1rpx solid #ddd;
padding: 14rpx;
text-align: center;
border-radius: 8rpx;
color: #333;
}
.confirm {
flex: 2;
background: #1e9fff;
padding: 14rpx;
text-align: center;
border-radius: 8rpx;
color: #fff;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 110rpx;
background: rgba(255,255,255,0.98);
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
border-top: 1rpx solid #eee;
}
.batch-item {
background: #fff;
padding: 14rpx 20rpx;
border-radius: 40rpx;
border: 1rpx solid #ddd;
}
</style>