|
|
|
|
@ -78,6 +78,8 @@
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<button class="location-refresh-btn location-refresh-btn--standalone" @click="refreshUserLocation">重新获取定位</button>
|
|
|
|
|
|
|
|
|
|
<!-- 过闸日期选择 -->
|
|
|
|
|
<view class="card">
|
|
|
|
|
<view class="card-title">过闸日期</view>
|
|
|
|
|
@ -106,6 +108,7 @@
|
|
|
|
|
<view style="position:absolute;left:0;top:0;right:0;bottom:0;z-index:2;" @tap="toggleAgreeNotice"></view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
</view>
|
|
|
|
|
<view class="reservation-bottom-bar">
|
|
|
|
|
<button class="reservation-btn" @click="onReserve">预约</button>
|
|
|
|
|
@ -121,7 +124,8 @@ import wx from 'jweixin-module'
|
|
|
|
|
// #endif
|
|
|
|
|
export default {
|
|
|
|
|
name: 'ReservationPage',
|
|
|
|
|
geofenceToleranceMeters: 20,
|
|
|
|
|
geofenceToleranceMeters: 1000,
|
|
|
|
|
geofenceAccuracyWarningMeters: 1000,
|
|
|
|
|
components: { NavBar },
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
@ -149,6 +153,17 @@ export default {
|
|
|
|
|
userLocation: null, // 用户位置信息 {latitude, longitude}
|
|
|
|
|
isInGeofence: null, // 是否在围栏范围内,null表示未检查,true表示在范围内,false表示不在范围内
|
|
|
|
|
geofenceRequestSeq: 0, // 方向切换时用于丢弃过期围栏响应
|
|
|
|
|
geofenceDebug: {
|
|
|
|
|
direction: '',
|
|
|
|
|
locationSource: '',
|
|
|
|
|
stage: '',
|
|
|
|
|
result: null,
|
|
|
|
|
message: '',
|
|
|
|
|
updatedAt: '',
|
|
|
|
|
location: null,
|
|
|
|
|
rawGeofences: null,
|
|
|
|
|
evaluations: [],
|
|
|
|
|
},
|
|
|
|
|
unitPrice: '', // 单价
|
|
|
|
|
calculationDescription: '', // 计算规则
|
|
|
|
|
}
|
|
|
|
|
@ -241,6 +256,36 @@ export default {
|
|
|
|
|
// #endif
|
|
|
|
|
return Promise.resolve(null);
|
|
|
|
|
},
|
|
|
|
|
handleLocationAccuracyNotice(location) {
|
|
|
|
|
const accuracy = location && typeof location.accuracy === 'number' ? location.accuracy : null;
|
|
|
|
|
if (accuracy !== null && accuracy > (this.$options.geofenceAccuracyWarningMeters || 1000)) {
|
|
|
|
|
uni.showToast({
|
|
|
|
|
title: `当前定位误差约${accuracy.toFixed(0)}米`,
|
|
|
|
|
icon: 'none',
|
|
|
|
|
duration: 2500
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async refreshUserLocation() {
|
|
|
|
|
uni.showLoading({ title: '重新获取定位...' });
|
|
|
|
|
const location = await this.getUserLocation();
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
|
|
|
|
if (!location) {
|
|
|
|
|
uni.showToast({ title: '重新获取定位失败', icon: 'none' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.handleLocationAccuracyNotice(location);
|
|
|
|
|
|
|
|
|
|
if (this.direction) {
|
|
|
|
|
await this.fetchGeofenceByDirection({
|
|
|
|
|
showModal: false,
|
|
|
|
|
direction: this.direction,
|
|
|
|
|
location,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 微信 H5 获取位置
|
|
|
|
|
getWeixinLocation() {
|
|
|
|
|
// #ifdef H5
|
|
|
|
|
@ -309,11 +354,25 @@ export default {
|
|
|
|
|
accuracy: res.accuracy
|
|
|
|
|
};
|
|
|
|
|
this.userLocation = location;
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
locationSource: '微信 JSSDK gcj02',
|
|
|
|
|
location,
|
|
|
|
|
message: '微信定位成功',
|
|
|
|
|
stage: '定位完成',
|
|
|
|
|
});
|
|
|
|
|
this.handleLocationAccuracyNotice(location);
|
|
|
|
|
console.log('[WeixinLocation] 已保存 userLocation =', this.userLocation);
|
|
|
|
|
resolve(location);
|
|
|
|
|
},
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
console.error('[WeixinLocation] wx.getLocation fail:', err);
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
locationSource: '微信 JSSDK gcj02',
|
|
|
|
|
location: null,
|
|
|
|
|
result: null,
|
|
|
|
|
message: '微信定位失败',
|
|
|
|
|
stage: '定位失败',
|
|
|
|
|
});
|
|
|
|
|
uni.showModal({
|
|
|
|
|
title: '提示',
|
|
|
|
|
content: '获取位置失败,请允许访问位置信息',
|
|
|
|
|
@ -352,11 +411,25 @@ export default {
|
|
|
|
|
speed: position.coords.speed
|
|
|
|
|
};
|
|
|
|
|
this.userLocation = location;
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
locationSource: '浏览器 geolocation',
|
|
|
|
|
location,
|
|
|
|
|
message: '浏览器定位成功',
|
|
|
|
|
stage: '定位完成',
|
|
|
|
|
});
|
|
|
|
|
this.handleLocationAccuracyNotice(location);
|
|
|
|
|
console.log('浏览器获取位置成功:', this.userLocation);
|
|
|
|
|
resolve(location);
|
|
|
|
|
},
|
|
|
|
|
(error) => {
|
|
|
|
|
console.error('浏览器获取位置失败:', error);
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
locationSource: '浏览器 geolocation',
|
|
|
|
|
location: null,
|
|
|
|
|
result: null,
|
|
|
|
|
message: '浏览器定位失败:' + (error && error.message ? error.message : '未知错误'),
|
|
|
|
|
stage: '定位失败',
|
|
|
|
|
});
|
|
|
|
|
let errorMsg = '获取位置失败';
|
|
|
|
|
switch(error.code) {
|
|
|
|
|
case error.PERMISSION_DENIED:
|
|
|
|
|
@ -400,11 +473,25 @@ export default {
|
|
|
|
|
accuracy: res.accuracy
|
|
|
|
|
};
|
|
|
|
|
this.userLocation = location;
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
locationSource: '微信小程序 gcj02',
|
|
|
|
|
location,
|
|
|
|
|
message: '小程序定位成功',
|
|
|
|
|
stage: '定位完成',
|
|
|
|
|
});
|
|
|
|
|
this.handleLocationAccuracyNotice(location);
|
|
|
|
|
console.log('小程序获取位置成功:', this.userLocation);
|
|
|
|
|
resolve(location);
|
|
|
|
|
},
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
console.error('小程序获取位置失败:', err);
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
locationSource: '微信小程序 gcj02',
|
|
|
|
|
location: null,
|
|
|
|
|
result: null,
|
|
|
|
|
message: '小程序定位失败',
|
|
|
|
|
stage: '定位失败',
|
|
|
|
|
});
|
|
|
|
|
uni.showModal({
|
|
|
|
|
title: '提示',
|
|
|
|
|
content: '获取位置失败,请在设置中允许位置权限',
|
|
|
|
|
@ -443,6 +530,11 @@ export default {
|
|
|
|
|
if (!token || !direction) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
direction,
|
|
|
|
|
stage: '请求围栏',
|
|
|
|
|
location: location || null,
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
const res = await new Promise((resolve, reject) => {
|
|
|
|
|
uni.request({
|
|
|
|
|
@ -455,6 +547,13 @@ export default {
|
|
|
|
|
if (res.data && res.data.errcode === 0) {
|
|
|
|
|
const geofenceData = res.data.data;
|
|
|
|
|
console.log(geofenceData);
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
direction,
|
|
|
|
|
rawGeofences: geofenceData,
|
|
|
|
|
stage: '围栏已返回',
|
|
|
|
|
location: location || null,
|
|
|
|
|
message: `获取到 ${Array.isArray(geofenceData) ? geofenceData.length : 0} 个围栏`,
|
|
|
|
|
});
|
|
|
|
|
const result = this.checkLocationInGeofence(geofenceData, {
|
|
|
|
|
showModal,
|
|
|
|
|
location,
|
|
|
|
|
@ -476,6 +575,12 @@ export default {
|
|
|
|
|
if (requestSeq === null || requestSeq === this.geofenceRequestSeq) {
|
|
|
|
|
this.isInGeofence = null;
|
|
|
|
|
}
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
direction,
|
|
|
|
|
result: null,
|
|
|
|
|
stage: '围栏请求失败',
|
|
|
|
|
message: e && e.message ? e.message : '围栏请求失败',
|
|
|
|
|
});
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
@ -489,6 +594,13 @@ export default {
|
|
|
|
|
// 检查用户位置是否存在
|
|
|
|
|
if (!location || !location.latitude || !location.longitude) {
|
|
|
|
|
console.warn('用户位置信息不存在,无法进行范围判断');
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
location: location || null,
|
|
|
|
|
result: null,
|
|
|
|
|
stage: '无法计算',
|
|
|
|
|
message: '用户位置信息不存在,无法进行范围判断',
|
|
|
|
|
evaluations: [],
|
|
|
|
|
});
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -497,20 +609,54 @@ export default {
|
|
|
|
|
|
|
|
|
|
// 如果数组为空,不做操作,允许提交
|
|
|
|
|
if (!geofenceData || !Array.isArray(geofenceData) || geofenceData.length === 0) {
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
location,
|
|
|
|
|
rawGeofences: geofenceData,
|
|
|
|
|
result: true,
|
|
|
|
|
stage: '围栏计算完成',
|
|
|
|
|
message: '未配置围栏,默认放行',
|
|
|
|
|
evaluations: [],
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const evaluations = [];
|
|
|
|
|
|
|
|
|
|
// 遍历围栏数据,查找 coordinates 字段
|
|
|
|
|
for (let i = 0; i < geofenceData.length; i++) {
|
|
|
|
|
const item = geofenceData[i];
|
|
|
|
|
if (item && item.coordinates && Array.isArray(item.coordinates) && item.coordinates.length > 0) {
|
|
|
|
|
// 判断用户位置是否在当前围栏范围内
|
|
|
|
|
if (this.isPointInPolygon(userLng, userLat, item.coordinates)) {
|
|
|
|
|
const evaluation = this.evaluatePointAgainstPolygon(userLng, userLat, item.coordinates);
|
|
|
|
|
evaluations.push({
|
|
|
|
|
geofenceId: item.id || null,
|
|
|
|
|
geofenceName: item.name || '',
|
|
|
|
|
pointCount: item.coordinates.length,
|
|
|
|
|
...evaluation
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (evaluation.inside) {
|
|
|
|
|
console.log('用户位置在围栏范围内');
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
location,
|
|
|
|
|
rawGeofences: geofenceData,
|
|
|
|
|
result: true,
|
|
|
|
|
stage: '围栏计算完成',
|
|
|
|
|
message: `命中围栏 ${item.name || item.id || i + 1}`,
|
|
|
|
|
evaluations,
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.updateGeofenceDebug({
|
|
|
|
|
location,
|
|
|
|
|
rawGeofences: geofenceData,
|
|
|
|
|
result: false,
|
|
|
|
|
stage: '围栏计算完成',
|
|
|
|
|
message: '未命中任何围栏',
|
|
|
|
|
evaluations,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 只有在选择方向时(showModal为true)才显示提示,提交时(showModal为false)不显示,由onReserve统一处理
|
|
|
|
|
if (showModal) {
|
|
|
|
|
@ -524,11 +670,22 @@ export default {
|
|
|
|
|
},
|
|
|
|
|
// 判断点是否在多边形内(使用射线法)
|
|
|
|
|
isPointInPolygon(lng, lat, coordinates) {
|
|
|
|
|
return this.evaluatePointAgainstPolygon(lng, lat, coordinates).inside;
|
|
|
|
|
},
|
|
|
|
|
evaluatePointAgainstPolygon(lng, lat, coordinates) {
|
|
|
|
|
const toleranceMeters = this.$options.geofenceToleranceMeters || 20;
|
|
|
|
|
if (!coordinates || coordinates.length < 3) {
|
|
|
|
|
return false;
|
|
|
|
|
return {
|
|
|
|
|
inside: false,
|
|
|
|
|
toleranceMeters,
|
|
|
|
|
polygonPointCount: 0,
|
|
|
|
|
onBoundary: false,
|
|
|
|
|
withinTolerance: false,
|
|
|
|
|
rayCastingInside: false,
|
|
|
|
|
minDistanceMeters: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将字符串坐标转换为数字
|
|
|
|
|
|
|
|
|
|
const polygon = coordinates.map(coord => {
|
|
|
|
|
if (Array.isArray(coord) && coord.length >= 2) {
|
|
|
|
|
return [
|
|
|
|
|
@ -538,42 +695,71 @@ export default {
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}).filter(coord => coord !== null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (polygon.length < 3) {
|
|
|
|
|
return false;
|
|
|
|
|
return {
|
|
|
|
|
inside: false,
|
|
|
|
|
toleranceMeters,
|
|
|
|
|
polygonPointCount: polygon.length,
|
|
|
|
|
onBoundary: false,
|
|
|
|
|
withinTolerance: false,
|
|
|
|
|
rayCastingInside: false,
|
|
|
|
|
minDistanceMeters: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.isPointOnPolygonBoundary(lng, lat, polygon)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const boundaryInfo = this.inspectPolygonBoundary(lng, lat, polygon, toleranceMeters);
|
|
|
|
|
const rayCastingInside = this.isPointInsidePolygonByRayCasting(lng, lat, polygon);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
inside: boundaryInfo.onBoundary || boundaryInfo.withinTolerance || rayCastingInside,
|
|
|
|
|
toleranceMeters,
|
|
|
|
|
polygonPointCount: polygon.length,
|
|
|
|
|
onBoundary: boundaryInfo.onBoundary,
|
|
|
|
|
withinTolerance: boundaryInfo.withinTolerance,
|
|
|
|
|
nearestSegmentIndex: boundaryInfo.nearestSegmentIndex,
|
|
|
|
|
minDistanceMeters: boundaryInfo.minDistanceMeters,
|
|
|
|
|
rayCastingInside,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
isPointInsidePolygonByRayCasting(lng, lat, polygon) {
|
|
|
|
|
let inside = false;
|
|
|
|
|
const x = lng;
|
|
|
|
|
const y = lat;
|
|
|
|
|
|
|
|
|
|
// 射线法:从点向右发射一条射线,统计与多边形边界的交点数
|
|
|
|
|
|
|
|
|
|
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
|
|
|
const xi = polygon[i][0];
|
|
|
|
|
const yi = polygon[i][1];
|
|
|
|
|
const xj = polygon[j][0];
|
|
|
|
|
const yj = polygon[j][1];
|
|
|
|
|
|
|
|
|
|
// 检查射线是否与边相交
|
|
|
|
|
const intersect = ((yi > y) !== (yj > y)) &&
|
|
|
|
|
|
|
|
|
|
const intersect = ((yi > y) !== (yj > y)) &&
|
|
|
|
|
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (intersect) {
|
|
|
|
|
inside = !inside;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return inside;
|
|
|
|
|
},
|
|
|
|
|
isPointOnPolygonBoundary(lng, lat, polygon, toleranceMeters = this.$options.geofenceToleranceMeters || 20) {
|
|
|
|
|
const boundaryInfo = this.inspectPolygonBoundary(lng, lat, polygon, toleranceMeters);
|
|
|
|
|
return boundaryInfo.onBoundary || boundaryInfo.withinTolerance;
|
|
|
|
|
},
|
|
|
|
|
inspectPolygonBoundary(lng, lat, polygon, toleranceMeters = this.$options.geofenceToleranceMeters || 20) {
|
|
|
|
|
if (!polygon || polygon.length < 2) {
|
|
|
|
|
return false;
|
|
|
|
|
return {
|
|
|
|
|
onBoundary: false,
|
|
|
|
|
withinTolerance: false,
|
|
|
|
|
minDistanceMeters: null,
|
|
|
|
|
nearestSegmentIndex: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let minDistanceMeters = null;
|
|
|
|
|
let nearestSegmentIndex = null;
|
|
|
|
|
|
|
|
|
|
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
|
|
|
const start = polygon[j];
|
|
|
|
|
const end = polygon[i];
|
|
|
|
|
@ -583,15 +769,27 @@ export default {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.isPointOnSegment(lng, lat, start[0], start[1], end[0], end[1])) {
|
|
|
|
|
return true;
|
|
|
|
|
return {
|
|
|
|
|
onBoundary: true,
|
|
|
|
|
withinTolerance: true,
|
|
|
|
|
minDistanceMeters: 0,
|
|
|
|
|
nearestSegmentIndex: i,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.getDistanceToSegmentMeters(lng, lat, start[0], start[1], end[0], end[1]) <= toleranceMeters) {
|
|
|
|
|
return true;
|
|
|
|
|
const distanceMeters = this.getDistanceToSegmentMeters(lng, lat, start[0], start[1], end[0], end[1]);
|
|
|
|
|
if (minDistanceMeters === null || distanceMeters < minDistanceMeters) {
|
|
|
|
|
minDistanceMeters = distanceMeters;
|
|
|
|
|
nearestSegmentIndex = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
return {
|
|
|
|
|
onBoundary: false,
|
|
|
|
|
withinTolerance: minDistanceMeters !== null && minDistanceMeters <= toleranceMeters,
|
|
|
|
|
minDistanceMeters: minDistanceMeters !== null ? Number(minDistanceMeters.toFixed(3)) : null,
|
|
|
|
|
nearestSegmentIndex,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
isPointOnSegment(px, py, x1, y1, x2, y2, epsilon = 1e-10) {
|
|
|
|
|
const cross = (px - x1) * (y2 - y1) - (py - y1) * (x2 - x1);
|
|
|
|
|
@ -634,6 +832,23 @@ export default {
|
|
|
|
|
|
|
|
|
|
return Math.hypot(pxMeters - closestX, pyMeters - closestY);
|
|
|
|
|
},
|
|
|
|
|
updateGeofenceDebug(payload = {}) {
|
|
|
|
|
this.geofenceDebug = {
|
|
|
|
|
...this.geofenceDebug,
|
|
|
|
|
...payload,
|
|
|
|
|
updatedAt: new Date().toLocaleString(),
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
formatDebug(value) {
|
|
|
|
|
if (value === null || typeof value === 'undefined') {
|
|
|
|
|
return 'null';
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return JSON.stringify(value, null, 2);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return String(value);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async fetchDailyReservationDeadline() {
|
|
|
|
|
const token = uni.getStorageSync('token');
|
|
|
|
|
if (!token) return;
|
|
|
|
|
@ -1099,6 +1314,21 @@ export default {
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
.location-refresh-btn {
|
|
|
|
|
margin-top: 20rpx;
|
|
|
|
|
height: 76rpx;
|
|
|
|
|
line-height: 76rpx;
|
|
|
|
|
border-radius: 999rpx;
|
|
|
|
|
background: #fff7ed;
|
|
|
|
|
color: #c2410c;
|
|
|
|
|
border: 1rpx solid #fff7ed;
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
}
|
|
|
|
|
.location-refresh-btn--standalone {
|
|
|
|
|
display: block;
|
|
|
|
|
width: calc(100% - 64rpx);
|
|
|
|
|
margin: 20rpx 32rpx 0;
|
|
|
|
|
}
|
|
|
|
|
.water-info-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|