修复 SGS SDK 导览数据源与模型加载

升级 SDK API 数据契约,补齐 guideStops、业务 POI、诊断与路线数据接口。

修复 SDK 模式下 POI/模型/楼层混用静态数据的问题,并为 ThreeMap 模型加载增加短退避重试。
This commit is contained in:
lyf
2026-07-02 10:26:52 +08:00
parent 9efcef5190
commit 7cda427de9
13 changed files with 966 additions and 104 deletions

View File

@@ -350,6 +350,7 @@ const floorExitConfirmThresholdMultiplier = 1.34
const floorExitZoomOutIntentMinDeltaRatio = 0.015
const floorExitAccumulatedZoomOutRatio = 0.26
const floorExitCandidateHoldMs = 900
const modelLoadRetryDelaysMs = [300, 900]
const syncControlInteractionOptions = () => {
if (!controls) return
@@ -2165,7 +2166,11 @@ const clearSceneData = () => {
clearPoiGroupChildren()
}
const loadModel = (
const wait = (delayMs: number) => new Promise<void>((resolve) => {
window.setTimeout(resolve, delayMs)
})
const loadModelOnce = (
url: string,
label: string,
loadToken?: number,
@@ -2193,6 +2198,41 @@ const loadModel = (
)
})
const loadModel = async (
url: string,
label: string,
loadToken?: number,
options: LoadModelOptions = {}
) => {
let lastError: unknown = null
for (let attempt = 0; attempt <= modelLoadRetryDelaysMs.length; attempt += 1) {
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
throw createStaleModelLoadError()
}
try {
return await loadModelOnce(url, label, loadToken, options)
} catch (error) {
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
throw createStaleModelLoadError()
}
lastError = error
const retryDelay = modelLoadRetryDelaysMs[attempt]
if (retryDelay === undefined) break
if (!options.suppressProgress) {
setProgress(18, `${label} 加载失败,正在重试 ${attempt + 1}/${modelLoadRetryDelaysMs.length}`)
}
await wait(retryDelay)
}
}
throw lastError instanceof Error ? lastError : new Error('模型资源加载失败')
}
const prepareModel = (model: THREE.Object3D) => {
model.traverse((child) => {
if (child instanceof THREE.Mesh) {

View File

@@ -6,10 +6,12 @@ import type {
GuideLocationPreview,
GuideMapDiagnostics,
GuideRouteReadiness,
MuseumCategory,
MuseumFloor,
MuseumPoi
} from '@/domain/museum'
MuseumCategory,
MuseumFloor,
MuseumPoi,
ExplainGuideStop,
AudioPlayTargetType
} from '@/domain/museum'
import {
NAV_ROUTE_UNAVAILABLE_MESSAGE
} from '@/domain/guideReadiness'
@@ -17,23 +19,30 @@ import {
isIndoorNavigableFloor
} from '@/domain/guideFloor'
import type {
SgsFloorDiagnosticsPayload,
SgsMapDiagnosticsPayload,
SgsNavigablePlacePayload,
SgsPoiPayload,
SgsFloorDiagnosticsPayload,
SgsGuideStopPayload,
SgsMapDiagnosticsPayload,
SgsNavigablePlacePayload,
SgsPoiPayload,
SgsPositionPayload,
SgsSdkFloorSummaryPayload,
SgsSdkManifestPayload,
SgsSpacePayload
} from '@/data/providers/sgsSdkApiProvider'
const defaultCategory: MuseumCategory = {
id: 'operation_experience',
label: '导览点位',
iconType: 'poi'
}
const hallCategory: MuseumCategory = {
const defaultCategory: MuseumCategory = {
id: 'operation_experience',
label: '导览点位',
iconType: 'poi'
}
const businessCategory: MuseumCategory = {
id: 'business_poi',
label: '运营服务',
iconType: 'business'
}
const hallCategory: MuseumCategory = {
id: 'exhibition_hall',
label: '展厅',
iconType: 'exhibition_hall'
@@ -45,7 +54,7 @@ const hallEntranceCategory: MuseumCategory = {
iconType: 'hall_entrance'
}
const categoryBySgsType: Record<string, MuseumCategory & { accessible?: boolean }> = {
const categoryBySgsType: Record<string, MuseumCategory & { accessible?: boolean }> = {
toilet: {
id: 'basic_service_facility',
label: '卫生间',
@@ -237,6 +246,16 @@ export interface SgsHallPoiDiagnostics {
skippedPlaceCount: number
}
const businessTypeLabels: Record<string, string> = {
shop: '商店',
restaurant: '餐饮',
cafe: '咖啡',
vending: '售卖机',
bookstore: '书店',
cultural_shop: '文创商店',
photo_spot: '打卡点'
}
interface SgsHallPoiBuildOptions {
fallbackY?: number
}
@@ -381,13 +400,23 @@ const normalizePlacePosition = (
fallbackY
)
const categoryFor = (poi: SgsPoiPayload): MuseumCategory & { accessible?: boolean } => {
const normalizedType = poi.type?.trim()
if (normalizedType && categoryBySgsType[normalizedType]) {
return categoryBySgsType[normalizedType]
}
if (poi.typeName) {
const categoryFor = (poi: SgsPoiPayload): MuseumCategory & { accessible?: boolean } => {
const normalizedType = poi.type?.trim().toLowerCase()
const normalizedBusinessType = poi.businessType?.trim().toLowerCase()
if (String(poi.poiGroup || '').toUpperCase() === 'BUSINESS') {
return {
...businessCategory,
label: normalizedBusinessType ? businessTypeLabels[normalizedBusinessType] || poi.typeName || businessCategory.label : poi.typeName || businessCategory.label,
iconType: normalizedBusinessType || businessCategory.iconType
}
}
if (normalizedType && categoryBySgsType[normalizedType]) {
return categoryBySgsType[normalizedType]
}
if (poi.typeName) {
return {
...defaultCategory,
label: poi.typeName,
@@ -395,8 +424,15 @@ const categoryFor = (poi: SgsPoiPayload): MuseumCategory & { accessible?: boolea
}
}
return defaultCategory
}
return defaultCategory
}
const kindForSgsPoi = (poi: SgsPoiPayload, category: MuseumCategory) => {
if (category.id === 'business_poi') return 'facility'
if (category.id === 'operation_experience') return 'guide'
return 'facility'
}
export const toMuseumPoiFromSgs = (
poi: SgsPoiPayload,
@@ -425,12 +461,41 @@ export const toMuseumPoiFromSgs = (
],
positionGltf: normalizePosition(poi),
sourceObjectName: poi.anchorNodeName || undefined,
sourceConfidence: 'backend-sgs-sdk',
navigationReadiness: '位置预览',
accessible: category.accessible === true,
kind: category.id === 'operation_experience' ? 'guide' : 'facility'
}
}
sourceConfidence: 'backend-sgs-sdk',
navigationReadiness: '位置预览',
accessible: category.accessible === true,
kind: kindForSgsPoi(poi, category)
}
}
const normalizeAudioTargetType = (targetType?: string | null): AudioPlayTargetType | undefined => {
const normalized = targetType?.toUpperCase()
if (normalized === 'ITEM' || normalized === 'STOP') return normalized
return undefined
}
export const toExplainGuideStopFromSgs = (stop: SgsGuideStopPayload): ExplainGuideStop | null => {
const id = stringifyId(stop.id)
if (!id) return null
return {
id,
name: normalizedText(stop.name) || `讲解点${id}`,
hallId: stringifyId(stop.hallId) || undefined,
hallName: normalizedText(stop.hallName) || undefined,
floorId: stringifyId(stop.floorId) || undefined,
targetType: normalizeAudioTargetType(stop.targetType) || 'STOP',
targetId: stringifyId(stop.targetId) || id,
coverImageUrl: normalizedText(stop.coverImageUrl) || undefined,
description: normalizedText(stop.description) || undefined,
hasAudio: stop.hasAudio === true || Boolean(normalizedText(stop.audioUrl)),
poiId: stringifyId(stop.poiId) || undefined,
outlineId: stringifyId(stop.outlineId || stop.routeId) || undefined,
outlineName: normalizedText(stop.outlineName || stop.routeName) || undefined,
sort: optionalNumber(stop.sort ?? stop.seqOrder)
}
}
export const toLocationPreviewFromPoi = (poi: MuseumPoi): GuideLocationPreview => ({
poiId: poi.id,

View File

@@ -10,17 +10,22 @@ import {
import type {
ExplainContentProvider
} from '@/data/providers/staticMuseumContentProvider'
import {
defaultSgsSdkApiProvider,
type SgsSdkApiProvider
} from '@/data/providers/sgsSdkApiProvider'
import {
toBackendExplainTrack,
toBackendGuideStop,
toBackendHallFromList,
toBackendMediaAsset,
toBackendMuseumExhibit,
groupGuideStopsByOutline,
type BackendExhibit,
type BackendGuideStop,
type BackendHall
} from '@/data/adapters/backendExplainDataAdapter'
import {
toExplainGuideStopFromSgs
} from '@/data/adapters/sgsSdkGuideAdapter'
interface CommonResult<T> {
code: number
@@ -28,7 +33,14 @@ interface CommonResult<T> {
data?: T
}
const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '')
const normalizeBaseUrl = (baseUrl: string) => {
const trimmed = baseUrl.trim().replace(/\/+$/, '')
if (!trimmed || /^https?:\/\//i.test(trimmed) || trimmed.startsWith('/')) {
return trimmed
}
return `/${trimmed}`
}
const resolveAppApiBaseUrl = () => {
const baseUrl = normalizeBaseUrl(dataSourceConfig.apiBaseUrl)
@@ -77,7 +89,10 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
private readonly guideStopCache = new Map<string, ExplainGuideStop[]>()
private readonly guideStopInflight = new Map<string, Promise<ExplainGuideStop[]>>()
constructor(private readonly fallbackProvider: ExplainContentProvider) {}
constructor(
private readonly fallbackProvider: ExplainContentProvider,
private readonly sgsSdkApiProvider: SgsSdkApiProvider = defaultSgsSdkApiProvider
) {}
private async requestStaticExplainExhibits() {
const cached = this.searchCache.get('')
@@ -223,14 +238,8 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
if (inflight) return inflight
const promise = (async () => {
const url = `${resolveAppApiBaseUrl()}/gis/sdk/halls/${encodeURIComponent(normalizedHallId)}/guide-stops`
const response = await requestJson<CommonResult<BackendGuideStop[]>>(url)
if (response.code !== 0 || !Array.isArray(response.data)) {
throw new Error(response.msg || '后端展厅讲解点加载失败')
}
const stops = response.data
.map(toBackendGuideStop)
const stops = (await this.sgsSdkApiProvider.getGuideStopsByHall(normalizedHallId))
.map(toExplainGuideStopFromSgs)
.filter(Boolean) as ExplainGuideStop[]
this.guideStopCache.set(normalizedHallId, stops)

View File

@@ -26,6 +26,27 @@ export interface SgsSdkManifestPayload {
capabilities?: Record<string, boolean | undefined>
}
export type SgsPoiGroupPayload = 'SERVICE' | 'BUSINESS' | 'OTHER'
export type SgsBusinessPoiTypePayload =
| 'shop'
| 'restaurant'
| 'cafe'
| 'vending'
| 'bookstore'
| 'cultural_shop'
| 'photo_spot'
| string
export interface SgsPoiQueryParamsPayload {
mapId?: string | number
floorId?: string | number
group?: SgsPoiGroupPayload
types?: string[]
keyword?: string
limit?: number
}
export interface SgsPositionPayload {
x?: number | null
y?: number | null
@@ -47,6 +68,14 @@ export interface SgsPoiPayload {
anchorNodeName?: string | null
description?: string | null
iconUrl?: string | null
poiGroup?: SgsPoiGroupPayload | string | null
businessType?: SgsBusinessPoiTypePayload | null
coverImageUrl?: string | null
phone?: string | null
businessHours?: string | null
sortOrder?: number | null
spatialAreaId?: string | number | null
spatialAreaName?: string | null
}
export interface SgsSpacePayload {
@@ -79,6 +108,36 @@ export interface SgsNavigablePlacePayload {
ownerName?: string | null
}
export interface SgsGuideStopPayload {
id: string | number
name?: string | null
type?: string | null
typeName?: string | null
floorId?: string | number | null
position?: SgsPositionPayload | null
x?: number | null
y?: number | null
z?: number | null
targetType?: string | null
targetId?: string | number | null
audioUrl?: string | null
coverImageUrl?: string | null
description?: string | null
status?: string | null
located?: boolean | null
hasAudio?: boolean | null
poiId?: string | number | null
outlineId?: string | number | null
outlineName?: string | null
hallId?: string | number | null
hallName?: string | null
sort?: number | null
routeId?: string | number | null
routeName?: string | null
seqOrder?: number | null
stayMinutes?: number | null
}
export interface SgsFloorDiagnosticsPayload {
floorId: string | number
floorCode?: string | null
@@ -121,7 +180,7 @@ export interface SgsFloorBundlePayload {
model?: SgsModelInfoPayload | null
pois?: SgsPoiPayload[]
spaces?: SgsSpacePayload[]
guideStops?: unknown[]
guideStops?: SgsGuideStopPayload[]
routeSummary?: {
hasRouteNetwork?: boolean
nodeCount?: number
@@ -196,6 +255,54 @@ export interface SgsRoutePlanResponsePayload {
segments?: SgsRouteSegmentPayload[]
}
export interface SgsRouteEndpointPayload {
id?: string | number | null
name?: string | null
floorId?: string | number | null
floorCode?: string | null
floorName?: string | null
x?: number | null
y?: number | null
z?: number | null
}
export interface SgsFeaturedRouteSummaryPayload {
id: string | number
mapId?: string | number | null
routeName?: string | null
routeNameEn?: string | null
routeType?: string | null
theme?: string | null
description?: string | null
coverImageUrl?: string | null
estimatedDuration?: number | null
distanceMeters?: number | null
waypointCount?: number | null
sortOrder?: number | null
status?: string | number | null
}
export interface SgsFeaturedRouteWaypointPayload {
order?: number | null
spaceId?: string | number | null
spaceName?: string | null
guideStopId?: string | number | null
guideStopName?: string | null
floorId?: string | number | null
floorCode?: string | null
floorName?: string | null
x?: number | null
y?: number | null
z?: number | null
stayMinutes?: number | null
}
export interface SgsFeaturedRouteDetailPayload extends SgsFeaturedRouteSummaryPayload {
startEntrance?: SgsRouteEndpointPayload | null
endEntrance?: SgsRouteEndpointPayload | null
waypoints?: SgsFeaturedRouteWaypointPayload[]
}
interface CommonResult<T> {
code: number
msg?: string
@@ -209,7 +316,19 @@ export interface SgsSdkApiProvider {
getFloorBundle(floorId: string): Promise<SgsFloorBundlePayload>
getFloorPois(floorId: string): Promise<SgsPoiPayload[]>
getFloorSpaces(floorId: string): Promise<SgsSpacePayload[]>
getGuideStops(floorId: string): Promise<SgsGuideStopPayload[]>
getGuideStopsByHall(hallId: string): Promise<SgsGuideStopPayload[]>
getNavigablePlaces(floorId: string): Promise<SgsNavigablePlacePayload[]>
getFloorBusinessPois(
floorId: string,
options?: { types?: string[]; keyword?: string; limit?: number }
): Promise<SgsPoiPayload[]>
queryPois(params: SgsPoiQueryParamsPayload): Promise<SgsPoiPayload[]>
getFeaturedRoutes(
mapId?: string,
options?: { limit?: number }
): Promise<SgsFeaturedRouteSummaryPayload[]>
getFeaturedRoute(routeId: string): Promise<SgsFeaturedRouteDetailPayload>
planRoute(payload: SgsRoutePlanRequestPayload): Promise<SgsRoutePlanResponsePayload>
}
@@ -295,6 +414,21 @@ const requestJson = <T>(
})
})
const buildQueryString = (params: object) => {
const query = new URLSearchParams()
Object.entries(params as Record<string, unknown>).forEach(([key, value]) => {
if (value === null || typeof value === 'undefined' || value === '') return
query.set(key, Array.isArray(value) ? value.join(',') : String(value))
})
const queryString = query.toString()
return queryString ? `?${queryString}` : ''
}
const stableCacheKey = (value: unknown) => JSON.stringify(value)
export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
const manifestCache = new Map<string, SgsSdkManifestPayload>()
const mapDiagnosticsCache = new Map<string, SgsMapDiagnosticsPayload>()
@@ -302,10 +436,17 @@ export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
const floorBundleCache = new Map<string, SgsFloorBundlePayload>()
const floorPoiCache = new Map<string, SgsPoiPayload[]>()
const floorSpaceCache = new Map<string, SgsSpacePayload[]>()
const floorGuideStopCache = new Map<string, SgsGuideStopPayload[]>()
const hallGuideStopCache = new Map<string, SgsGuideStopPayload[]>()
const navigablePlaceCache = new Map<string, SgsNavigablePlacePayload[]>()
const floorBusinessPoiCache = new Map<string, SgsPoiPayload[]>()
const poiQueryCache = new Map<string, SgsPoiPayload[]>()
const featuredRoutesCache = new Map<string, SgsFeaturedRouteSummaryPayload[]>()
const featuredRouteDetailCache = new Map<string, SgsFeaturedRouteDetailPayload>()
const mapIdFor = (mapId?: string) => mapId || dataSourceConfig.sgsMapId
const floorIdFor = (floorId: string) => encodeURIComponent(floorId)
const hallIdFor = (hallId: string) => encodeURIComponent(hallId)
return {
async getManifest(mapId) {
@@ -370,6 +511,26 @@ export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
floorSpaceCache.set(floorId, spaces)
return spaces
},
async getGuideStops(floorId) {
const cached = floorGuideStopCache.get(floorId)
if (cached) return cached
const guideStops = await requestJson<SgsGuideStopPayload[]>(
`/gis/sdk/floors/${floorIdFor(floorId)}/guide-stops`
)
floorGuideStopCache.set(floorId, guideStops)
return guideStops
},
async getGuideStopsByHall(hallId) {
const cached = hallGuideStopCache.get(hallId)
if (cached) return cached
const guideStops = await requestJson<SgsGuideStopPayload[]>(
`/gis/sdk/halls/${hallIdFor(hallId)}/guide-stops`
)
hallGuideStopCache.set(hallId, guideStops)
return guideStops
},
async getNavigablePlaces(floorId) {
const cached = navigablePlaceCache.get(floorId)
if (cached) return cached
@@ -380,6 +541,50 @@ export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
navigablePlaceCache.set(floorId, places)
return places
},
async getFloorBusinessPois(floorId, options = {}) {
const cacheKey = `${floorId}:${stableCacheKey(options)}`
const cached = floorBusinessPoiCache.get(cacheKey)
if (cached) return cached
const pois = await requestJson<SgsPoiPayload[]>(
`/gis/sdk/floors/${floorIdFor(floorId)}/business-pois${buildQueryString(options)}`
)
floorBusinessPoiCache.set(cacheKey, pois)
return pois
},
async queryPois(params) {
const cacheKey = stableCacheKey(params)
const cached = poiQueryCache.get(cacheKey)
if (cached) return cached
const pois = await requestJson<SgsPoiPayload[]>(
`/gis/sdk/pois${buildQueryString(params)}`
)
poiQueryCache.set(cacheKey, pois)
return pois
},
async getFeaturedRoutes(mapId, options = {}) {
const normalizedMapId = mapIdFor(mapId)
const cacheKey = `${normalizedMapId}:${stableCacheKey(options)}`
const cached = featuredRoutesCache.get(cacheKey)
if (cached) return cached
const routes = await requestJson<SgsFeaturedRouteSummaryPayload[]>(
`/gis/sdk/maps/${encodeURIComponent(normalizedMapId)}/featured-routes${buildQueryString(options)}`
)
featuredRoutesCache.set(cacheKey, routes)
return routes
},
async getFeaturedRoute(routeId) {
const cached = featuredRouteDetailCache.get(routeId)
if (cached) return cached
const route = await requestJson<SgsFeaturedRouteDetailPayload>(
`/gis/sdk/featured-routes/${encodeURIComponent(routeId)}`
)
featuredRouteDetailCache.set(routeId, route)
return route
},
async planRoute(payload) {
return requestJson<SgsRoutePlanResponsePayload>(
'/gis/sdk/routes/plan',

View File

@@ -96,12 +96,8 @@ import type {
GuideModelSource,
GuideRenderPoi
} from '@/domain/guideModel'
import {
StaticGuideModelRepository
} from '@/repositories/GuideModelRepository'
const fallbackIndoorModelSource = new StaticGuideModelRepository()
const indoorModelSource = ref<GuideModelSource>(fallbackIndoorModelSource)
const indoorModelSource: GuideModelSource = guideUseCase.getModelSource()
const guideFloors = ref<MuseumFloor[]>([])
const normalizeGuideFloorId = (labelOrId: string) =>
guideFloors.value.find((floor) => floor.id === labelOrId || floor.label === labelOrId)?.id
@@ -123,19 +119,16 @@ const facility = ref<FacilityDetailViewModel>({
const loadGuideFloors = async () => {
try {
const modelPackage = await fallbackIndoorModelSource.loadPackage()
guideFloors.value = modelPackage.floors.map((floor) => ({
id: floor.floorId,
label: floor.label,
order: floor.order
}))
guideFloors.value = await guideUseCase.getFloors()
const floors = guideFloors.value
const defaultFloor = floors.find((floor) => floor.label === activeFloor.value)?.label
|| floors.find((floor) => floor.id === 'L1')?.label
|| floors[0]?.label
const normalizedActiveFloorId = activeFloor.value
? normalizeGuideFloorId(activeFloor.value)
: ''
const defaultFloor = floors.find((floor) => floor.id === normalizedActiveFloorId)?.id
|| floors.find((floor) => floor.label === '1F')?.id
|| floors[0]?.id
|| activeFloor.value
activeFloor.value = floors.find((floor) => floor.label === activeFloor.value)?.label
|| defaultFloor
activeFloor.value = defaultFloor
} catch (error) {
console.error('加载导览楼层失败:', error)
guideFloors.value = []
@@ -146,7 +139,7 @@ const focusFacilityPoi = (poi: MuseumPoi) => {
if (!poi.floorId) return
activeMode.value = '3d'
activeFloor.value = poi.floorLabel
activeFloor.value = poi.floorId
targetFocusRequestId.value += 1
targetFocusRequest.value = toTargetFocusRequestViewModel({
poiId: poi.id,
@@ -164,7 +157,7 @@ const focusRenderPoi = (poi: GuideRenderPoi) => {
const floorLabel = guideFloors.value.find((floor) => floor.id === poi.floorId)?.label || poi.floorId
activeMode.value = '3d'
activeFloor.value = floorLabel
activeFloor.value = poi.floorId
targetFocusRequestId.value += 1
targetFocusRequest.value = toTargetFocusRequestViewModel({
poiId: poi.id,
@@ -242,10 +235,10 @@ const resolveRenderPoiByName = async () => {
const normalizedTarget = normalizePoiName(facility.value.name)
if (!normalizedTarget) return null
const modelPackage = await indoorModelSource.value.loadPackage()
const modelPackage = await indoorModelSource.loadPackage()
for (const floor of modelPackage.floors) {
const floorId = floor.floorId
const pois = await indoorModelSource.value.loadFloorPois(floorId).catch(() => [])
const pois = await indoorModelSource.loadFloorPois(floorId).catch(() => [])
const matchedPoi = pois.find((poi) => {
const poiName = normalizePoiName(poi.name)
const hallName = normalizePoiName(poi.hallName || '')

View File

@@ -255,7 +255,6 @@ export class SgsSdkGuideRepository implements GuideRepository {
private floorsCache: MuseumFloor[] | null = null
private poiCache: MuseumPoi[] | null = null
private floorAliases: Map<string, string> | null = null
private readonly staticFallback = new StaticGuideRepository()
constructor(private readonly provider: SgsSdkApiProvider = defaultSgsSdkApiProvider) {}
@@ -331,7 +330,7 @@ export class SgsSdkGuideRepository implements GuideRepository {
const pois = await this.listPois()
return pois.find((poi) => poi.id === id)
|| findPoiByNavIdNameFallback(pois, id)
|| this.staticFallback.getPoiById(id)
|| null
}
async searchPois(keyword = '') {

View File

@@ -14,6 +14,7 @@ import {
} from '@/repositories/GuideRouteRepository'
import {
defaultSgsSdkApiProvider,
type SgsFloorDiagnosticsPayload,
type SgsNavigablePlacePayload,
type SgsSdkApiProvider
} from '@/data/providers/sgsSdkApiProvider'
@@ -29,13 +30,40 @@ const createUnavailableReadiness = (message: string): GuideRouteReadiness => ({
})
const logSdkRouteReadinessIssue = (message: string, payload: Record<string, unknown>) => {
console.error(`[SDKRouteReadiness] ${message}`, payload)
console.warn(`[SDKRouteReadiness] ${message}`, payload)
}
const toNumber = (value: unknown) => (
Number.isFinite(Number(value)) ? Number(value) : undefined
)
const isPositiveNumber = (value: unknown) => {
const numberValue = toNumber(value)
return typeof numberValue === 'number' && numberValue > 0
}
const hasRouteNodeId = (place: SgsNavigablePlacePayload) => (
place.nodeId !== null && place.nodeId !== undefined && place.nodeId !== ''
)
const hasRouteGraphEvidence = (floor: Pick<SgsFloorDiagnosticsPayload, 'routePlanningReady' | 'routeNodeCount' | 'routeEdgeCount'>) => (
floor.routePlanningReady === true
|| (isPositiveNumber(floor.routeNodeCount) && isPositiveNumber(floor.routeEdgeCount))
)
const hasRouteTargetEvidence = (
floor: Pick<SgsFloorDiagnosticsPayload, 'navigablePlaceCount'>,
routeTargetCount?: number
) => {
if (typeof routeTargetCount === 'number') return routeTargetCount > 0
return floor.navigablePlaceCount === undefined || isPositiveNumber(floor.navigablePlaceCount)
}
const isRouteReadyFromDiagnostics = (
floor: Pick<SgsFloorDiagnosticsPayload, 'routePlanningReady' | 'routeNodeCount' | 'routeEdgeCount' | 'navigablePlaceCount'>,
routeTargetCount?: number
) => hasRouteGraphEvidence(floor) && hasRouteTargetEvidence(floor, routeTargetCount)
const positionFromPlace = (place: SgsNavigablePlacePayload): [number, number, number] | undefined => {
const source = place.position || {
x: place.x,
@@ -70,36 +98,42 @@ export class SdkGuideRouteRepository {
return createUnavailableReadiness('SDK 地图楼层数据为空')
}
// 收集有路线数据的楼层
const readyFloors = floors.filter(
(floor) =>
floor.routePlanningReady &&
floor.routeNodeCount &&
floor.routeNodeCount > 0 &&
floor.routeEdgeCount &&
floor.routeEdgeCount > 0
)
// 地图级 diagnostics 的楼层摘要只保证 routePlanningReady/navigablePlaceCount
// routeNodeCount/routeEdgeCount 可能只出现在 floor diagnostics 或 summary 中。
let readyFloors = floors.filter((floor) => isRouteReadyFromDiagnostics(floor))
let notReadyFloors = floors.filter((floor) => !isRouteReadyFromDiagnostics(floor))
if (readyFloors.length === 0) {
const floorEvidence = await Promise.all(
floors.map((floor) => this.loadFloorRouteEvidence(floor))
)
readyFloors = floorEvidence
.filter((item) => item.ready)
.map((item) => item.floor)
notReadyFloors = floorEvidence
.filter((item) => !item.ready)
.map((item) => item.floor)
}
// 没有楼层有路线数据时不可用
if (readyFloors.length === 0) {
logSdkRouteReadinessIssue('SDK 路线数据不完整', {
logSdkRouteReadinessIssue('SDK 路线数据未达到位置关系生成条件', {
floorCount: floors.length,
readyFloorCount: readyFloors.length,
summary: diagnostics.summary,
floors: floors.map((floor) => ({
floorId: floor.floorId,
floorName: floor.floorName,
floorCode: floor.floorCode,
routePlanningReady: floor.routePlanningReady,
routeNodeCount: floor.routeNodeCount,
routeEdgeCount: floor.routeEdgeCount
routeEdgeCount: floor.routeEdgeCount,
navigablePlaceCount: floor.navigablePlaceCount
}))
})
return createUnavailableReadiness('无法定位,当前仅支持点位位置预览')
}
// 收集未就绪的楼层
const notReadyFloors = floors.filter((floor) => !floor.routePlanningReady)
// 如果有楼层未就绪,给出警告但仍然可用
if (notReadyFloors.length > 0) {
const floorLabels = notReadyFloors
@@ -125,6 +159,25 @@ export class SdkGuideRouteRepository {
}
}
private async loadFloorRouteEvidence(floor: SgsFloorDiagnosticsPayload) {
const rawFloorId = stringifyId(floor.floorId)
const [floorDiagnostics, navigablePlaces] = await Promise.all([
this.sdkApiProvider.getFloorDiagnostics(rawFloorId).catch(() => null),
this.sdkApiProvider.getNavigablePlaces(rawFloorId).catch(() => [])
])
const routeTargetCount = navigablePlaces.filter(hasRouteNodeId).length
const evidence = floorDiagnostics || floor
return {
floor: {
...floor,
...floorDiagnostics
},
ready: isRouteReadyFromDiagnostics(evidence, routeTargetCount),
routeTargetCount
}
}
async listRouteTargets(): Promise<GuideRouteTarget[]> {
if (this.targetsCache) return this.targetsCache
@@ -141,7 +194,7 @@ export class SdkGuideRouteRepository {
for (const place of places) {
// 过滤掉没有 nodeId 的点位,这些无法用于路线规划
if (place.nodeId === null || place.nodeId === undefined || place.nodeId === '') {
if (!hasRouteNodeId(place)) {
continue
}

View File

@@ -1,7 +1,7 @@
export type SgsMapNode =
| { type: 'coord'; floorId: string | number; x: number; z: number; y?: number }
| { type: 'poiId'; value: number }
| { type: 'nodeName'; value: string }
| { type: 'poiId'; value: string | number; floorId?: string | number }
| { type: 'nodeName'; value: string; floorId?: string | number }
export interface SgsSnapDetail {
original: { x: number; z: number }
@@ -14,13 +14,14 @@ export interface SgsRouteResult {
distance: number
duration: number
pathPoints: Array<{ x: number; y: number; z: number }>
startSnap: SgsSnapDetail
endSnap: SgsSnapDetail
computeTimeMs: number
polyCount: number
startSnap?: SgsSnapDetail
endSnap?: SgsSnapDetail
computeTimeMs?: number
polyCount?: number
warnings: string[]
errorCode?: string
message?: string
segments?: SgsRouteSegment[]
}
export interface SgsMapSDKOptions {
@@ -28,17 +29,39 @@ export interface SgsMapSDKOptions {
sdkUrl?: string
targetOrigin?: string
floorId?: string | number
mapId?: string | number
timeout?: number
readyTimeout?: number
}
export interface SgsMapEvents {
ready: { engineVersion: string; protocolVersion: number }
poiClick: { id?: number; poiId?: number; name: string; type: string; floorId?: string | number; description?: string }
poiClick: { id: string | number; poiId?: string | number; name: string; type: string; floorId?: string | number; description?: string }
floorChanged: { floorId: string | number }
error: { code: string; message: string }
loadError: { type: string; detail: string }
routeTransfer: {
currentFloorId: string | number
targetFloorId: string | number
fromFloorId: string | number
fromNodeName: string
toNodeName: string
transferType: SgsRouteTransferType
connectorName?: string
segmentIndex: number
totalSegments: number
routeId?: string
}
modelLoading: { floorId: string | number; modelUrl: string }
modelReady: { floorId: string | number; parseTimeMs: number }
modelError: { floorId: string | number; error: string; fallbackUsed: boolean }
floorBundleReady: { floorId: string | number; dataVersion: string }
routeReady: { routeId: string; distance: number; duration: number; segmentCount?: number; hasTransfer?: boolean }
}
export type SgsRouteTransferType = 'ELEVATOR' | 'STAIRS' | 'ESCALATOR'
export type SgsRouteSegmentType = 'WALK' | 'TRANSFER'
export type SgsErrorCode =
| 'ERR_TIMEOUT'
| 'ERR_NO_PATH'
@@ -50,6 +73,9 @@ export type SgsErrorCode =
| 'ERR_UNAUTHORIZED'
| 'ERR_UNSUPPORTED'
| 'ERR_FAILED'
| 'ERR_DESTROYED'
| 'ERR_IFRAME_LOAD'
| 'ERR_READY_TIMEOUT'
export interface SgsMarkerConfig {
id: string
@@ -64,21 +90,316 @@ export interface SgsMapSDKInstance {
off<K extends keyof SgsMapEvents>(event: K, listener: (payload: SgsMapEvents[K]) => void): void
getIsReady(): boolean
getCurrentFloor(): string | number
getMapId(): string | number
getVersion(): string
whenReady(): Promise<unknown>
ready(): Promise<unknown>
focusTo(node: SgsMapNode | string): void
changeFloor(floorId: string | number, timeout?: number): Promise<{ success: boolean; floorId: string | number }>
addMarker(config: SgsMarkerConfig): void
removeMarker(markerId: string): void
clearMarkers(): void
highlightPolygon(nodeName: string): void
highlightPolygon(payload: string | object): void
planRoute(from: SgsMapNode | string, to: SgsMapNode | string, options?: unknown, timeout?: number): Promise<SgsRouteResult>
planRouteV2(from: SgsMapNode, to: SgsMapNode, options?: unknown, timeout?: number): Promise<SgsRouteResult>
clearRoute(): void
getState(): Promise<unknown>
resetView(): void
setViewMode(mode: '2d' | '3d'): void
resetView(timeout?: number): Promise<{ success: boolean }>
setViewMode(mode: '2d' | '3d', timeout?: number): Promise<{ success: boolean; mode: string }>
toggleLayer(types: string[], visible: boolean, timeout?: number): Promise<{ success: boolean; visibleTypes: string[] }>
setVisibleLayers(types: string[], timeout?: number): Promise<{ success: boolean; visibleTypes: string[] }>
setRouteViewActive(active: boolean, timeout?: number): Promise<{ success: boolean; active: boolean }>
getManifest(timeout?: number): Promise<SgsSdkManifest>
loadFloorBundle(floorId: string | number, timeout?: number): Promise<SgsFloorBundle>
getFloorPois(floorId: string | number, timeout?: number): Promise<SgsPoi[]>
getNavigablePlaces(floorId: string | number, timeout?: number): Promise<SgsNavigablePlace[]>
getPois(floorCode: string, timeout?: number): Promise<SgsPoi[]>
getSpaces(floorId: string | number, timeout?: number): Promise<SgsSpaceArea[]>
getGuideStops(floorId: string | number, timeout?: number): Promise<SgsGuideStop[]>
getGuideStopsByHall(hallId: string | number, timeout?: number): Promise<SgsGuideStop[]>
preloadFloor(floorId: string | number, timeout?: number): Promise<{ cached: boolean }>
getPerformanceStats(timeout?: number): Promise<SgsPerformanceStats>
getDiagnostics(timeout?: number): Promise<SgsMapDiagnostics>
getFloorDiagnostics(floorId: string | number, timeout?: number): Promise<SgsFloorDiagnostics>
clearModelCache(): void
getBusinessPois(floorId: string | number, options?: { types?: string[]; keyword?: string; limit?: number }, timeout?: number): Promise<SgsPoi[]>
queryPois(params: SgsPoiQueryParams, timeout?: number): Promise<SgsPoi[]>
getFeaturedRoutes(mapId?: string | number, options?: { limit?: number }, timeout?: number): Promise<SgsFeaturedRouteSummary[]>
getFeaturedRoute(routeId: string | number, timeout?: number): Promise<SgsFeaturedRouteDetail>
showFlowline(routeId: string | number, options?: SgsFlowlineOptions, timeout?: number): Promise<{ success: boolean; routeId: string | number }>
clearFlowline(): void
destroy(): void
}
export type SgsPoiGroup = 'SERVICE' | 'BUSINESS' | 'OTHER'
export type SgsBusinessPoiType =
| 'shop'
| 'restaurant'
| 'cafe'
| 'vending'
| 'bookstore'
| 'cultural_shop'
| 'photo_spot'
| string
export interface SgsPoiQueryParams {
mapId?: string | number
floorId?: string | number
group?: SgsPoiGroup
types?: string[]
keyword?: string
limit?: number
}
export interface SgsPoi {
id: string | number
name: string
type: string
typeName: string
floorCode: string
floorId?: string | number
position: { x: number; y: number; z: number }
status: 'ACTIVE' | 'INACTIVE'
anchorNodeName?: string | null
description?: string
iconUrl?: string
poiGroup?: SgsPoiGroup
businessType?: SgsBusinessPoiType
coverImageUrl?: string | null
phone?: string | null
businessHours?: string | null
sortOrder?: number | null
spatialAreaId?: string | number | null
spatialAreaName?: string | null
}
export interface SgsSdkManifest {
mapId: string | number
mapName: string
sdkVersion: string
protocolVersion: number
dataVersion: string
updatedAt: string
coordinateSystem: string
floors: SgsSdkFloorSummary[]
capabilities: Record<string, boolean | undefined>
}
export interface SgsSdkFloorSummary {
floorId: string | number
floorCode: string
floorName: string
sortOrder: number
modelSizeBytes: number
poiCount: number
spaceCount: number
}
export interface SgsFloorBundle {
floor: SgsSdkFloorSummary
model: {
modelUrl: string
fallbackModelUrl: string
compressionType: 'draco' | 'none'
modelVersion: string
nodeCount: number
sizeBytes: number
originSizeBytes: number
}
pois: SgsPoi[]
spaces: SgsSpaceArea[]
guideStops: SgsGuideStop[]
routeSummary: {
hasRouteNetwork: boolean
nodeCount: number
edgeCount: number
}
hiddenSceneNodeNames: string[]
dataVersion: string
updatedAt: string
}
export interface SgsSpaceArea {
id: string | number
name: string
type: string
floorId: string | number
boundaryWkt: string
center: { x: number; y: number; z: number }
sourceNodeName?: string
status: 'ACTIVE' | 'INACTIVE'
colorHex?: string
}
export interface SgsGuideStop {
id: string | number
name: string
type: string
typeName?: string
floorId: string | number
position: { x: number; y: number; z: number }
targetType: string
targetId?: string | number
audioUrl?: string
coverImageUrl?: string
description?: string
status: 'ACTIVE' | 'INACTIVE'
located?: boolean
hasAudio?: boolean
poiId?: string | number
outlineId?: string | number
outlineName?: string
hallId?: string | number
hallName?: string
sort?: number
routeId?: string | number
routeName?: string
seqOrder?: number
stayMinutes?: number
}
export interface SgsNavigablePlace {
id: string | number
name: string
category: 'door' | 'poi' | 'space' | 'guide_stop'
floorId: string | number
floorCode: string
floorName?: string
x: number
z: number
nodeId?: string | number
ownerName?: string
typeCode?: string
typeName?: string
}
export interface SgsRouteSegment {
segmentIndex: number
floorId?: string | number | null
floorCode: string
floorName: string
segmentType?: SgsRouteSegmentType
type: 'WALK' | SgsRouteTransferType
transferType?: 'WALK' | SgsRouteTransferType
distance: number
duration: number
pathPoints: Array<{ x: number; y: number; z: number }>
points?: Array<{ x: number; y: number; z: number }>
fromNodeName?: string | null
toNodeName?: string | null
startNodeName?: string | null
endNodeName?: string | null
connectorName?: string | null
}
export interface SgsPerformanceStats {
sdkCreatedAt?: number
iframeDomReadyMs?: number
engineReadyMs?: number
manifestLoadMs?: number
bundleLoadMs?: number
modelDownloadMs?: number
modelDownloadAndParseMs?: number
modelParseMs?: number
firstRenderMs?: number
routePlanMs?: number
dracoUsed?: boolean
fallbackUsed?: boolean
currentFloorId?: string | number
}
export interface SgsMapDiagnostics {
mapId: string | number
mapName: string
sdkVersion: string
status: 'OK' | 'WARN' | 'ERROR'
floors: SgsFloorDiagnosticsSummary[]
summary: {
floorCount: number
modelReadyFloorCount: number
poiCount: number
spaceCount: number
guideStopCount: number
navigablePlaceCount: number
routeNodeCount: number
routeEdgeCount: number
}
warnings: string[]
}
export interface SgsFloorDiagnosticsSummary {
floorId: string | number
floorCode: string
floorName: string
status: 'OK' | 'WARN' | 'ERROR'
modelReady: boolean
routePlanningReady: boolean
navigablePlaceCount: number
}
export interface SgsFloorDiagnostics extends SgsFloorDiagnosticsSummary {
poiCount: number
spaceCount: number
guideStopCount: number
routeNodeCount: number
routeEdgeCount: number
warnings: string[]
}
export interface SgsFeaturedRouteSummary {
id: string | number
mapId: string | number
routeName: string
routeNameEn?: string | null
description?: string | null
descriptionEn?: string | null
coverImageUrl?: string | null
routeColor?: string | null
estimatedMinutes?: number | null
displayOrder?: number | null
waypointCount: number
startEntranceId?: string | number | null
endEntranceId?: string | number | null
updatedAt?: string | null
}
export interface SgsRouteEndpoint {
id: string | number
name: string
floorId: string | number
floorCode?: string | null
position?: { x: number; y: number; z: number } | null
}
export interface SgsFeaturedRouteWaypoint {
order: number
spaceId: string | number
spaceName?: string | null
floorId?: string | number | null
floorCode?: string | null
position: { x: number; y: number; z: number }
title?: string | null
subtitle?: string | null
description?: string | null
imageUrl?: string | null
audioUrl?: string | null
staySeconds?: number | null
tags?: string[]
highlight?: boolean
}
export interface SgsFeaturedRouteDetail extends SgsFeaturedRouteSummary {
startEntrance?: SgsRouteEndpoint | null
endEntrance?: SgsRouteEndpoint | null
waypoints: SgsFeaturedRouteWaypoint[]
}
export interface SgsFlowlineOptions {
autoFit?: boolean
showLabels?: boolean
animate?: boolean
}
export type SgsMapSDKConstructor = new (options: SgsMapSDKOptions) => SgsMapSDKInstance
declare global {

View File

@@ -1,5 +1,20 @@
# SGS Map SDK Changelog
## [2.4.0] - 2026-06-27
### 🚀 新特性 (New Features)
- **业务 POI 接口与扩展机制**:新增 `getBusinessPois` 获取商户级业务 POI新增 `queryPois` 支持按地图、楼层、分组、类型、关键词进行组合查询,实现与底层空间基础设施数据解耦。
- **特色动线查询与展示**:新增 `getFeaturedRoutes` / `getFeaturedRoute` 接口获取馆方精选路线,新增 `showFlowline` / `clearFlowline` 接口提供跨层全局鸟瞰特色动线展示功能。
- **独立 H5 渲染引擎包 (SDK Engine)**:新增 `sdk-engine/` 独立打包构建目录,支持静态编译输出至静态 `engine/` 目录。底座不依赖 Next.js 前端路由及 hydration 状态限制,由宿主系统通过 iframe 挂载 `../engine/index.html` 独立运行。
- **配置与数据解耦**:底座数据服务入口由 `server` 查询参数动态指定,静态部署后可通过 `?server=http://localhost:3001` 指向任意数据服务地址,彻底解耦。
### 🐞 修复与强化 (Fixes & Improvements)
- **解耦 Next.js 依赖**:底座引擎完全脱离 Next.js / Zustand 等大体积依赖精简为极速加载的原生单页应用Vite + React
- **优化发包构建管线**:重新梳理 `package.json` scripts将 SDK Bridge 与 SDK Engine 打包构建命令在命令行端进行解耦和串联(`npm run build:sdk`),优化了 `scripts/copy-sdk.js` 拷贝管线,避免嵌套与死循环构建。
- **演示 Demo 改造**:修改 SDK 官方 Demo 的 `app.js` 使其默认消费静态 `../engine/index.html` 渲染底座,并开启 `server` 参数透传及 `targetOrigin: '*'` 确权广播,实现宿主与静态底座零配置无缝通信。
---
## [2.3.0] - 2026-06-23
### 🚀 新特性 (New Features)

View File

@@ -1,6 +1,6 @@
# SGS Map SDK 交付包
> **版本**V2.3.0
> **版本**V2.4.0
> **定位**:深圳自然博物馆统一三维高精地图导航服务平台 H5 SDK
欢迎接入 SGS Map SDK本 SDK 将复杂的 WebGL 三维渲染、GLB 模型管线与 NavMesh 物理寻路引擎封装在服务端基座中业务端H5 / 大屏 / 小程序)只需通过几行代码即可极速唤起 3D 地图。
@@ -16,7 +16,13 @@ sgs-map-sdk-release/
│ ├── index.mjs.map # ESM source map
│ ├── index.d.ts # TypeScript 类型声明文件
│ └── index.d.mts # TypeScript 模块声明文件
├── demo/ # SDK 能力展示 Demo可直接运行
│ ├── index.html # Demo 入口页面
│ ├── app.js # Demo 应用逻辑
│ └── styles.css # Demo 样式表
├── package.json # NPM 元信息
├── CHANGELOG.md # 版本变更日志
├── RELEASE_NOTES_2.4.0.md # V2.4.0 正式发布说明
├── sdk-quickstart.md # 【必读】10 分钟快速接入指南(含完整示例代码)
├── sdk-api-reference.md # 【必读】第三方开发 API 参考手册
├── sdk-protocol.md # 【选读】底层通信协议与安全沙箱规范说明
@@ -52,6 +58,14 @@ sgs-map-sdk-release/
## 📚 如何开始?
- **快速体验 Demo**
发布包内附带 `demo/` 目录,是一个完整的 SDK 能力展示页面支持空间查询、POI 查询、讲解点、导航规划、诊断等全部功能。运行方式:
```bash
npx -y serve . -l 5555
# 浏览器打开(?server= 指向运行中的地图服务)
# http://localhost:5555/demo/?server=http://localhost:3001
```
- **如果您是普通前端业务开发 / 微信小程序开发**
直接打开 `sdk-quickstart.md`复制里面的示例代码10分钟即可完成接入。
*(注:微信小程序团队完全无需引入 `dist` 下的代码,请仔细阅读文档中小程序 `<web-view>` 的原生接入方式)*
@@ -59,12 +73,28 @@ sgs-map-sdk-release/
- **如果您需要逐个查询 API 参数、返回值和错误码**
打开 `sdk-api-reference.md`,按方法名查阅第三方调用说明。
- **如果您需要了解 V2.4.0 新版本变化与发布边界**
打开 `RELEASE_NOTES_2.4.0.md`,查看业务 POI、精品路线、动线展示、兼容性和已验证范围。
- **如果您是对架构感兴趣的高级开发**
可以阅读 `sdk-protocol.md`,了解我们如何通过双域确权协议解决 `postMessage` 多实例安全问题,以及 WebGL 的安全释放机制。
---
## 🆕 V2.3.0 当前能力概览
## 🆕 V2.4.0 当前能力概览
### V2.4.0 新增方法
| 方法 | 签名 | 说明 |
|------|------|------|
| `getBusinessPois` | `getBusinessPois(floorId: string \| number, options?: any)` | 获取指定楼层的业务 POI例如商铺、餐饮等与基础设施 POI 分离 |
| `queryPois` | `queryPois(params: SgsPoiQueryParams)` | 灵活的多字段 POI 检索接口,支持按地图、楼层、分组、类型、关键词组合查询 |
| `getFeaturedRoutes` | `getFeaturedRoutes(mapId: string \| number, options?: any)` | 获取当前地图关联的特色精选路线列表摘要 |
| `getFeaturedRoute` | `getFeaturedRoute(routeId: string \| number)` | 根据路线 ID 获取特色精选路线的详细路点序列与说明 |
| `showFlowline` | `showFlowline(routeId: string \| number)` | 在地图上高亮渲染展示一条特色动线(自动切换至全局俯瞰视角) |
| `clearFlowline` | `clearFlowline()` | 清除当前展示的特色动线并恢复默认视图 |
## V2.3.0 基础数据能力
### V2.3.0 新增方法

View File

@@ -240,6 +240,16 @@ interface SgsLayerTreeNode {
/** 包含的 3D 节点数量 */
nodeCount?: number;
}
type SgsPoiGroup = "SERVICE" | "BUSINESS" | "OTHER";
type SgsBusinessPoiType = "shop" | "restaurant" | "cafe" | "vending" | "bookstore" | "cultural_shop" | "photo_spot";
interface SgsPoiQueryParams {
mapId?: string | number;
floorId?: string | number;
group?: SgsPoiGroup;
types?: string[];
keyword?: string;
limit?: number;
}
/** SDK 消费的 POI — 对应 GET /app-api/gis/sdk/floors/{floorId}/pois */
interface SgsPoi {
/** POI ID雪花 ID字符串或数字均可 */
@@ -266,6 +276,15 @@ interface SgsPoi {
description?: string;
/** 图标 URL */
iconUrl?: string;
floorId?: string | number;
poiGroup?: SgsPoiGroup;
businessType?: SgsBusinessPoiType | string;
coverImageUrl?: string | null;
phone?: string | null;
businessHours?: string | null;
sortOrder?: number | null;
spatialAreaId?: string | number | null;
spatialAreaName?: string | null;
}
/** 路径规划结果 — 对应 POST /app-api/gis/sdk/routes/plan */
interface SgsRoutePlanV2 {
@@ -369,6 +388,10 @@ interface SgsSdkManifest {
highlight?: boolean;
/** 是否支持诊断接口 */
diagnostics?: boolean;
businessPoi?: boolean;
poiSearch?: boolean;
featuredRoutes?: boolean;
flowlinePreview?: boolean;
};
}
/** v2.2.0 楼层摘要manifest 内嵌) */
@@ -651,6 +674,80 @@ interface SgsFloorDiagnostics {
/** 警告信息列表 */
warnings: string[];
}
interface SgsFeaturedRouteSummary {
id: string | number;
mapId: string | number;
routeName: string;
routeNameEn?: string | null;
description?: string | null;
descriptionEn?: string | null;
coverImageUrl?: string | null;
routeColor?: string | null;
estimatedMinutes?: number | null;
displayOrder?: number | null;
waypointCount: number;
startEntranceId?: string | number | null;
endEntranceId?: string | number | null;
updatedAt?: string | null;
}
interface SgsRouteEndpoint {
id: string | number;
name: string;
floorId: string | number;
floorCode?: string | null;
position?: {
x: number;
y: number;
z: number;
} | null;
}
interface SgsFeaturedRouteWaypoint {
order: number;
spaceId: string | number;
spaceName?: string | null;
floorId?: string | number | null;
floorCode?: string | null;
position: {
x: number;
y: number;
z: number;
};
title?: string | null;
subtitle?: string | null;
description?: string | null;
imageUrl?: string | null;
audioUrl?: string | null;
staySeconds?: number | null;
tags?: string[];
highlight?: boolean;
}
interface SgsFeaturedRouteDetail extends SgsFeaturedRouteSummary {
startEntrance?: SgsRouteEndpoint | null;
endEntrance?: SgsRouteEndpoint | null;
waypoints: SgsFeaturedRouteWaypoint[];
}
interface SgsFlowlineOptions {
autoFit?: boolean;
showLabels?: boolean;
animate?: boolean;
}
/**
* 过滤 EXTERIOR 楼层,检查楼层是否是室内可导航楼层
*/
declare function isIndoorNavigableFloor(floorCode: string): boolean;
/**
* 物理楼层顺序由低到高排序
*/
declare function sortIndoorFloors<T extends {
floorCode: string;
}>(floors: T[]): T[];
declare const utils_isIndoorNavigableFloor: typeof isIndoorNavigableFloor;
declare const utils_sortIndoorFloors: typeof sortIndoorFloors;
declare namespace utils {
export { utils_isIndoorNavigableFloor as isIndoorNavigableFloor, utils_sortIndoorFloors as sortIndoorFloors };
}
/**
* SGS 3D 地图 H5 SDK 入口类
@@ -659,9 +756,10 @@ interface SgsFloorDiagnostics {
* <p>所有需要后端数据的操作均通过 postMessage 指令由底座代理,
* Demo / 宿主不允许直连后端 API。
*
* @version 2.3.0
* @version 2.4.0
*/
declare class SGSMapSDK extends EventEmitter<SGSMapEvents> {
static utils: typeof utils;
private container;
private sdkUrl;
private floorId;
@@ -685,6 +783,8 @@ declare class SGSMapSDK extends EventEmitter<SGSMapEvents> {
private _messageListener;
/** ready 超时计时器 */
private _readyTimer;
/** 🔧 P0: HELLO 重试定时器 — 解决 iframe onload 与 React useEffect 时序竞争 */
private _helloInterval;
/** ENGINE_READY 返回的 payload供外部读取 */
readyPayload: any;
/** ready Promise 的 resolve/reject保证 whenReady() 返回同一个 Promise */
@@ -769,7 +869,7 @@ declare class SGSMapSDK extends EventEmitter<SGSMapEvents> {
/** 清空所有业务图钉 */
clearMarkers(): void;
/** 高亮多边形轮廓 */
highlightPolygon(nodeName: string): void;
highlightPolygon(payload: string | object): void;
/** 寻路规划v1 兼容) */
planRoute(from: SgsMapNode | string, to: SgsMapNode | string, options?: any, timeout?: number): Promise<RouteResult>;
/** 寻路规划 V2结构化参数 */
@@ -779,9 +879,14 @@ declare class SGSMapSDK extends EventEmitter<SGSMapEvents> {
/** 获取引擎状态 */
getState(): Promise<any>;
/** 重置视角 */
resetView(): void;
resetView(timeout?: number): Promise<{
success: boolean;
}>;
/** 设置 2D/3D 视图模式 */
setViewMode(mode: '2d' | '3d'): void;
setViewMode(mode: '2d' | '3d', timeout?: number): Promise<{
success: boolean;
mode: string;
}>;
/** 切换图层可见性 */
toggleLayer(types: string[], visible: boolean, timeout?: number): Promise<{
success: boolean;
@@ -792,6 +897,11 @@ declare class SGSMapSDK extends EventEmitter<SGSMapEvents> {
success: boolean;
visibleTypes: string[];
}>;
/** 设置路线展示视图是否活跃 */
setRouteViewActive(active: boolean, timeout?: number): Promise<{
success: boolean;
active: boolean;
}>;
/** 获取地图 Manifest 清单 */
getManifest(timeout?: number): Promise<SgsSdkManifest>;
/** 加载指定楼层的完整 Bundle 数据(含 POI/空间面/讲解点/路网) */
@@ -820,6 +930,27 @@ declare class SGSMapSDK extends EventEmitter<SGSMapEvents> {
getFloorDiagnostics(floorId: string | number, timeout?: number): Promise<SgsFloorDiagnostics>;
/** 清空客户端模型缓存 */
clearModelCache(): void;
/** 获取指定楼层的业务 POI 列表 */
getBusinessPois(floorId: string | number, options?: {
types?: string[];
keyword?: string;
limit?: number;
}, timeout?: number): Promise<SgsPoi[]>;
/** 查询 POI 列表 */
queryPois(params: SgsPoiQueryParams, timeout?: number): Promise<SgsPoi[]>;
/** 获取特色路线列表 */
getFeaturedRoutes(mapId?: string | number, options?: {
limit?: number;
}, timeout?: number): Promise<SgsFeaturedRouteSummary[]>;
/** 获取特色路线详情 */
getFeaturedRoute(routeId: string | number, timeout?: number): Promise<SgsFeaturedRouteDetail>;
/** 显示动线 */
showFlowline(routeId: string | number, options?: SgsFlowlineOptions, timeout?: number): Promise<{
success: boolean;
routeId: string | number;
}>;
/** 清除动线 */
clearFlowline(): void;
/** @deprecated 使用 getManifest() 替代 */
getReleaseManifest(timeout?: number): Promise<any>;
/** @deprecated 已废弃,无替代方法 */
@@ -837,4 +968,4 @@ declare class SGSMapSDK extends EventEmitter<SGSMapEvents> {
destroy(): void;
}
export { type MarkerConfig, type RouteOptions, type RouteResult, type SGSErrorCode, type SGSMapEvents, type SGSMapSDKOptions, type SgsFloorBundle, type SgsFloorDiagnostics, type SgsFloorDiagnosticsSummary, type SgsFloorResource, type SgsGuideStop, type SgsLayerTreeNode, type SgsMapDiagnostics, type SgsMapManifest, type SgsMapNode, type SgsModelResource, type SgsNavigablePlace, type SgsPerformanceStats, type SgsPoi, type SgsRoutePlanV2, type SgsRouteSegment, type SgsRouteSegmentType, type SgsRouteTransferType, type SgsSdkError, type SgsSdkFloorSummary, type SgsSdkManifest, type SgsSpaceArea, type SnapDetail, SGSMapSDK as default };
export { type MarkerConfig, type RouteOptions, type RouteResult, type SGSErrorCode, type SGSMapEvents, type SGSMapSDKOptions, type SgsBusinessPoiType, type SgsFeaturedRouteDetail, type SgsFeaturedRouteSummary, type SgsFeaturedRouteWaypoint, type SgsFloorBundle, type SgsFloorDiagnostics, type SgsFloorDiagnosticsSummary, type SgsFloorResource, type SgsFlowlineOptions, type SgsGuideStop, type SgsLayerTreeNode, type SgsMapDiagnostics, type SgsMapManifest, type SgsMapNode, type SgsModelResource, type SgsNavigablePlace, type SgsPerformanceStats, type SgsPoi, type SgsPoiGroup, type SgsPoiQueryParams, type SgsRouteEndpoint, type SgsRoutePlanV2, type SgsRouteSegment, type SgsRouteSegmentType, type SgsRouteTransferType, type SgsSdkError, type SgsSdkFloorSummary, type SgsSdkManifest, type SgsSpaceArea, type SnapDetail, SGSMapSDK as default };

View File

@@ -1,7 +1,7 @@
{
"name": "@sgs/map-sdk",
"version": "2.3.0",
"description": "SGS Map SDK",
"version": "2.4.0",
"description": "SGS Map SDK API contract copy for frontend-miniapp; iframe runtime is not enabled by this project",
"main": "./index.global.js",
"types": "./index.d.ts",
"exports": {
@@ -10,12 +10,13 @@
"default": "./index.global.js"
}
},
"sideEffects": true,
"sideEffects": false,
"files": [
"index.global.js",
"index.global.js.map",
"index.d.ts",
"README.md",
"CHANGELOG.md",
"sdk-quickstart.md",
"sdk-api-reference.md",
"sdk-protocol.md"

View File

@@ -62,7 +62,7 @@ interface SGSMapSDKOptions {
| `floorId` | 否 | `1` | 初始楼层 ID支持字符串或数字 |
| `mapId` | 否 | `1` | 地图 ID当前由 SDK 实例保存 |
| `timeout` | 否 | `5000` | 普通命令超时时间,单位 ms |
| `readyTimeout` | 否 | `15000` | 等待 `ENGINE_READY` 的最大时间,单位 ms |
| `readyTimeout` | 否 | `30000` | 等待 `ENGINE_READY` 的最大时间,单位 ms |
生产环境必须保证 `sdkUrl` 可访问,并且 `targetOrigin``sdkUrl` 的 Origin 一致。