feat: 引入uView Plus组件库,新增部分静态页面

This commit is contained in:
admin
2026-01-17 16:55:19 +08:00
parent 34c63780a8
commit 849647d3c9
562 changed files with 73370 additions and 121 deletions

619
pages/order/detail.vue Normal file
View File

@@ -0,0 +1,619 @@
<template>
<s-layout title="订单详情" class="set-userinfo-wrap">
<view class="order-detail-page">
<!-- 地图占位可替换为原生 map 组件或第三方地图组件 -->
<view class="map-area">
<!-- 真实项目建议使用 <map> 并渲染 polyline/markers -->
<image class="map-image" src="/static/img/map-placeholder.png" mode="widthFix" v-if="!mapAvailable" />
<map v-else class="map-native" :latitude="order?.pickupLat" :longitude="order?.pickupLng" show-location enable-3D
enable-zoom :scale="16"></map>
<view class="map-overlay">
<view class="eta">距离商家{{ distanceText }}预计{{ etaText }}到达</view>
<view class="nav-btn" @click="navigateToShop">导航到商家</view>
</view>
</view>
<!-- 地址块 -->
<scroll-view class="content" scroll-y="true">
<view class="address-block">
<view class="addr-row">
<view class="badge pickup"></view>
<view class="addr-body">
<text class="addr-title">{{ order?.shopName || '店铺名称' }}</text>
<text class="addr-sub">{{ order?.pickupAddress }}</text>
</view>
<view class="nav-icon" @click="openMap(order?.pickupLat, order?.pickupLng, order?.pickupAddress)">导航</view>
</view>
<view class="addr-row">
<view class="badge deliver"></view>
<view class="addr-body">
<text class="addr-title">{{ order?.deliveryAddress }}</text>
<text
class="addr-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="note" v-if="order?.note">
<text>顾客{{ order.note }}</text>
</view>
<!-- 商品清单 -->
<view class="goods-list">
<view class="goods-header">
<text>商品清单</text>
<text class="item-count">{{ totalCount }}</text>
<text class="total-price">¥{{ totalPrice.toFixed(2) }}</text>
</view>
<view class="goods-item" v-for="(g, idx) in order?.items || []" :key="idx">
<view class="g-left">
<text class="g-name">{{ g.name }}{{ g.spec ? ('·' + g.spec) : '' }}</text>
<text class="g-qty">×{{ g.quantity }}</text>
</view>
<view class="g-right">¥{{ (g.price || 0).toFixed(2) }}</view>
</view>
</view>
<!-- 联系/记录等 -->
<view class="action-record">
<!-- <view class="call" @click="callPhone(order?.receiverPhone)">
<text>电话联系</text>
</view> -->
<view class="record" @click="toRecord">交接记录</view>
</view>
<!-- 占位底部高度避免内容被底部按钮遮挡 -->
<view style="height:140rpx;"></view>
</scroll-view>
<!-- 底部操作 -->
<view class="fixed-actions">
<view class="left-actions">
<view class="icon-phone" @click="callPhone(order?.receiverPhone)"></view>
</view>
<view class="right-actions">
<view class="btn remind">催单</view>
<view class="btn remind" @click="openRemindPopup">电话联系</view>
<!-- <view class="btn confirm">转单</view> -->
<view class="btn confirm" @click="confirmArrive">确认到店</view>
</view>
</view>
<!-- 催单弹框uView Plus up-popup -->
<up-popup v-model:show="showRemind" mode="bottom" :closeable="false" border-radius="12">
<view class="remind-popup">
<view class="remind-row" @click="callShopPhone">
<text class="remind-title">联系商家</text>
<view class="remind-btn" @click.stop="callShopPhone">拨打电话</view>
</view>
<view class="remind-row" @click="callCustomerPhone">
<text class="remind-title">联系顾客</text>
<view class="remind-btn" @click.stop="callCustomerPhone">拨打电话</view>
</view>
</view>
<template #bottom>
<view class="remind-cancel" @click="showRemind = false">取消</view>
</template>
</up-popup>
</view>
</s-layout>
</template>
<script setup>
import {
ref,
computed
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
import sheep from '@/sheep';
const orderId = ref(null);
const order = ref(null);
const loading = ref(false);
const mapAvailable = ref(false); // 如果需要使用 map 组件,置为 true
// 入口:从页面参数取 orderId然后加载数据
onLoad((options = {}) => {
orderId.value = options.id || options.orderId || null;
fetchOrder();
});
async function fetchOrder() {
loading.value = true;
try {
// 优先尝试平台统一 request项目内可能封装在 sheep.request 或 sheep.api
if (sheep && typeof sheep.request === 'function') {
const res = await sheep.request({
url: '/order/detail',
method: 'GET',
data: {
id: orderId.value
}
});
// 根据封装不同,这里兼容 res.data 或 res
order.value = (res && res.data) ? res.data : res;
} else if (sheep && sheep.$api && sheep.$api.trade && typeof sheep.$api.trade.detail === 'function') {
const res = await sheep.$api.trade.detail({
id: orderId.value
});
order.value = res?.data || res;
} else {
// 回退 mock 数据,避免界面空白,开发时替换为真实接口
order.value = {
id: orderId.value || 1001,
shopName: '店铺名(示例)',
pickupAddress: '广东省广州市天河区学院站荷光路118-121号',
pickupLat: 23.1005,
pickupLng: 113.3301,
deliveryAddress: '广东省广州市天河区华景新城软件园区B栋西梯501',
deliveryLat: 23.105,
deliveryLng: 113.335,
receiverName: '张先生',
receiverPhone: '13900001234',
note: '依据餐量提供餐具',
items: [{
name: '商品名称A',
spec: '规格1',
quantity: 2,
price: 23.89
},
{
name: '商品名称B',
spec: '规格2',
quantity: 1,
price: 45.00
}
]
};
}
} catch (e) {
console.error('fetchOrder error', e);
// 友好提示
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('获取订单失败,请稍后重试');
} finally {
loading.value = false;
}
}
const totalCount = computed(() => {
if (!order.value || !order.value.items) return 0;
return order.value.items.reduce((s, it) => s + (it.quantity || 0), 0);
});
const totalPrice = computed(() => {
if (!order.value || !order.value.items) return 0;
return order.value.items.reduce((s, it) => s + ((it.price || 0) * (it.quantity || 0)), 0);
});
// 显示距离与预计时间(示例,真实项目可使用服务端或高德/百度 SDK 计算)
const distanceText = computed(() => {
// 此处为示例固定值,后续接入定位/路程计算替换
return order.value ? '873m' : '--';
});
const etaText = computed(() => {
return order.value ? '预计十分钟到达' : '--';
});
function goBack() {
uni.navigateBack();
}
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 navigateToShop() {
openMap(order.value?.pickupLat, order.value?.pickupLng, order.value?.pickupAddress);
}
function callPhone(phone) {
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到联系电话');
return;
}
uni.makePhoneCall({
phoneNumber: phone
});
}
// 催单弹框控制(使用 uView Plus 的 up-popup
const showRemind = ref(false);
function openRemindPopup() {
showRemind.value = true;
}
function closeRemindPopup() {
showRemind.value = false;
}
function callShopPhone() {
// 商家电话优先使用 order.shopPhone否则尝试 fallback
const phone = order.value?.shopPhone || order.value?.shopPhoneNumber || order.value?.receiverPhone || '';
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到商家电话');
return;
}
callPhone(phone);
closeRemindPopup();
}
function callCustomerPhone() {
const phone = order.value?.receiverPhone || '';
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到顾客电话');
return;
}
callPhone(phone);
closeRemindPopup();
}
function confirmArrive() {
// 确认到店:调用接口或本地改变状态
if (!order.value) return;
// 示例:调用后端接口(兼容性判断)
(async () => {
try {
if (sheep && typeof sheep.request === 'function') {
await sheep.request({
url: '/order/confirmArrive',
method: 'POST',
data: {
id: order.value.id
}
});
}
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('已确认到店');
// 可在此刷新订单状态
fetchOrder();
} catch (e) {
console.error(e);
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('确认失败,请重试');
}
})();
}
//跳转交接记录
function toRecord(id) {
uni.navigateTo({
url: `/pages/order/handoverRecord?orderId=${orderId.value}`
})
}
</script>
<style scoped lang="scss">
.order-detail-page {
// background: #fff;
background: #f7f7f7;
min-height: 100vh;
position: relative;
overflow-x: hidden;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
}
.header {
height: 88rpx;
display: flex;
align-items: center;
padding: 0 20rpx;
border-bottom: 1rpx solid #eee;
}
.back {
width: 44rpx;
font-size: 40rpx;
color: #333;
}
.title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 700;
color: #333;
margin-right: 44rpx;
}
.map-area {
height: 360rpx;
background: #f3f3f3;
position: relative;
overflow: hidden;
}
.map-image {
width: 100%;
height: 100%;
display: block;
box-sizing: border-box;
}
.map-native {
width: 100%;
height: 100%;
}
.map-overlay {
position: absolute;
left: 20rpx;
right: 20rpx;
bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.eta {
background: rgba(255, 255, 255, 0.95);
padding: 10rpx 14rpx;
border-radius: 20rpx;
font-size: 24rpx;
color: #333;
}
.nav-btn {
background: #fff;
padding: 10rpx 14rpx;
border-radius: 20rpx;
color: #1e9fff;
border: 1rpx solid #dbeeff;
}
.content {
padding: 20rpx;
padding-left: 20rpx;
padding-right: 20rpx;
background: #f7f7f7;
min-height: 200rpx;
box-sizing: border-box;
overflow-x: hidden;
}
.address-block {
background: #fff;
padding: 16rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.04);
}
.addr-row {
display: flex;
align-items: flex-start;
padding: 12rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.addr-row:last-child {
border-bottom: none;
}
.badge {
width: 46rpx;
height: 46rpx;
border-radius: 23rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 20rpx;
margin-right: 12rpx;
}
.badge.pickup {
background: #87d6ff;
}
.badge.deliver {
background: #ffd591;
color: #333;
}
.addr-body {
flex: 1;
min-width: 0;
}
.addr-title {
display: block;
font-size: 28rpx;
font-weight: 600;
word-break: break-word;
}
.addr-sub {
display: block;
font-size: 22rpx;
color: #888;
margin-top: 6rpx;
}
.nav-icon {
color: #1e9fff;
padding: 6rpx 8rpx;
}
.note {
margin-top: 12rpx;
background: #fff;
padding: 12rpx;
border-radius: 8rpx;
color: #666;
}
.goods-list {
margin-top: 14rpx;
}
.goods-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx;
background: #fff;
border-radius: 8rpx;
font-weight: 700;
color: #333;
}
.goods-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx;
background: #fff;
margin-top: 8rpx;
border-radius: 8rpx;
box-sizing: border-box;
}
.g-left {
display: flex;
gap: 8rpx;
align-items: center;
}
.g-left {
min-width: 0;
}
.g-name {
font-size: 26rpx;
color: #333;
max-width: 62%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.g-qty {
color: #888;
margin-left: 8rpx;
}
.g-right {
color: #333;
font-weight: 600;
}
.action-record {
display: flex;
gap: 12rpx;
margin-top: 14rpx;
}
.call {
flex: 1;
background: #fff;
padding: 12rpx;
border-radius: 8rpx;
text-align: center;
border: 1rpx solid #ddd;
}
.record {
flex: 2;
background: #fff;
padding: 12rpx;
border-radius: 8rpx;
text-align: center;
border: 1rpx solid #ddd;
}
.fixed-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 140rpx;
display: flex;
align-items: center;
padding: 20rpx;
gap: 12rpx;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.left-actions {
width: 72rpx;
display: flex;
align-items: center;
justify-content: center;
}
.right-actions {
flex: 1;
display: flex;
gap: 12rpx;
justify-content: flex-end;
align-items: center;
}
.btn {
padding: 16rpx 18rpx;
border-radius: 12rpx;
color: #fff;
font-weight: 700;
}
.btn.remind {
background: #f39c12;
}
.btn.confirm {
background: #1e9fff;
}
.remind-popup {
padding: 20rpx 0;
}
.remind-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
background: #f6f6f6;
margin: 10rpx 16rpx;
border-radius: 8rpx;
}
.remind-title {
font-size: 28rpx;
font-weight: 700;
color: #333;
}
.remind-btn {
background: #1e9fff;
color: #fff;
padding: 10rpx 18rpx;
border-radius: 8rpx;
font-weight: 700;
}
.remind-cancel {
text-align: center;
padding: 18rpx 0 70rpx;
color: #666;
font-size: 26rpx;
background: #fff;
margin-top: 10rpx;
}
</style>