feat: 新增 SGS SDK 数据适配器与腾讯地图服务

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
lyf
2026-06-26 04:21:46 +08:00
parent 15fbbec12d
commit cc7c21a285
13 changed files with 2938 additions and 0 deletions

View 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 '将调起手机地图应用进行导航'
}

View 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
}