feat: 新增 SGS SDK 数据适配器与腾讯地图服务
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
163
src/services/MapNavigationService.ts
Normal file
163
src/services/MapNavigationService.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* 地图导航调起服务
|
||||
* 使用 uni.openLocation 调起设备地图 App 进行导航
|
||||
*/
|
||||
|
||||
import { MUSEUM_LOCATION } from '@/config/museum'
|
||||
|
||||
export interface NavigationOptions {
|
||||
/** 目的地纬度 */
|
||||
latitude: number
|
||||
/** 目的地经度 */
|
||||
longitude: number
|
||||
/** 目的地名称 */
|
||||
name: string
|
||||
/** 目的地地址(可选) */
|
||||
address?: string
|
||||
/** 缩放级别(可选,默认 18) */
|
||||
scale?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 调起外部地图应用进行导航
|
||||
* @param options 导航选项
|
||||
* @returns Promise<boolean> 是否成功调起
|
||||
*/
|
||||
export function openExternalNavigation(options: NavigationOptions): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const scale = options.scale ?? 18
|
||||
|
||||
uni.openLocation({
|
||||
latitude: options.latitude,
|
||||
longitude: options.longitude,
|
||||
name: options.name,
|
||||
address: options.address ?? '',
|
||||
scale,
|
||||
success: () => {
|
||||
console.log('成功调起地图导航:', options.name)
|
||||
resolve(true)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('调起地图导航失败:', err)
|
||||
|
||||
// 根据错误类型提供友好的提示
|
||||
if (err.errMsg) {
|
||||
if (err.errMsg.includes('cancel')) {
|
||||
// 用户取消
|
||||
resolve(false)
|
||||
} else if (err.errMsg.includes('auth deny') || err.errMsg.includes('auth reject')) {
|
||||
uni.showToast({
|
||||
title: '需要位置权限才能导航',
|
||||
icon: 'none'
|
||||
})
|
||||
resolve(false)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '无法调起地图应用',
|
||||
icon: 'none'
|
||||
})
|
||||
resolve(false)
|
||||
}
|
||||
} else {
|
||||
resolve(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航到深圳自然博物馆
|
||||
* @param fromLat 起点纬度(可选,用于日志)
|
||||
* @param fromLng 起点经度(可选,用于日志)
|
||||
* @returns Promise<boolean> 是否成功调起
|
||||
*/
|
||||
export function navigateToMuseum(
|
||||
fromLat?: number,
|
||||
fromLng?: number
|
||||
): Promise<boolean> {
|
||||
if (fromLat !== undefined && fromLng !== undefined) {
|
||||
console.log(`从 (${fromLat}, ${fromLng}) 导航到深圳自然博物馆`)
|
||||
} else {
|
||||
console.log('导航到深圳自然博物馆')
|
||||
}
|
||||
|
||||
return openExternalNavigation({
|
||||
latitude: MUSEUM_LOCATION.latitude,
|
||||
longitude: MUSEUM_LOCATION.longitude,
|
||||
name: MUSEUM_LOCATION.name,
|
||||
address: MUSEUM_LOCATION.address,
|
||||
scale: 18
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前位置并导航到博物馆
|
||||
* @returns Promise<boolean> 是否成功调起
|
||||
*/
|
||||
export function getCurrentLocationAndNavigate(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
uni.getLocation({
|
||||
type: 'gcj02',
|
||||
success: (locationRes) => {
|
||||
console.log('获取当前位置成功:', locationRes)
|
||||
navigateToMuseum(locationRes.latitude, locationRes.longitude).then(resolve)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('获取当前位置失败:', err)
|
||||
|
||||
if (err.errMsg) {
|
||||
if (err.errMsg.includes('cancel')) {
|
||||
resolve(false)
|
||||
} else if (err.errMsg.includes('auth deny') || err.errMsg.includes('auth reject')) {
|
||||
uni.showToast({
|
||||
title: '需要位置权限才能获取当前位置',
|
||||
icon: 'none'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '无法获取当前位置',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否支持调起地图应用
|
||||
* 在某些 H5 环境下可能不支持
|
||||
*/
|
||||
export function isMapNavigationSupported(): boolean {
|
||||
// #ifdef H5
|
||||
// H5 环境检查
|
||||
const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''
|
||||
// 微信浏览器需要在微信环境中使用 jssdk
|
||||
const isWechat = /MicroMessenger/i.test(ua)
|
||||
if (isWechat) {
|
||||
console.warn('微信环境需要使用微信 JSSDK 进行地图导航')
|
||||
return false
|
||||
}
|
||||
// #endif
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导航提示信息
|
||||
*/
|
||||
export function getNavigationHint(): string {
|
||||
// #ifdef H5
|
||||
const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''
|
||||
const isWechat = /MicroMessenger/i.test(ua)
|
||||
if (isWechat) {
|
||||
return '请在微信中打开后使用'
|
||||
}
|
||||
// #endif
|
||||
|
||||
return '将调起手机地图应用进行导航'
|
||||
}
|
||||
521
src/services/tencent/TencentMapService.ts
Normal file
521
src/services/tencent/TencentMapService.ts
Normal file
@@ -0,0 +1,521 @@
|
||||
/**
|
||||
* 腾讯地图 WebService API 服务
|
||||
* 用于 POI 搜索、地址解析、路线规划等
|
||||
*/
|
||||
|
||||
// 优先从环境变量读取 API Key,回退到硬编码值(仅用于开发环境)
|
||||
const TENCENT_MAP_KEY = import.meta.env.VITE_TENCENT_MAP_KEY || 'EJPBZ-DQEEQ-PDN5U-4ZDVX-F4I3F-6MBJC'
|
||||
const TENCENT_MAP_BASE_URL = 'https://apis.map.qq.com/ws'
|
||||
const TENCENT_DIRECTION_BASE_URL = `${TENCENT_MAP_BASE_URL}/direction/v1`
|
||||
|
||||
export interface TencentPoiResult {
|
||||
id: string
|
||||
title: string
|
||||
address: string
|
||||
category: string
|
||||
lat: number
|
||||
lng: number
|
||||
location: {
|
||||
lat: number
|
||||
lng: number
|
||||
}
|
||||
distance?: number
|
||||
}
|
||||
|
||||
export interface TencentSearchResult {
|
||||
status: number
|
||||
message: string
|
||||
count: number
|
||||
data: TencentPoiResult[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 路线规划起点/终点坐标
|
||||
*/
|
||||
export interface RoutePoint {
|
||||
latitude: number
|
||||
longitude: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 路线规划结果
|
||||
*/
|
||||
export interface RouteResult {
|
||||
/** 状态码,0 表示成功 */
|
||||
status: number
|
||||
/** 状态消息 */
|
||||
message: string
|
||||
/** 路线数据 */
|
||||
routes?: Route[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 路线数据
|
||||
*/
|
||||
export interface Route {
|
||||
/** 路线唯一标识 */
|
||||
id: number
|
||||
/** 起点坐标 */
|
||||
startPoint: RoutePoint
|
||||
/** 终点坐标 */
|
||||
endPoint: RoutePoint
|
||||
/** 路线总距离(米) */
|
||||
distance: number
|
||||
/** 路线总耗时(秒) */
|
||||
duration: number
|
||||
/** 路线描述 */
|
||||
description?: string
|
||||
/** 路线点串(经纬度数组,用于在地图上绘制路线) */
|
||||
polyline: RoutePoint[]
|
||||
/** 路线步骤列表 */
|
||||
steps: RouteStep[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 路线步骤
|
||||
*/
|
||||
export interface RouteStep {
|
||||
/** 起点坐标 */
|
||||
startPoint: RoutePoint
|
||||
/** 终点坐标 */
|
||||
endPoint: RoutePoint
|
||||
/** 步骤说明 */
|
||||
instruction: string
|
||||
/** 步骤距离(米) */
|
||||
distance: number
|
||||
/** 步骤耗时(秒) */
|
||||
duration: number
|
||||
/** 步骤方向 */
|
||||
direction?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 路线点串解码(腾讯地图使用自定义编码)
|
||||
* @param encoded 点串编码字符串
|
||||
* @returns 解码后的坐标数组
|
||||
*/
|
||||
function decodePolyline(encoded: string): RoutePoint[] {
|
||||
const points: RoutePoint[] = []
|
||||
let index = 0
|
||||
let lat = 0
|
||||
let lng = 0
|
||||
|
||||
while (index < encoded.length) {
|
||||
let b: number
|
||||
let shift = 0
|
||||
let result = 0
|
||||
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63
|
||||
result |= (b & 0x1f) << shift
|
||||
shift += 5
|
||||
} while (b >= 0x20)
|
||||
|
||||
const dlat = (result & 1) !== 0 ? ~(result >> 1) : (result >> 1)
|
||||
lat += dlat
|
||||
|
||||
shift = 0
|
||||
result = 0
|
||||
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63
|
||||
result |= (b & 0x1f) << shift
|
||||
shift += 5
|
||||
} while (b >= 0x20)
|
||||
|
||||
const dlng = (result & 1) !== 0 ? ~(result >> 1) : (result >> 1)
|
||||
lng += dlng
|
||||
|
||||
points.push({
|
||||
latitude: lat / 1e6,
|
||||
longitude: lng / 1e6
|
||||
})
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
|
||||
/**
|
||||
* 步行路线规划
|
||||
* @param from 起点坐标
|
||||
* @param to 终点坐标
|
||||
* @returns 路线规划结果
|
||||
*/
|
||||
export async function planWalkingRoute(
|
||||
from: RoutePoint,
|
||||
to: RoutePoint
|
||||
): Promise<RouteResult> {
|
||||
return new Promise((resolve) => {
|
||||
uni.request({
|
||||
url: `${TENCENT_DIRECTION_BASE_URL}/walking`,
|
||||
method: 'GET',
|
||||
timeout: 10000,
|
||||
data: {
|
||||
from: `${from.longitude},${from.latitude}`,
|
||||
to: `${to.longitude},${to.latitude}`,
|
||||
key: TENCENT_MAP_KEY
|
||||
},
|
||||
success: (response) => {
|
||||
const data = response.data as any
|
||||
if (response.statusCode === 200 && data && data.status === 0) {
|
||||
const routes: Route[] = data.result.routes.map((route: any) => {
|
||||
const polyline = decodePolyline(route.polyline)
|
||||
return {
|
||||
id: route.id,
|
||||
startPoint: from,
|
||||
endPoint: to,
|
||||
distance: route.distance,
|
||||
duration: route.duration,
|
||||
description: route.description,
|
||||
polyline,
|
||||
steps: route.steps.map((step: any) => ({
|
||||
startPoint: {
|
||||
latitude: step.start_point.lat,
|
||||
longitude: step.start_point.lng
|
||||
},
|
||||
endPoint: {
|
||||
latitude: step.end_point.lat,
|
||||
longitude: step.end_point.lng
|
||||
},
|
||||
instruction: step.instruction,
|
||||
distance: step.distance,
|
||||
duration: step.duration,
|
||||
direction: step.direction
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
resolve({
|
||||
status: 0,
|
||||
message: 'ok',
|
||||
routes
|
||||
})
|
||||
} else {
|
||||
resolve({
|
||||
status: data?.status ?? -1,
|
||||
message: data?.message ?? '路线规划失败'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('步行路线规划失败:', err)
|
||||
resolve({
|
||||
status: -1,
|
||||
message: '网络请求失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 驾车路线规划
|
||||
* @param from 起点坐标
|
||||
* @param to 终点坐标
|
||||
* @returns 路线规划结果
|
||||
*/
|
||||
export async function planDrivingRoute(
|
||||
from: RoutePoint,
|
||||
to: RoutePoint
|
||||
): Promise<RouteResult> {
|
||||
return new Promise((resolve) => {
|
||||
uni.request({
|
||||
url: `${TENCENT_DIRECTION_BASE_URL}/driving`,
|
||||
method: 'GET',
|
||||
timeout: 10000,
|
||||
data: {
|
||||
from: `${from.longitude},${from.latitude}`,
|
||||
to: `${to.longitude},${to.latitude}`,
|
||||
key: TENCENT_MAP_KEY
|
||||
},
|
||||
success: (response) => {
|
||||
const data = response.data as any
|
||||
if (response.statusCode === 200 && data && data.status === 0) {
|
||||
const routes: Route[] = data.result.routes.map((route: any) => {
|
||||
const polyline = decodePolyline(route.polyline)
|
||||
return {
|
||||
id: route.id,
|
||||
startPoint: from,
|
||||
endPoint: to,
|
||||
distance: route.distance,
|
||||
duration: route.duration,
|
||||
description: route.description,
|
||||
polyline,
|
||||
steps: route.steps.map((step: any) => ({
|
||||
startPoint: {
|
||||
latitude: step.start_point.lat,
|
||||
longitude: step.start_point.lng
|
||||
},
|
||||
endPoint: {
|
||||
latitude: step.end_point.lat,
|
||||
longitude: step.end_point.lng
|
||||
},
|
||||
instruction: step.instruction,
|
||||
distance: step.distance,
|
||||
duration: step.duration,
|
||||
direction: step.direction
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
resolve({
|
||||
status: 0,
|
||||
message: 'ok',
|
||||
routes
|
||||
})
|
||||
} else {
|
||||
resolve({
|
||||
status: data?.status ?? -1,
|
||||
message: data?.message ?? '路线规划失败'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('驾车路线规划失败:', err)
|
||||
resolve({
|
||||
status: -1,
|
||||
message: '网络请求失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 公交路线规划
|
||||
* @param from 起点坐标
|
||||
* @param to 终点坐标
|
||||
* @returns 路线规划结果
|
||||
*/
|
||||
export async function planTransitRoute(
|
||||
from: RoutePoint,
|
||||
to: RoutePoint
|
||||
): Promise<RouteResult> {
|
||||
return new Promise((resolve) => {
|
||||
uni.request({
|
||||
url: `${TENCENT_DIRECTION_BASE_URL}/transit`,
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
data: {
|
||||
from: `${from.longitude},${from.latitude}`,
|
||||
to: `${to.longitude},${to.latitude}`,
|
||||
key: TENCENT_MAP_KEY,
|
||||
policy: 'LEAST_TIME' // 选择时间最短的路线
|
||||
},
|
||||
success: (response) => {
|
||||
const data = response.data as any
|
||||
if (response.statusCode === 200 && data && data.status === 0) {
|
||||
const routes: Route[] = data.result.routes.map((route: any, index: number) => {
|
||||
// 公交路线使用完整的路线点
|
||||
const allPoints: RoutePoint[] = []
|
||||
route.steps.forEach((step: any) => {
|
||||
if (step.polyline) {
|
||||
allPoints.push(...decodePolyline(step.polyline))
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id: index,
|
||||
startPoint: from,
|
||||
endPoint: to,
|
||||
distance: route.distance,
|
||||
duration: route.duration,
|
||||
description: route.description,
|
||||
polyline: allPoints,
|
||||
steps: route.steps.map((step: any) => ({
|
||||
startPoint: {
|
||||
latitude: step.start_point.lat,
|
||||
longitude: step.start_point.lng
|
||||
},
|
||||
endPoint: {
|
||||
latitude: step.end_point.lat,
|
||||
longitude: step.end_point.lng
|
||||
},
|
||||
instruction: step.instruction,
|
||||
distance: step.distance,
|
||||
duration: step.duration,
|
||||
direction: step.direction
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
resolve({
|
||||
status: 0,
|
||||
message: 'ok',
|
||||
routes
|
||||
})
|
||||
} else {
|
||||
resolve({
|
||||
status: data?.status ?? -1,
|
||||
message: data?.message ?? '路线规划失败'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('公交路线规划失败:', err)
|
||||
resolve({
|
||||
status: -1,
|
||||
message: '网络请求失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索地点 POI
|
||||
* @param keyword 搜索关键词
|
||||
* @param region 搜索城市/区域
|
||||
* @param pageIndex 页码
|
||||
* @param pageSize 每页数量
|
||||
*/
|
||||
export function searchPoi(
|
||||
keyword: string,
|
||||
region: string = '深圳市',
|
||||
pageIndex: number = 1,
|
||||
pageSize: number = 10
|
||||
): Promise<TencentSearchResult> {
|
||||
const boundary = `region(${region},0)`
|
||||
|
||||
return new Promise((resolve) => {
|
||||
uni.request({
|
||||
url: `${TENCENT_MAP_BASE_URL}/search`,
|
||||
method: 'GET',
|
||||
data: {
|
||||
keyword,
|
||||
boundary,
|
||||
key: TENCENT_MAP_KEY,
|
||||
page_index: pageIndex,
|
||||
page_size: pageSize
|
||||
},
|
||||
success: (response) => {
|
||||
const data = response.data as TencentSearchResult
|
||||
if (response.statusCode === 200 && data) {
|
||||
resolve(data)
|
||||
} else {
|
||||
resolve({
|
||||
status: -1,
|
||||
message: '请求失败',
|
||||
count: 0,
|
||||
data: []
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('腾讯地图 POI 搜索失败:', err)
|
||||
resolve({
|
||||
status: -1,
|
||||
message: '网络请求失败',
|
||||
count: 0,
|
||||
data: []
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索深圳自然博物馆入口
|
||||
* @returns 入口点位信息
|
||||
*/
|
||||
export async function searchMuseumEntrance(): Promise<TencentPoiResult | null> {
|
||||
// 先搜索博物馆
|
||||
const museumResult = await searchPoi('深圳自然博物馆', '深圳市', 1, 5)
|
||||
|
||||
if (museumResult.status !== 0 || museumResult.data.length === 0) {
|
||||
console.warn('未找到深圳自然博物馆')
|
||||
return null
|
||||
}
|
||||
|
||||
const museum = museumResult.data[0]
|
||||
console.log('找到深圳自然博物馆:', museum)
|
||||
|
||||
// 搜索博物馆周边的入口
|
||||
const entranceKeywords = ['入口', '出入口', '访客入口', '主入口']
|
||||
const entranceResults: TencentPoiResult[] = []
|
||||
|
||||
for (const keyword of entranceKeywords) {
|
||||
const result = await searchNearbyPoi(
|
||||
museum.location.lat,
|
||||
museum.location.lng,
|
||||
keyword,
|
||||
500
|
||||
)
|
||||
|
||||
if (result.status === 0 && result.data.length > 0) {
|
||||
entranceResults.push(...result.data)
|
||||
}
|
||||
}
|
||||
|
||||
if (entranceResults.length === 0) {
|
||||
return museum
|
||||
}
|
||||
|
||||
return entranceResults.sort((a, b) => (a.distance || 0) - (b.distance || 0))[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* 周边搜索
|
||||
*/
|
||||
export function searchNearbyPoi(
|
||||
lat: number,
|
||||
lng: number,
|
||||
keyword: string,
|
||||
radius: number = 1000
|
||||
): Promise<TencentSearchResult> {
|
||||
const boundary = `nearby(${lat},${lng},${radius})`
|
||||
|
||||
return new Promise((resolve) => {
|
||||
uni.request({
|
||||
url: `${TENCENT_MAP_BASE_URL}/search`,
|
||||
method: 'GET',
|
||||
data: {
|
||||
keyword,
|
||||
boundary,
|
||||
key: TENCENT_MAP_KEY,
|
||||
page_size: 10
|
||||
},
|
||||
success: (response) => {
|
||||
const data = response.data as TencentSearchResult
|
||||
if (response.statusCode === 200 && data) {
|
||||
resolve(data)
|
||||
} else {
|
||||
resolve({
|
||||
status: -1,
|
||||
message: '请求失败',
|
||||
count: 0,
|
||||
data: []
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('腾讯地图周边搜索失败:', err)
|
||||
resolve({
|
||||
status: -1,
|
||||
message: '网络请求失败',
|
||||
count: 0,
|
||||
data: []
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取深圳自然博物馆入口坐标
|
||||
*/
|
||||
export async function getMuseumEntranceLocation(): Promise<{ lat: number; lng: number } | null> {
|
||||
const entrance = await searchMuseumEntrance()
|
||||
|
||||
if (entrance) {
|
||||
console.log('深圳自然博物馆入口位置:', {
|
||||
name: entrance.title,
|
||||
address: entrance.address,
|
||||
lat: entrance.location.lat,
|
||||
lng: entrance.location.lng
|
||||
})
|
||||
return entrance.location
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user