feat: wire guide data source and POI focus UI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lyf
2026-06-12 09:25:22 +08:00
parent a6bfda30e1
commit 2055b13b90
58 changed files with 5768 additions and 1898 deletions

View File

@@ -206,15 +206,11 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import type {
ExplainExhibitViewModel
} from '@/view-models/explainViewModels'
export interface Exhibit {
id: string
name: string
artist?: string
hall?: string
floor?: string
image?: string
hasAudio?: boolean
export interface Exhibit extends ExplainExhibitViewModel {
audioUrl?: string
}
@@ -225,6 +221,16 @@ interface SearchResult {
type: 'exhibit' | 'hall' | 'facility'
}
const props = withDefaults(defineProps<{
exhibits?: Exhibit[]
hotSearches?: string[]
initialSearchHistory?: string[]
}>(), {
exhibits: () => [],
hotSearches: () => ['恐龙化石', '银杏', '海洋生物', '霸王龙', '古植物', '矿石标本', '恐龙', '化石'],
initialSearchHistory: () => ['霸王龙', '化石', '恐龙']
})
const emit = defineEmits<{
exhibitClick: [exhibit: Exhibit]
audioClick: [exhibit: Exhibit]
@@ -259,46 +265,31 @@ let touchStartY = 0
let touchCurrentY = 0
// 热门搜索
const hotSearches = ['恐龙化石', '银杏', '海洋生物', '霸王龙', '古植物', '矿石标本', '恐龙', '化石']
const hotSearches = computed(() => props.hotSearches)
// 搜索历史
const searchHistory = ref<string[]>(['霸王龙', '化石', '恐龙'])
const searchHistory = ref<string[]>([...props.initialSearchHistory])
// 搜索数据
const allData: SearchResult[] = [
{ id: '1', name: '霸王龙化石骨架', desc: '恐龙厅 · 2F', type: 'exhibit' },
{ id: '2', name: '银杏化石', desc: '演化厅 · B1', type: 'exhibit' },
{ id: '3', name: '三叶虫化石', desc: '演化厅 · B1', type: 'exhibit' },
{ id: '4', name: '腕龙骨骼', desc: '恐龙厅 · 2F', type: 'exhibit' },
{ id: '5', name: '猛犸象象牙', desc: '人类厅 · 3F', type: 'exhibit' },
{ id: 'h1', name: '恐龙厅', desc: '2F · 25件展品', type: 'hall' },
{ id: 'h2', name: '演化厅', desc: 'B1 · 30件展品', type: 'hall' },
{ id: 'h3', name: '生物厅', desc: '3F · 18件展品', type: 'hall' },
{ id: 'f1', name: '洗手间', desc: '1F', type: 'facility' },
{ id: 'f2', name: '咖啡厅', desc: '1F', type: 'facility' },
]
const allData = computed<SearchResult[]>(() => props.exhibits.map((exhibit) => ({
id: exhibit.id,
name: exhibit.name,
desc: exhibit.hall || exhibit.floor || '展项内容',
type: 'exhibit'
})))
// 搜索结果计算
const searchResults = computed(() => {
if (!searchInputValue.value.trim()) return []
const kw = searchInputValue.value.toLowerCase()
return allData.filter(item =>
item.name.toLowerCase().includes(kw) ||
return allData.value.filter(item =>
item.name.toLowerCase().includes(kw) ||
item.desc.toLowerCase().includes(kw)
)
})
// 展品列表
const exhibits = ref<Exhibit[]>([
{ id: '1', name: '银杏化石', hall: '演化厅 B1', hasAudio: true, audioUrl: '' },
{ id: '2', name: '三叶虫化石', hall: '演化厅 B1', hasAudio: true, audioUrl: '' },
{ id: '3', name: '腕龙骨骼', hall: '恐龙厅 2F', hasAudio: true, audioUrl: '' },
{ id: '4', name: '始祖鸟标本', hall: '演化厅 B1', hasAudio: false, audioUrl: '' },
{ id: '5', name: '猛犸象象牙', hall: '人类厅 3F', hasAudio: true, audioUrl: '' },
{ id: '6', name: '珊瑚礁标本', hall: '生物厅 3F', hasAudio: false, audioUrl: '' },
{ id: '7', name: '剑龙化石', hall: '恐龙厅 2F', hasAudio: true, audioUrl: '' },
{ id: '8', name: '鹦鹉螺化石', hall: '演化厅 B1', hasAudio: true, audioUrl: '' }
])
const exhibits = computed(() => props.exhibits)
// 过滤展品
const filteredExhibits = computed(() => {
@@ -306,7 +297,7 @@ const filteredExhibits = computed(() => {
return exhibits.value
}
const keyword = searchKeyword.value.toLowerCase()
return exhibits.value.filter(exhibit =>
return exhibits.value.filter(exhibit =>
exhibit.name.toLowerCase().includes(keyword) ||
(exhibit.hall && exhibit.hall.toLowerCase().includes(keyword))
)

View File

@@ -30,16 +30,9 @@ const emit = defineEmits<{
change: [floorId: string]
}>()
const currentFloor = ref(props.current || '1F')
const currentFloor = ref(props.current || props.floors?.[0]?.id || '')
const defaultFloors: Floor[] = [
{ id: '3F', label: '3F' },
{ id: '2F', label: '2F' },
{ id: '1F', label: '1F' },
{ id: 'B1', label: 'B1' }
]
const floors = props.floors || defaultFloors
const floors = props.floors || []
const handleFloorClick = (floor: Floor) => {
if (floor.disabled) return

View File

@@ -58,7 +58,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { computed, ref, onMounted } from 'vue'
const isH5 = typeof window !== 'undefined'
@@ -94,6 +94,19 @@ interface Polygon {
zIndex: number
}
interface MapFloor {
id: string
label: string
}
const props = withDefaults(defineProps<{
activeFloor?: string
floors?: MapFloor[]
}>(), {
activeFloor: '1F',
floors: () => [] as MapFloor[]
})
const emit = defineEmits<{
markerClick: [markerId: number]
floorChange: [floor: string]
@@ -111,15 +124,10 @@ const mapCenter = ref<MapCenter>({
const mapScale = ref(17)
// 当前楼层
const currentFloor = ref('1F')
const currentFloor = ref(props.activeFloor)
// 楼层列表
const floors = [
{ id: '3F', label: '3F' },
{ id: '2F', label: '2F' },
{ id: '1F', label: '1F' },
{ id: 'B1', label: 'B1' }
]
const floors = computed(() => props.floors)
// 博物馆建筑围栏(多边形)
// 深圳自然博物馆实际建筑轮廓坐标

View File

@@ -50,56 +50,23 @@ import * as THREE from 'three'
import { GLTFLoader, type GLTF } from 'three/addons/loaders/GLTFLoader.js'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import FloorSwitcher from './FloorSwitcher.vue'
import { NAV_ASSET_BASE_URL } from '@/services/navAssets'
import type {
GuideModelFloorAsset,
GuideModelRenderPackage,
GuideModelSource,
GuideRenderPoi
} from '@/domain/guideModel'
type ViewMode = 'overview' | 'floor'
interface ManifestFloorModel {
floorId: string
order: number
asset: string
}
interface CleanNavManifest {
status: string
assets: {
overviewModel: {
asset: string
}
floorModels: ManifestFloorModel[]
}
data: {
floorIndex: string
}
}
interface FloorIndexItem {
floorId: string
order: number
modelAsset: string
poiDataAsset: string
poiCount: number
}
interface FloorIndexData {
status: string
floors: FloorIndexItem[]
}
type FloorIndexItem = GuideModelFloorAsset
interface FloorOption {
id: string
label: string
}
interface CleanPoi {
id: string
name: string
floorId: string
primaryCategory: string
primaryCategoryZh: string
iconType: string
positionGltf?: [number, number, number]
}
type RenderPoi = GuideRenderPoi
interface TargetPoiFocusRequest {
requestId: number | string
@@ -119,23 +86,18 @@ interface TargetPoiFocusResult {
message?: string
}
interface FloorPoiData {
status: string
floorId: string
pois: CleanPoi[]
}
type ContainerRef = HTMLElement | { $el?: HTMLElement } | null
const props = withDefaults(defineProps<{
assetBaseUrl?: string
modelSource: GuideModelSource
initialFloorId?: string
initialView?: ViewMode
showControls?: boolean
showPoi?: boolean
targetFocus?: TargetPoiFocusRequest | null
}>(), {
assetBaseUrl: NAV_ASSET_BASE_URL,
assetBaseUrl: '',
initialFloorId: 'L1',
initialView: 'overview',
showControls: true,
@@ -145,7 +107,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
floorChange: [floorId: string]
poiClick: [poi: CleanPoi]
poiClick: [poi: RenderPoi]
targetFocus: [result: TargetPoiFocusResult]
}>()
@@ -156,9 +118,10 @@ const loadingProgress = ref(0)
const loadingMessage = ref('正在读取室内导览资源...')
const activeView = ref<ViewMode>(props.initialView)
const currentFloor = ref(props.initialFloorId)
const selectedPOI = ref<CleanPoi | null>(null)
const selectedPOI = ref<RenderPoi | null>(null)
const activeFocusPoiId = ref('')
const floorIndex = ref<FloorIndexItem[]>([])
const renderPackage = ref<GuideModelRenderPackage | null>(null)
const floors = computed<FloorOption[]>(() => (
[...floorIndex.value]
@@ -170,7 +133,6 @@ const floors = computed<FloorOption[]>(() => (
))
const shouldRenderPoiMarkers = computed(() => props.showPoi || props.showControls || Boolean(props.targetFocus))
let manifest: CleanNavManifest | null = null
let scene: THREE.Scene | null = null
let camera: THREE.PerspectiveCamera | null = null
let renderer: THREE.WebGLRenderer | null = null
@@ -178,16 +140,13 @@ let controls: OrbitControls | null = null
let loader: GLTFLoader | null = null
let activeModel: THREE.Object3D | null = null
let poiGroup: THREE.Group | null = null
let activeFocusLabelSprite: THREE.Sprite | null = null
let animationId = 0
let resizeObserver: ResizeObserver | null = null
let isDisposed = false
let pendingTargetFocus: TargetPoiFocusRequest | null = null
let targetFocusQueue: Promise<unknown> = Promise.resolve()
const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '')
const assetUrl = (relativePath: string) => `${normalizeBaseUrl(props.assetBaseUrl)}/${relativePath.replace(/^\/+/, '')}`
const getContainerElement = () => {
const container = containerRef.value
if (!container) return null
@@ -202,27 +161,15 @@ const getContainerElement = () => {
return element instanceof HTMLElement ? element : null
}
const formatFloorLabel = (floorId: string) => {
if (floorId === 'L-2') return 'B2'
if (floorId === 'L-1') return 'B1'
if (floorId === 'L1.5') return '1.5F'
if (floorId.startsWith('L')) return `${floorId.slice(1)}F`
return floorId
}
const formatFloorLabel = (floorId: string) => (
floorIndex.value.find((floor) => floor.floorId === floorId)?.label || floorId
)
const setProgress = (progress: number, message: string) => {
loadingProgress.value = Math.min(100, Math.max(0, progress))
loadingMessage.value = message
}
const fetchJson = async <T,>(url: string): Promise<T> => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`资源读取失败: ${response.status} ${url}`)
}
return response.json() as Promise<T>
}
const waitForContainer = async () => {
await nextTick()
@@ -274,7 +221,7 @@ const initThree = async () => {
loader = new GLTFLoader()
poiGroup = new THREE.Group()
poiGroup.name = 'CleanPackagePOI'
poiGroup.name = 'GuideModelPOI'
scene.add(poiGroup)
const ambientLight = new THREE.AmbientLight(0xffffff, 1.6)
@@ -340,6 +287,7 @@ const clearSceneData = () => {
}
activeModel = null
disposeFocusLabel()
selectedPOI.value = null
if (poiGroup) {
@@ -405,15 +353,16 @@ const fitCameraToObject = (object: THREE.Object3D) => {
}
const loadOverview = async () => {
if (!manifest || !scene) return
const packageData = renderPackage.value
if (!packageData || !scene) return
activeView.value = 'overview'
clearSceneData()
setProgress(18, '正在加载全馆三维模型...')
const gltf = await loadModel(assetUrl(manifest.assets.overviewModel.asset), '正在加载全馆三维模型')
const gltf = await loadModel(packageData.overviewModelUrl, '正在加载全馆三维模型')
activeModel = gltf.scene
activeModel.name = 'CleanOverviewModel'
activeModel.name = 'GuideOverviewModel'
prepareModel(activeModel)
scene.add(activeModel)
fitCameraToObject(activeModel)
@@ -428,9 +377,9 @@ const loadFloor = async (floorId: string) => {
clearSceneData()
setProgress(18, `正在加载 ${formatFloorLabel(floor.floorId)} 模型...`)
const gltf = await loadModel(assetUrl(floor.modelAsset), `正在加载 ${formatFloorLabel(floor.floorId)} 模型`)
const gltf = await loadModel(floor.modelUrl, `正在加载 ${formatFloorLabel(floor.floorId)} 模型`)
activeModel = gltf.scene
activeModel.name = `CleanFloorModel_${floor.floorId}`
activeModel.name = `GuideFloorModel_${floor.floorId}`
prepareModel(activeModel)
scene.add(activeModel)
fitCameraToObject(activeModel)
@@ -440,7 +389,7 @@ const loadFloor = async (floorId: string) => {
}
}
const createPoiMaterial = (poi: CleanPoi) => {
const createPoiMaterial = (poi: RenderPoi) => {
const canvas = document.createElement('canvas')
canvas.width = 96
canvas.height = 96
@@ -476,6 +425,69 @@ const createPoiMaterial = (poi: CleanPoi) => {
})
}
const createPoiLabelSprite = (poi: RenderPoi, markerSize: number) => {
const canvas = document.createElement('canvas')
canvas.width = 512
canvas.height = 144
const context = canvas.getContext('2d')
const label = poi.name.length > 14 ? `${poi.name.slice(0, 14)}` : poi.name
if (context) {
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = 'rgba(0, 0, 0, 0.82)'
context.beginPath()
context.roundRect(36, 18, 440, 82, 30)
context.fill()
context.beginPath()
context.moveTo(238, 100)
context.lineTo(274, 100)
context.lineTo(256, 124)
context.closePath()
context.fill()
context.font = '600 38px sans-serif'
context.textAlign = 'center'
context.textBaseline = 'middle'
context.fillStyle = '#ffffff'
context.fillText(label, 256, 59, 390)
}
const texture = new THREE.CanvasTexture(canvas)
texture.colorSpace = THREE.SRGBColorSpace
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthTest: false,
depthWrite: false
}))
sprite.userData.isPoiLabel = true
sprite.renderOrder = 30
sprite.scale.set(markerSize * 4.8, markerSize * 1.35, 1)
return sprite
}
const disposeFocusLabel = () => {
if (!activeFocusLabelSprite) return
activeFocusLabelSprite.parent?.remove(activeFocusLabelSprite)
disposeObject(activeFocusLabelSprite)
activeFocusLabelSprite = null
}
const showFocusPoiLabel = (poi: RenderPoi) => {
if (!poiGroup || !poi.positionGltf) return
disposeFocusLabel()
const markerSize = getPoiMarkerSize()
const [x, y, z] = poi.positionGltf
activeFocusLabelSprite = createPoiLabelSprite(poi, markerSize)
activeFocusLabelSprite.position.set(x, y + markerSize * 2.35, z)
poiGroup.add(activeFocusLabelSprite)
}
const getPoiColor = (category: string) => {
const colorMap: Record<string, string> = {
touring_poi: '#2f6fed',
@@ -509,8 +521,8 @@ const updatePoiMarkerFocus = () => {
if (!poiGroup) return
poiGroup.children.forEach((child) => {
if (child instanceof THREE.Sprite) {
const poi = child.userData.poi as CleanPoi | undefined
if (child instanceof THREE.Sprite && !child.userData.isPoiLabel) {
const poi = child.userData.poi as RenderPoi | undefined
setPoiSpriteFocusStyle(child, Boolean(poi && poi.id === activeFocusPoiId.value))
}
})
@@ -518,15 +530,15 @@ const updatePoiMarkerFocus = () => {
const findPoiSprite = (poiId: string) => (
poiGroup?.children.find((child): child is THREE.Sprite => (
child instanceof THREE.Sprite && (child.userData.poi as CleanPoi | undefined)?.id === poiId
child instanceof THREE.Sprite && (child.userData.poi as RenderPoi | undefined)?.id === poiId
))
)
const findLoadedPoi = (poiId: string) => (
findPoiSprite(poiId)?.userData.poi as CleanPoi | undefined
findPoiSprite(poiId)?.userData.poi as RenderPoi | undefined
)
const createPoiFromFocusRequest = (request: TargetPoiFocusRequest): CleanPoi | null => {
const createPoiFromFocusRequest = (request: TargetPoiFocusRequest): RenderPoi | null => {
if (!request.positionGltf) return null
return {
@@ -561,8 +573,7 @@ const addFocusMarkerFromRequest = (request: TargetPoiFocusRequest) => {
const loadFloorPOIs = async (floor: FloorIndexItem) => {
if (!poiGroup) return
const data = await fetchJson<FloorPoiData>(assetUrl(floor.poiDataAsset))
const validPois = data.pois.filter((poi) => Array.isArray(poi.positionGltf) && poi.positionGltf.length === 3)
const validPois = await props.modelSource.loadFloorPois(floor.floorId)
const markerSize = getPoiMarkerSize()
validPois.forEach((poi) => {
@@ -588,8 +599,10 @@ const handlePointerDown = (event: PointerEvent) => {
raycaster.setFromCamera(pointer, camera)
const hits = raycaster.intersectObjects(poiGroup.children, false)
if (hits[0]?.object.userData.poi) {
selectedPOI.value = hits[0].object.userData.poi as CleanPoi
const hit = hits.find((item) => !item.object.userData.isPoiLabel && item.object.userData.poi)
if (hit?.object.userData.poi) {
selectedPOI.value = hit.object.userData.poi as RenderPoi
activeFocusPoiId.value = selectedPOI.value.id
updatePoiMarkerFocus()
emit('poiClick', selectedPOI.value)
@@ -614,6 +627,7 @@ const clearTargetFocus = () => {
pendingTargetFocus = null
activeFocusPoiId.value = ''
selectedPOI.value = null
disposeFocusLabel()
updatePoiMarkerFocus()
}
@@ -623,7 +637,7 @@ const isSceneReadyForTargetFocus = () => (
&& !loadError.value
)
const focusCameraOnPoi = (poi: CleanPoi) => {
const focusCameraOnPoi = (poi: RenderPoi) => {
if (!camera || !controls || !poi.positionGltf) return
const [x, y, z] = poi.positionGltf
@@ -675,6 +689,7 @@ const focusTargetPoi = async (request: TargetPoiFocusRequest) => {
if (!poi?.positionGltf) {
selectedPOI.value = null
disposeFocusLabel()
updatePoiMarkerFocus()
emitTargetFocus(request, 'missing', '目标暂无三维坐标')
return false
@@ -682,6 +697,7 @@ const focusTargetPoi = async (request: TargetPoiFocusRequest) => {
selectedPOI.value = poi
updatePoiMarkerFocus()
showFocusPoiLabel(poi)
focusCameraOnPoi(poi)
emitTargetFocus(request, 'focused')
return true
@@ -718,17 +734,13 @@ const queueTargetFocus = (request: TargetPoiFocusRequest | null) => {
})
}
const loadCleanPackage = async () => {
const loadModelPackage = async () => {
setProgress(8, '正在读取室内导览资源...')
manifest = await fetchJson<CleanNavManifest>(assetUrl('app_nav_manifest.json'))
if (manifest.status !== 'pass') {
throw new Error('室内导览资源包状态不是 pass')
}
const packageData = await props.modelSource.loadPackage()
renderPackage.value = packageData
setProgress(14, '正在读取楼层索引...')
const floorData = await fetchJson<FloorIndexData>(assetUrl(manifest.data.floorIndex))
floorIndex.value = [...floorData.floors].sort((a, b) => a.order - b.order)
floorIndex.value = [...packageData.floors].sort((a, b) => a.order - b.order)
if (!floorIndex.value.some((floor) => floor.floorId === currentFloor.value)) {
currentFloor.value = floorIndex.value.find((floor) => floor.floorId === 'L1')?.floorId || floorIndex.value[0]?.floorId || 'L1'
@@ -752,13 +764,13 @@ const init3DScene = async () => {
setProgress(0, '正在初始化三维场景...')
await initThree()
await loadCleanPackage()
await loadModelPackage()
setProgress(100, '室内三维模型加载完成')
isLoading.value = false
queueTargetFocus(pendingTargetFocus || props.targetFocus || null)
} catch (error) {
console.error('室内 3D clean package 加载失败:', error)
console.error('室内 3D 模型资源加载失败:', error)
loadError.value = true
isLoading.value = false
setProgress(0, error instanceof Error ? error.message : '请检查模型资源或网络状态后重试')
@@ -840,11 +852,12 @@ defineExpose({
clearNavigation: () => {
activeFocusPoiId.value = ''
selectedPOI.value = null
disposeFocusLabel()
updatePoiMarkerFocus()
}
})
watch(() => props.assetBaseUrl, () => {
watch(() => props.modelSource, () => {
init3DScene()
})

View File

@@ -4,9 +4,11 @@
<template v-if="mapType === 'indoor'">
<!-- #ifdef H5 -->
<ThreeMap
v-if="indoorModelSource"
ref="threeMapRef"
class="indoor-three-map"
:asset-base-url="indoorAssetBaseUrl"
:model-source="indoorModelSource"
:initial-floor-id="activeFloorId"
:initial-view="indoorInitialView"
:show-controls="false"
@@ -23,7 +25,11 @@
<!-- #endif -->
</template>
<view v-else class="outdoor-map-bg">
<TencentMap class="outdoor-map" />
<TencentMap
class="outdoor-map"
:active-floor="activeFloor"
:floors="props.floors"
/>
</view>
<view v-if="dimmed" class="dim-layer"></view>
</view>
@@ -105,7 +111,7 @@
<view v-if="showFloor" class="floor-switcher" :style="floorSwitcherStyle">
<view
v-for="floor in floors"
v-for="floor in floorLabels"
:key="floor"
class="floor-item"
:class="{ active: activeFloor === floor }"
@@ -136,11 +142,14 @@ import TencentMap from '@/components/map/TencentMap.vue'
// #ifdef H5
import ThreeMap from '@/components/map/ThreeMap.vue'
// #endif
import {
NAV_ASSET_BASE_URL,
NAV_FLOOR_OPTIONS,
navFloorIdFromLabel
} from '@/services/navAssets'
import type {
GuideModelSource
} from '@/domain/guideModel'
interface GuideFloorOption {
id: string
label: string
}
interface TargetPoiFocusRequest {
requestId: number | string
@@ -183,6 +192,9 @@ const props = withDefaults(defineProps<{
mapType?: 'indoor' | 'outdoor'
outdoorVariant?: 'home' | 'entrance'
indoorAssetBaseUrl?: string
indoorModelSource?: GuideModelSource
floors?: GuideFloorOption[]
normalizeFloorId?: (labelOrId: string) => string
dimmed?: boolean
targetFocusRequest?: TargetPoiFocusRequest | null
}>(), {
@@ -205,7 +217,10 @@ const props = withDefaults(defineProps<{
modeStatusTone: 'solid',
mapType: 'indoor',
outdoorVariant: 'home',
indoorAssetBaseUrl: NAV_ASSET_BASE_URL,
indoorAssetBaseUrl: '',
indoorModelSource: undefined,
floors: () => [] as GuideFloorOption[],
normalizeFloorId: (labelOrId: string) => labelOrId,
dimmed: false,
targetFocusRequest: null
})
@@ -223,9 +238,11 @@ const threeMapRef = ref<{
switchFloor?: (floorId: string) => Promise<void> | void
showOverview?: () => Promise<void> | void
} | null>(null)
const floors = NAV_FLOOR_OPTIONS.map((floor) => floor.label)
const activeFloorId = computed(() => navFloorIdFromLabel(props.activeFloor))
const floorLabels = computed(() => props.floors.map((floor) => floor.label))
const indoorModelSource = computed(() => props.indoorModelSource)
const activeFloorId = computed(() => props.normalizeFloorId(props.activeFloor))
const searchFieldStyle = computed(() => ({
top: props.searchTop
@@ -258,7 +275,7 @@ const handleModeChange = (mode: '2d' | '3d') => {
}
const handleFloorChange = (floor: string) => {
const floorId = navFloorIdFromLabel(floor)
const floorId = props.normalizeFloorId(floor)
void threeMapRef.value?.switchFloor?.(floorId)
emit('indoorViewChange', 'floor')
emit('floorChange', floor)
@@ -507,10 +524,11 @@ const handleTargetFocus = (result: TargetPoiFocusResult) => {
position: absolute;
right: 16px;
width: 46px;
height: 218px;
height: auto;
max-height: calc(100vh - 260px);
padding: 1px;
box-sizing: border-box;
overflow: hidden;
overflow: visible;
background: rgba(255, 255, 255, 0.84);
border: 1px solid #dde5df;
border-radius: 10px;

View File

@@ -25,7 +25,7 @@
<text class="action-text">查看室外地图</text>
</view>
<view class="action-btn primary" @tap="handleShowTargetLocation">
<text class="action-text">查看目标位置</text>
<text class="action-text">重新定位目标</text>
</view>
</view>
</view>

View File

@@ -107,14 +107,9 @@
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, onMounted } from 'vue'
import {
formatNavFloorLabel,
searchCleanNavPois,
type CleanNavPoi
} from '@/services/navAssets'
import { ref, computed, watch, nextTick } from 'vue'
interface SearchResult {
export interface SearchResult {
id: string
name: string
desc: string
@@ -126,9 +121,11 @@ const props = withDefaults(defineProps<{
placeholder?: string
/** 嵌入模式:隐藏背景遮罩和取消按钮,直接嵌入父容器 */
embedded?: boolean
results?: SearchResult[]
}>(), {
placeholder: '搜索展品、展厅、设施...',
embedded: false
embedded: false,
results: () => []
})
const emit = defineEmits<{
@@ -144,14 +141,7 @@ const hotSearches = ref(['卫生间', '电梯', '服务台', '无障碍', '入
const searchHistory = ref<string[]>([])
const allData = ref<SearchResult[]>([])
const toSearchResult = (poi: CleanNavPoi): SearchResult => ({
id: poi.id,
name: poi.name,
desc: `${formatNavFloorLabel(poi.floorId)} · ${poi.primaryCategoryZh}`,
type: poi.primaryCategory === 'touring_poi' ? 'hall' : 'facility'
})
const allData = computed(() => props.results)
// 搜索结果计算
const allResults = computed(() => {
@@ -218,20 +208,6 @@ const getResultIcon = (type: string): string => {
return icons[type] || '📍'
}
const loadCleanResults = async () => {
try {
const pois = await searchCleanNavPois()
allData.value = pois.slice(0, 120).map(toSearchResult)
} catch (error) {
console.error('加载 clean POI 搜索数据失败:', error)
allData.value = []
}
}
onMounted(() => {
loadCleanResults()
})
// 打开时聚焦输入框
watch(() => props.isOpen, async (newVal) => {
if (newVal) {