import amapFile from '../libs/amap-wx.130.js';
|
import { _AMAP_KEY, _ROBOTAXI_HOST, _UID } from '../constants/api';
|
import Taro from '@tarojs/taro';
|
import request from './request';
|
import { getFenceCheck, getNearestSite } from '../robotaxi/api/home';
|
import { getOpenCity } from '../robotaxi/api/select-city';
|
import { getStorage, getOrCreateLocalId } from './tools';
|
|
/**
|
* 安全的 JSON 解析函数
|
* @param str 需要解析的字符串
|
* @param defaultVal 解析失败时的默认返回值
|
*/
|
export const safeParse = (str: any, defaultVal: any = null) => {
|
if (typeof str !== 'string' || str.includes('服务器时间')) {
|
return str || defaultVal;
|
}
|
try {
|
return JSON.parse(str);
|
} catch (e) {
|
console.error('safeParse error:', e, 'original string:', str);
|
return defaultVal;
|
}
|
};
|
|
// 辅助函数:计算两点之间的距离(单位:米)
|
export const getDistance = (lat1: number, lng1: number, lat2: number, lng2: number) => {
|
const radLat1 = lat1 * Math.PI / 180.0;
|
const radLat2 = lat2 * Math.PI / 180.0;
|
const a = radLat1 - radLat2;
|
const b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
|
let s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
|
s = s * 6378.137; // 地球半径
|
s = Math.round(s * 10000) / 10000;
|
return s * 1000;
|
};
|
|
/**
|
* 计算点 P 在线段 AB 上的投影点(吸附算法)
|
* @param p 真实点 {latitude, longitude}
|
* @param a 线段起点 {latitude, longitude}
|
* @param b 线段终点 {latitude, longitude}
|
* @returns 投影点 {latitude, longitude}
|
*/
|
export const getProjectedPoint = (p: any, a: any, b: any) => {
|
if (!p || !a || !b) return p;
|
if (a.latitude === b.latitude && a.longitude === b.longitude) return a;
|
|
const x0 = p.longitude, y0 = p.latitude;
|
const x1 = a.longitude, y1 = a.latitude;
|
const x2 = b.longitude, y2 = b.latitude;
|
|
// 向量 AB
|
const dx = x2 - x1;
|
const dy = y2 - y1;
|
// 向量 AP
|
const ex = x0 - x1;
|
const ey = y0 - y1;
|
|
// 计算投影比例 t
|
const dotProduct = ex * dx + ey * dy;
|
const lengthSquared = dx * dx + dy * dy;
|
const t = dotProduct / lengthSquared;
|
|
// 如果投影点落在线段外,则取最近的端点
|
if (t <= 0) return { latitude: a.latitude, longitude: a.longitude };
|
if (t >= 1) return { latitude: b.latitude, longitude: b.longitude };
|
|
// 返回投影点
|
return {
|
latitude: y1 + t * dy,
|
longitude: x1 + t * dx
|
};
|
};
|
|
// 判断是否需要更新路线(距离大于100米或时间超过10秒)
|
export const shouldUpdateRoute = (lastLat: number, lastLng: number, newLat: number, newLng: number, lastUpdateTime: number): boolean => {
|
const distance = getDistance(lastLat, lastLng, newLat, newLng);
|
const now = Date.now();
|
const timeElapsed = now - lastUpdateTime;
|
return distance > 100 || timeElapsed >= 10000;
|
};
|
|
// 格式化距离
|
export const formatDistance = (val: any) => {
|
if (!val) return '0m';
|
const d = Number(val);
|
if (Number.isNaN(d)) return val;
|
if (d >= 1000) {
|
return (d / 1000).toFixed(1) + 'km';
|
}
|
return d.toFixed(0) + 'm';
|
}
|
|
export const computeAdaptiveBottomPadding = (
|
points: any[],
|
basePadding: number,
|
minPadding = 200,
|
minAspect = 0.8,
|
minRatio = 0.7
|
) => {
|
if (!Array.isArray(points) || points.length < 2) {
|
return basePadding;
|
}
|
let minLat = Number.POSITIVE_INFINITY;
|
let maxLat = Number.NEGATIVE_INFINITY;
|
let minLng = Number.POSITIVE_INFINITY;
|
let maxLng = Number.NEGATIVE_INFINITY;
|
for (const p of points) {
|
const lat = Number(p?.latitude);
|
const lng = Number(p?.longitude);
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
|
continue;
|
}
|
if (lat < minLat) minLat = lat;
|
if (lat > maxLat) maxLat = lat;
|
if (lng < minLng) minLng = lng;
|
if (lng > maxLng) maxLng = lng;
|
}
|
if (!Number.isFinite(minLat) || !Number.isFinite(maxLat) || !Number.isFinite(minLng) || !Number.isFinite(maxLng)) {
|
return basePadding;
|
}
|
const latRange = maxLat - minLat;
|
const lngRange = maxLng - minLng;
|
if (latRange <= 0 || lngRange <= 0) {
|
return basePadding;
|
}
|
const avgLat = (minLat + maxLat) / 2;
|
const adjustedLngRange = lngRange * Math.cos((avgLat * Math.PI) / 180);
|
if (!Number.isFinite(adjustedLngRange) || adjustedLngRange <= 0) {
|
return basePadding;
|
}
|
const aspect = latRange / adjustedLngRange;
|
if (aspect >= minAspect) {
|
return basePadding;
|
}
|
const ratio = Math.max(minRatio, Math.min(1, minRatio + (aspect / minAspect) * (1 - minRatio)));
|
return Math.max(minPadding, Math.round(basePadding * ratio));
|
};
|
|
// 查找最近的标记点
|
export const findNearestMarker = (lat: number, lng: number, markers: any[]) => {
|
let minDistance = Infinity;
|
let targetMarker: any = null;
|
|
markers.forEach(marker => {
|
const distance = getDistance(lat, lng, marker.latitude, marker.longitude);
|
if (distance < minDistance) {
|
minDistance = distance;
|
targetMarker = marker;
|
}
|
});
|
|
return { targetMarker, minDistance };
|
};
|
|
// 解析路线数据为坐标点数组
|
export const parseRouteToPoints = (routeData: any) => {
|
const points: any[] = [];
|
if (routeData.paths && routeData.paths[0] && routeData.paths[0].steps) {
|
const steps = routeData.paths[0].steps;
|
for (let i = 0; i < steps.length; i++) {
|
const poLen = steps[i].polyline.split(';');
|
for (let j = 0; j < poLen.length; j++) {
|
points.push({
|
longitude: parseFloat(poLen[j].split(',')[0]),
|
latitude: parseFloat(poLen[j].split(',')[1])
|
})
|
}
|
}
|
}
|
return points;
|
};
|
|
// 格式化站点为标记点
|
export const formatSiteToMarker = (sites: any[]) => {
|
return sites.map((site, index) => ({
|
...site,
|
// id: index + 1,
|
iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-poi.png',
|
width: 15,
|
height: 15,
|
anchor: { x: 0.5, y: 0.5 }
|
}));
|
};
|
|
// 获取位置的逆地理编码
|
export const getAddressByLocation = (longitude: number, latitude: number): Promise<any> => {
|
return new Promise((resolve, reject) => {
|
const myAmapFun = new amapFile.AMapWX({ key: _AMAP_KEY });
|
myAmapFun.getRegeo({
|
location: `${longitude},${latitude}`,
|
success: (data) => {
|
if (data && data[0]) {
|
resolve(data[0]);
|
} else {
|
reject('No data found');
|
}
|
},
|
fail: (info) => {
|
reject(info);
|
}
|
});
|
});
|
};
|
|
// 获取天气信息
|
export const getWeatherInfo = (city?: string): Promise<any> => {
|
return new Promise((resolve, reject) => {
|
const myAmapFun = new amapFile.AMapWX({ key: _AMAP_KEY });
|
const options: any = {
|
success: (data) => resolve(data),
|
fail: (info) => reject(info)
|
};
|
if (city) {
|
options.city = city;
|
}
|
myAmapFun.getWeather(options);
|
});
|
};
|
|
// 格式化电子围栏数据
|
export const formatGeofencePolygons = (geofenceList: any[]) => {
|
if (!geofenceList || !Array.isArray(geofenceList)) {
|
return [];
|
}
|
return geofenceList.map(item => {
|
return {
|
points: item.points.map(p => ({
|
latitude: p.lat,
|
longitude: p.lng
|
})),
|
strokeWidth: 2,
|
strokeColor: '#A1B1FF',
|
fillColor: '#6d7fd933',
|
zIndex: 1
|
}
|
});
|
};
|
|
// 提取多边形所有顶点
|
export const extractPointsFromPolygons = (polygons: any[]) => {
|
const points: any[] = [];
|
if (!polygons || !Array.isArray(polygons)) return points;
|
|
polygons.forEach(polygon => {
|
if (polygon.points && Array.isArray(polygon.points)) {
|
points.push(...polygon.points);
|
}
|
});
|
return points;
|
};
|
|
// 转换 adcode 为 cityCode
|
export const convertAdcodeToCityCode = (adcode: string, cityName: string, province: string) => {
|
let cityCode = adcode;
|
if (adcode && typeof adcode === 'string' && adcode.length === 6) {
|
if (cityName === province) {
|
// 直辖市:前2位 + 0000
|
cityCode = adcode.substring(0, 2) + '0000';
|
} else {
|
// 普通城市:前4位 + 00
|
cityCode = adcode.substring(0, 4) + '00';
|
}
|
}
|
return cityCode;
|
};
|
|
// 处理 Banner 点击跳转
|
export const handleBannerJump = (item: any) => {
|
if (!item) return;
|
const { jumpType, jumpUrl } = item;
|
// jumpType: 1小程序内页面 2小程序地址 3网页地址
|
if (jumpType === 1) {
|
if (jumpUrl) {
|
Taro.navigateTo({
|
url: jumpUrl
|
});
|
}
|
} else if (jumpType === 2) {
|
if (jumpUrl) {
|
Taro.navigateToMiniProgram({
|
appId: jumpUrl,
|
// path: '',
|
success(res) {
|
console.log('打开小程序成功', res);
|
},
|
fail(err) {
|
console.error('打开小程序失败', err);
|
Taro.showToast({ title: '跳转失败', icon: 'none' });
|
}
|
});
|
}
|
} else if (jumpType === 3) {
|
if (jumpUrl) {
|
Taro.navigateTo({
|
url: `/pages/webpage/index?pageUrl=${encodeURIComponent(jumpUrl)}`
|
});
|
}
|
}
|
};
|
|
// 防抖函数
|
export const debounce = (fn: Function, wait: number) => {
|
let timer: any = null;
|
return function (this: any, ...args: any[]) {
|
if (timer) clearTimeout(timer);
|
timer = setTimeout(() => {
|
fn.apply(this, args);
|
}, wait);
|
}
|
}
|
|
// 节流函数
|
export const throttle = (fn: Function, wait: number) => {
|
let _lastTime: any = null;
|
return function (this: any, ...args: any[]) {
|
let _nowTime = +new Date();
|
if (!_lastTime || _nowTime - _lastTime > wait) {
|
fn.apply(this, args);
|
_lastTime = _nowTime;
|
}
|
}
|
}
|
|
// 预下载图片,返回本地临时路径(WeApp),失败或非WeApp环境返回原始URL
|
export const preloadImageToTemp = async (url: string): Promise<string> => {
|
try {
|
if (typeof Taro.getEnv !== 'function' || Taro.getEnv() !== Taro.ENV_TYPE.WEAPP) {
|
return url;
|
}
|
const res = await Taro.downloadFile({ url });
|
if (res?.statusCode === 200 && res.tempFilePath) {
|
return res.tempFilePath;
|
}
|
const info = await Taro.getImageInfo({ src: url });
|
if (info?.path) {
|
return info.path;
|
}
|
} catch (e) {
|
}
|
return url;
|
}
|
|
// 获取地图数据(围栏检查+附近站点)
|
export const fetchMapData = async (lat: number, lng: number, cityCode?: string) => {
|
try {
|
// 1. 先检查是否在运营区域内
|
const fenceRes: any = await getFenceCheck({ latitude: lat, longitude: lng, cityCode }).catch(e => {
|
return { status: 'inside' }; // 默认兜底
|
});
|
|
const isInside = fenceRes.status === 'inside';
|
const electronicAreaId = fenceRes?.electronicAreaId || '';
|
|
// 2. 只有在运营区域内才获取附近站点
|
let sites: any = { points: [] };
|
if (isInside) {
|
sites = await getNearestSite({ latitude: lat, longitude: lng, electronicAreaId }).catch(e => {
|
return { points: [] };
|
});
|
// 兼容处理
|
if (!sites || !sites.points) {
|
sites = { points: [] };
|
}
|
}
|
|
const newMarkers = formatSiteToMarker(sites.points);
|
|
return { isInside, electronicAreaId, newMarkers, sites: sites.points };
|
} catch (error) {
|
console.error('fetchMapData error', error);
|
return { isInside: true, electronicAreaId: '', newMarkers: [], sites: [] };
|
}
|
}
|
|
// 获取步行规划路线
|
export const getWalkingRoute = (origin: string, destination: string): Promise<{ points: any[], duration: number, distance: number }> => {
|
return new Promise((resolve, reject) => {
|
const myAmapFun = new amapFile.AMapWX({ key: _AMAP_KEY });
|
myAmapFun.getWalkingRoute({
|
origin: origin,
|
destination: destination,
|
success: (data) => {
|
const points = parseRouteToPoints(data);
|
let duration = 0;
|
let distance = 0;
|
if (data.paths && data.paths[0]) {
|
duration = Number(data.paths[0].duration);
|
distance = Number(data.paths[0].distance);
|
}
|
resolve({ points, duration, distance });
|
},
|
fail: (info) => {
|
reject(info);
|
}
|
});
|
});
|
};
|
|
// 获取城市中心经纬度
|
export const getCityLocation = (cityName: string): Promise<{ latitude: number, longitude: number }> => {
|
return new Promise((resolve, reject) => {
|
const myAmapFun = new amapFile.AMapWX({ key: _AMAP_KEY });
|
myAmapFun.getInputtips({
|
keywords: cityName,
|
city: cityName,
|
success: (data) => {
|
if (data && data.tips && data.tips.length > 0) {
|
// 优先取第一个有坐标的结果
|
const tip = data.tips.find(t => t.location && t.location.length > 0);
|
if (tip) {
|
const locationArr = tip.location.split(',');
|
if (locationArr.length === 2) {
|
resolve({
|
longitude: Number(locationArr[0]),
|
latitude: Number(locationArr[1])
|
});
|
return;
|
}
|
}
|
reject('No valid location found');
|
} else {
|
reject('No data found');
|
}
|
},
|
fail: (info) => {
|
reject(info);
|
}
|
});
|
});
|
};
|
|
// 获取包含 ID 的城市信息
|
export const getCityInfoWithId = async (cityCode: string, cityName: string) => {
|
|
try {
|
const res: any = await getOpenCity();
|
|
let cityId = 0;
|
if (Array.isArray(res.cities)) {
|
// 优先匹配 adcode
|
let target = res.cities.find(item => item.adcode === cityCode);
|
// 如果 adcode 匹配失败,尝试匹配 cityName
|
if (!target && cityName) {
|
// 移除"市"字进行模糊匹配,增加匹配成功率
|
const normalizedCityName = cityName.replace(/市$/, '');
|
target = res.cities.find(item => {
|
const itemName = item.name.replace(/市$/, '');
|
return itemName === normalizedCityName || item.name.includes(normalizedCityName) || normalizedCityName.includes(item.name);
|
});
|
}
|
|
if (target) {
|
cityId = target.id;
|
// 修正返回的 code,确保使用的是系统匹配到的城市的标准 adcode
|
if (target.adcode) {
|
cityCode = target.adcode;
|
}
|
}
|
}
|
return {
|
name: cityName,
|
code: cityCode,
|
id: cityId
|
};
|
} catch (error) {
|
console.error('getOpenCity error', error);
|
return {
|
name: cityName,
|
code: cityCode,
|
id: 0
|
};
|
}
|
};
|
|
// 格式化秒数为时钟时间(分钟:秒)
|
export const formatDurationToClock = (sec: number) => {
|
const safeSeconds = Math.max(0, sec);
|
const m = Math.floor(safeSeconds / 60);
|
const s = safeSeconds % 60;
|
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
};
|
|
// 格式化日期时间
|
export const formatDateTime = (date: Date) => {
|
const y = date.getFullYear();
|
const mo = String(date.getMonth() + 1).padStart(2, '0');
|
const d = String(date.getDate()).padStart(2, '0');
|
const h = String(date.getHours()).padStart(2, '0');
|
const mi = String(date.getMinutes()).padStart(2, '0');
|
const s = String(date.getSeconds()).padStart(2, '0');
|
return `${y}-${mo}-${d} ${h}:${mi}:${s}`;
|
};
|
|
export const formatMoney = (value: any) => {
|
const num = Number(value);
|
if (Number.isNaN(num)) {
|
return '--';
|
}
|
return num.toFixed(2);
|
};
|
|
export const formatNumber = (value: any, digits = 1) => {
|
const num = Number(value);
|
if (Number.isNaN(num)) {
|
return '--';
|
}
|
return digits === 0 ? Math.round(num).toString() : num.toFixed(digits);
|
};
|
|
export const formatTripDistance = (value: any) => {
|
const num = Number(value);
|
if (!Number.isFinite(num)) {
|
return '--';
|
}
|
const safe = Math.max(0, num);
|
if (safe < 1000) {
|
return `${Math.round(safe)}米`;
|
}
|
return `${(safe / 1000).toFixed(1)}公里`;
|
};
|
|
export const formatTripDurationFromSeconds = (value: any) => {
|
const num = Number(value);
|
if (!Number.isFinite(num)) {
|
return '--';
|
}
|
const safe = Math.max(0, Math.round(num));
|
if (safe < 60) {
|
return `${safe}秒`;
|
}
|
if (safe < 3600) {
|
return `${Math.floor(safe / 60)}分钟`;
|
}
|
const hours = Math.floor(safe / 3600);
|
const minutes = Math.floor((safe % 3600) / 60);
|
if (minutes === 0) {
|
return `${hours}小时`;
|
}
|
return `${hours}小时${minutes}分钟`;
|
};
|
|
export const formatDurationDisplay = (value: any) => {
|
const num = Number(value);
|
if (Number.isNaN(num)) {
|
return { value: 0, unit: '分钟' };
|
}
|
if (num < 60) {
|
return { value: Math.max(0, Math.round(num)), unit: '秒' };
|
}
|
return { value: Math.ceil(num / 60), unit: '分钟' };
|
};
|
|
export const getTypeLabel = (type: any) => {
|
if (type === 2) return '夜间行驶时段';
|
if (type === 3) return '节假日时段';
|
return '普通时段';
|
};
|
|
export const formatCountdown = (seconds: number) => {
|
const safeSeconds = Math.max(0, seconds);
|
const minutes = Math.floor(safeSeconds / 60);
|
const restSeconds = safeSeconds % 60;
|
const mm = String(minutes).padStart(2, '0');
|
const ss = String(restSeconds).padStart(2, '0');
|
return `${mm}:${ss}`;
|
};
|
|
export const getRemainingSeconds = (endTimeMs: number | null) => {
|
if (!endTimeMs) {
|
return null;
|
}
|
const diff = endTimeMs - Date.now();
|
if (diff <= 0) {
|
return 0;
|
}
|
return Math.ceil(diff / 1000);
|
};
|
|
export const parseFlexibleEndTimeMs = (value: any) => {
|
const num = Number(value);
|
if (Number.isNaN(num) || num <= 0) {
|
return null;
|
}
|
if (num > 1000000000000) {
|
return num;
|
}
|
if (num > 1000000000) {
|
return num * 1000;
|
}
|
if (num <= 360) {
|
return Date.now() + num * 60 * 1000;
|
}
|
return Date.now() + num * 1000;
|
};
|
|
// 清除订单行程存储
|
export const clearOrderTripStorage = () => {
|
Taro.removeStorageSync('orderId');
|
Taro.removeStorageSync('tripStatus');
|
};
|
|
/**
|
* 通过 IP 获取当前位置信息(用于定位授权失败时的兜底)
|
*/
|
export const getLocationByIP = (): Promise<any> => {
|
return new Promise(async (resolve, reject) => {
|
try {
|
// 1. 获取当前 network IP
|
const res: any = await request({
|
url: _ROBOTAXI_HOST + '/map/base/v1/mini-program/point/getPointByIp',
|
method: 'GET',
|
});
|
|
if (res && res.point && typeof res.point.latitude === 'number' && typeof res.point.longitude === 'number') {
|
resolve({
|
longitude: res.point.longitude,
|
latitude: res.point.latitude,
|
city: res.city || '',
|
adcode: res.adcode || ''
|
});
|
} else {
|
// 如果后端接口不可用或未返回数据,尝试使用第三方接口作为备选获取 IP
|
Taro.request({
|
url: 'https://api.ipify.org?format=json',
|
success: (ipRes) => {
|
const ip = ipRes.data?.ip;
|
if (ip) {
|
// 拿着获取到的 IP 去请求服务端接口
|
request({
|
url: _ROBOTAXI_HOST + '/map/base/v1/mini-program/point/getPointByIp',
|
method: 'POST',
|
payload: { ip }
|
}).then((businessRes: any) => {
|
if (businessRes && businessRes.point && typeof businessRes.point.latitude === 'number' && typeof businessRes.point.longitude === 'number') {
|
resolve({
|
longitude: businessRes.point.longitude,
|
latitude: businessRes.point.latitude,
|
city: businessRes.city || '',
|
adcode: businessRes.adcode || ''
|
});
|
} else {
|
reject('Business IP location failed');
|
}
|
}).catch(reject);
|
} else {
|
reject('Failed to get network IP');
|
}
|
},
|
fail: reject
|
});
|
}
|
} catch (error) {
|
reject(error);
|
}
|
});
|
};
|
|
export const mapOrderStatusToTripStatus = (status: any) => {
|
switch (Number(status)) {
|
case 0:
|
return 1;
|
case 1:
|
return 3;
|
case 2:
|
case 3:
|
return 4;
|
case 4:
|
return 5;
|
case 5:
|
case 6:
|
return 6;
|
case 7:
|
return 7;
|
case 8:
|
return 8;
|
case 9:
|
case 10:
|
return 9;
|
default:
|
return 0;
|
}
|
}
|
|
// 埋点
|
export const reportEvent = async (eventId: string, para: any) => {
|
let appVersion = '';
|
const $app: any = Taro.getApp().$app;
|
const { openId } = await $app.getIdData() || {};
|
try {
|
const accountInfo = Taro.getAccountInfoSync();
|
appVersion = accountInfo?.miniProgram?.version || accountInfo?.miniProgram?.envVersion || '';
|
} catch (e) {
|
console.warn('获取小程序版本号失败:', e);
|
}
|
|
getStorage(_UID).then((uid) => {
|
$app.xpReportSend({
|
eventId,
|
para: {
|
...para,
|
user_id: uid || '-1',
|
masked_device_id: openId,
|
event_time: Date.now(),
|
city: Taro.getStorageSync('selectedCity').name,
|
orderId: Taro.getStorageSync('orderId') || '',
|
os: Taro.getSystemInfoSync().system,
|
app_ver: appVersion,
|
},
|
});
|
});
|
}
|