From b99ae572da003281630d18c74c5f35b0340b1efa Mon Sep 17 00:00:00 2001 From: lyf <2514544224@qq.com> Date: Sun, 5 Jul 2026 23:55:20 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=82=B9=E4=BD=8D=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=88=9D=E5=A7=8B=E7=A9=BA=E9=97=B4=E7=82=B9=E4=BD=8D?= =?UTF-8?q?=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/search/PoiSearchPanel.vue | 47 +++++++- src/data/adapters/sgsSdkGuideAdapter.ts | 142 ++++++++++++++++++++--- src/domain/museum.ts | 2 +- src/repositories/GuideRepository.ts | 44 ++++++- src/usecases/guideUseCase.ts | 4 + 5 files changed, 218 insertions(+), 21 deletions(-) diff --git a/src/components/search/PoiSearchPanel.vue b/src/components/search/PoiSearchPanel.vue index ba16f7d..fab6d56 100644 --- a/src/components/search/PoiSearchPanel.vue +++ b/src/components/search/PoiSearchPanel.vue @@ -117,7 +117,7 @@ {{ emptyTitle }} - 可切换楼层或搜索服务设施 + {{ emptyDesc }} @@ -192,6 +192,7 @@ const floors = ref([]) const activeFloor = ref('1F') const pois = ref([]) const isLoading = ref(false) +const initialSpaceLoadError = ref('') const showSearchContent = computed(() => props.variant === 'page' || homeExpanded.value) const collapseDragThreshold = 48 @@ -206,7 +207,13 @@ const floorResults = computed(() => { }) const emptyTitle = computed(() => ( - isLoading.value ? '正在读取点位' : '暂无匹配点位' + isLoading.value ? '正在读取点位' : initialSpaceLoadError.value || '暂无匹配点位' +)) + +const emptyDesc = computed(() => ( + initialSpaceLoadError.value + ? '请检查 SGS 空间点位数据接口后重试' + : '可切换楼层或搜索服务设施' )) const isVisiblePoi = (poi: MuseumPoi) => poi.primaryCategory.id !== 'touring_poi' @@ -277,6 +284,7 @@ const loadFloors = async () => { const loadPois = async (keyword = '') => { isLoading.value = true + initialSpaceLoadError.value = '' try { const result = await guideUseCase.searchPois(keyword) pois.value = result.filter(isVisiblePoi) @@ -292,6 +300,26 @@ const loadPois = async (keyword = '') => { } } +const loadInitialSpacePoints = async () => { + isLoading.value = true + initialSpaceLoadError.value = '' + + try { + const result = await guideUseCase.getInitialSearchSpacePoints() + pois.value = result.filter(isVisiblePoi) + } catch (error) { + console.error('加载 SDK 空间点位失败:', error) + pois.value = [] + initialSpaceLoadError.value = '空间点位读取失败' + uni.showToast({ + title: '空间点位读取失败', + icon: 'none' + }) + } finally { + isLoading.value = false + } +} + const focusSearchInput = async () => { searchInputFocused.value = false await nextTick() @@ -365,7 +393,7 @@ const handleSearchInput = (event: any) => { const handleSearchClear = async () => { searchDraftKeyword.value = '' searchKeyword.value = '' - await loadPois() + await loadInitialSpacePoints() await focusSearchInput() } @@ -443,10 +471,17 @@ const handleResultTap = (poi: MuseumPoi) => { } const applyInitialKeyword = async (keyword: string) => { - searchKeyword.value = keyword - searchDraftKeyword.value = keyword + const normalizedKeyword = keyword.trim() + searchKeyword.value = normalizedKeyword + searchDraftKeyword.value = normalizedKeyword homeExpanded.value = props.variant === 'page' || Boolean(keyword) - await loadPois(keyword) + + if (normalizedKeyword) { + await loadPois(normalizedKeyword) + return + } + + await loadInitialSpacePoints() } watch(() => props.initialKeyword, (keyword) => { diff --git a/src/data/adapters/sgsSdkGuideAdapter.ts b/src/data/adapters/sgsSdkGuideAdapter.ts index fda5490..64361ca 100644 --- a/src/data/adapters/sgsSdkGuideAdapter.ts +++ b/src/data/adapters/sgsSdkGuideAdapter.ts @@ -48,11 +48,66 @@ const hallCategory: MuseumCategory = { iconType: 'exhibition_hall' } -const hallEntranceCategory: MuseumCategory = { - id: 'exhibition_hall_entrance', - label: '展厅出入口', - iconType: 'hall_entrance' -} +const hallEntranceCategory: MuseumCategory = { + id: 'exhibition_hall_entrance', + label: '展厅出入口', + iconType: 'hall_entrance' +} + +const spaceCategory: MuseumCategory = { + id: 'space_point', + label: '空间点位', + iconType: 'space' +} + +const spaceCategoryBySgsType: Record = { + exhibition_hall: hallCategory, + theater: { + id: 'space_theater', + label: '剧场空间', + iconType: 'theater' + }, + education_activity: { + id: 'space_education_activity', + label: '教育活动空间', + iconType: 'education_activity' + }, + ramp: { + id: 'space_ramp', + label: '空间坡道', + iconType: 'ramp' + }, + public_area: { + id: 'space_public_area', + label: '公共空间', + iconType: 'public_area' + }, + service_space: { + id: 'space_service', + label: '服务空间', + iconType: 'service_space' + }, + commercial: { + id: 'space_commercial', + label: '商业空间', + iconType: 'commercial' + }, + parking: { + id: 'space_parking', + label: '停车空间', + iconType: 'parking' + }, + plaza: { + id: 'space_plaza', + label: '广场空间', + iconType: 'plaza' + }, + room: { + id: 'space_room', + label: '房间空间', + iconType: 'room' + } +} const categoryBySgsType: Record = { poi: { @@ -278,6 +333,11 @@ interface SgsHallPoiBuildOptions { const sgsSpaceCenterConfidence = 'backend-sgs-sdk-space-center' const sgsSpaceBoundaryCenterConfidence = 'backend-sgs-sdk-space-boundary-center' const sgsHallEntranceConfidence = 'backend-sgs-sdk-hall-entrance' + +const normalizeTypeId = (value?: string | null) => normalizedText(value) + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, '_') + .replace(/^_+|_+$/g, '') const hasExhibitionKeyword = (value?: string | null) => ( exhibitionHallKeywords.some((keyword) => normalizedText(value).includes(keyword)) @@ -294,7 +354,7 @@ const searchableSpaceText = (space: SgsSpacePayload) => [ const parseBoundaryWktCenter = ( boundaryWkt?: string | null, - fallbackY?: number + fallbackY = 0 ): [number, number, number] | undefined => { const matches = [...(boundaryWkt || '').matchAll(/(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)/g)] const points = matches @@ -573,19 +633,75 @@ const findMatchedHallSpace = ( return rankedMatches[0]?.space } -const floorLabelForSgs = ( - floorId: string, - floors: SgsSdkFloorSummaryPayload[], - floorCode?: string | null, +const floorLabelForSgs = ( + floorId: string, + floors: SgsSdkFloorSummaryPayload[], + floorCode?: string | null, floorName?: string | null ) => { const matchedFloor = floors.find((floor) => stringifyId(floor.floorId) === floorId || floor.floorCode === floorCode) return formatSgsFloorLabel( matchedFloor?.floorCode || floorCode, matchedFloor?.floorName || floorName - ) -} - + ) +} + +const categoryForSgsSpace = (space: SgsSpacePayload): MuseumCategory => { + const normalizedType = normalizeTypeId(space.type) + + if (normalizedType && spaceCategoryBySgsType[normalizedType]) { + return spaceCategoryBySgsType[normalizedType] + } + + const typeLabel = normalizedText(space.type) + if (typeLabel) { + return { + id: `space_${normalizedType || 'point'}`, + label: typeLabel, + iconType: normalizedType || spaceCategory.iconType + } + } + + return spaceCategory +} + +export const toMuseumSpacePointFromSgs = ( + space: SgsSpacePayload, + floors: SgsSdkFloorSummaryPayload[], + fallbackFloorId: string, + options: SgsHallPoiBuildOptions = {} +): MuseumPoi | null => { + const spaceId = stringifyId(space.id) + const floorId = stringifyId(space.floorId) || fallbackFloorId + const name = normalizedText(space.name) + const spacePosition = normalizeSpacePosition(space, options.fallbackY) + + if (!spaceId || !name || !spacePosition) return null + + const category = categoryForSgsSpace(space) + + return { + id: `space-${spaceId}`, + name, + floorId, + floorLabel: floorLabelForSgs(floorId, floors), + primaryCategory: category, + categories: [ + category + ], + positionGltf: spacePosition.position, + sourceObjectName: space.sourceNodeName || undefined, + sourceConfidence: spacePosition.sourceConfidence, + navigationReadiness: '位置预览', + accessible: false, + kind: 'space', + hallId: category.id === hallCategory.id ? spaceId : undefined, + hallName: category.id === hallCategory.id ? name : undefined, + spaceId, + sourceSpaceId: spaceId + } +} + const createHallEntrance = ( place: SgsNavigablePlacePayload, floors: SgsSdkFloorSummaryPayload[], diff --git a/src/domain/museum.ts b/src/domain/museum.ts index eb17af5..7de59c2 100644 --- a/src/domain/museum.ts +++ b/src/domain/museum.ts @@ -72,7 +72,7 @@ export interface MuseumCategory { iconType?: string } -export type MuseumPoiKind = 'facility' | 'hall' | 'hall_entrance' | 'guide' +export type MuseumPoiKind = 'facility' | 'hall' | 'hall_entrance' | 'guide' | 'space' export interface MuseumPoiEntrance { id: string diff --git a/src/repositories/GuideRepository.ts b/src/repositories/GuideRepository.ts index fcabcf4..201ce14 100644 --- a/src/repositories/GuideRepository.ts +++ b/src/repositories/GuideRepository.ts @@ -38,6 +38,7 @@ import { toMuseumFloorFromSgs, toMuseumHallPoisFromSgs, toMuseumPoiFromSgs, + toMuseumSpacePointFromSgs, toRouteReadinessFromSgsDiagnostics } from '@/data/adapters/sgsSdkGuideAdapter' @@ -130,6 +131,7 @@ export interface GuideRepository { getFloors(): Promise normalizeFloorId(labelOrId: string): string listPois(): Promise + listSpacePoints(): Promise getPoiById(id: string): Promise searchPois(keyword?: string): Promise getLocationPreview(poiId: string): Promise @@ -175,6 +177,10 @@ export class StaticGuideRepository implements GuideRepository { return this.poiCache } + async listSpacePoints() { + return [] + } + async getPoiById(id: string) { const pois = await this.listPois() return pois.find((poi) => poi.id === id) || findPoiByNavIdNameFallback(pois, id) @@ -254,6 +260,7 @@ export class StaticGuideRepository implements GuideRepository { export class SgsSdkGuideRepository implements GuideRepository { private floorsCache: MuseumFloor[] | null = null private poiCache: MuseumPoi[] | null = null + private spacePointCache: MuseumPoi[] | null = null private floorAliases: Map | null = null constructor(private readonly provider: SgsSdkApiProvider = defaultSgsSdkApiProvider) {} @@ -326,10 +333,45 @@ export class SgsSdkGuideRepository implements GuideRepository { return this.poiCache } + async listSpacePoints() { + if (this.spacePointCache) return this.spacePointCache + + const manifest = await this.provider.getManifest() + const indoorFloors = manifest.floors.filter(isIndoorNavigableFloor) + const indoorFloorIds = new Set(indoorFloors.map((floor) => String(floor.floorId))) + this.floorAliases = buildSgsFloorAliases(indoorFloors) + + const floorDataGroups = await Promise.all( + indoorFloors.map(async (floor) => { + const floorId = String(floor.floorId) + const spaces = await this.provider.getFloorSpaces(floorId).catch(() => []) + + return { + floorId, + spaces + } + }) + ) + + const spacePoints = floorDataGroups + .flatMap((group) => group.spaces.map((space) => toMuseumSpacePointFromSgs(space, manifest.floors, group.floorId))) + .filter((poi): poi is MuseumPoi => Boolean(poi)) + .filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi)) + + this.spacePointCache = dedupePoisById(spacePoints) + + return this.spacePointCache + } + async getPoiById(id: string) { const pois = await this.listPois() - return pois.find((poi) => poi.id === id) + const matchedPoi = pois.find((poi) => poi.id === id) || findPoiByNavIdNameFallback(pois, id) + + if (matchedPoi) return matchedPoi + + const spacePoints = await this.listSpacePoints() + return spacePoints.find((poi) => poi.id === id || poi.spaceId === id || poi.sourceSpaceId === id) || null } diff --git a/src/usecases/guideUseCase.ts b/src/usecases/guideUseCase.ts index a51cd04..82a821b 100644 --- a/src/usecases/guideUseCase.ts +++ b/src/usecases/guideUseCase.ts @@ -139,6 +139,10 @@ export class GuideUseCase { return this.guide.searchPois(keyword) } + getInitialSearchSpacePoints() { + return this.guide.listSpacePoints() + } + getPoiById(id: string) { return this.guide.getPoiById(id) }