提交室内导览交互与讲解优化
This commit is contained in:
202
src/data/mock/sgsSceneExplainData.ts
Normal file
202
src/data/mock/sgsSceneExplainData.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
export interface SgsSceneSpaceMock {
|
||||
id: number
|
||||
name: string
|
||||
spaceCategory: 'EXHIBITION' | 'EXPERIENCE' | 'SERVICE' | 'COMMERCIAL' | 'EDUCATION' | 'CIRCULATION' | 'BACK_OFFICE'
|
||||
isPublic: boolean
|
||||
area?: number
|
||||
exhibitCount?: number
|
||||
boundaryStatus?: 'DEFINED' | 'UNDEFINED'
|
||||
floorId: string
|
||||
floorLabel: string
|
||||
poiId?: string
|
||||
}
|
||||
|
||||
export interface SgsSceneExhibitMock {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
category?: string
|
||||
era?: string
|
||||
origin?: string
|
||||
description: string
|
||||
descriptionEn?: string
|
||||
audioUrl?: string
|
||||
videoUrl?: string
|
||||
coverImageUrl?: string
|
||||
modelUrl?: string
|
||||
spaceId: number
|
||||
spaceName: string
|
||||
zoneId?: number
|
||||
zoneName?: string
|
||||
poiId?: string
|
||||
}
|
||||
|
||||
export interface SgsSceneExplainDatasetMock {
|
||||
schemaVersion: string
|
||||
sourceProject: string
|
||||
sourceFiles: string[]
|
||||
floorId: string
|
||||
floorLabel: string
|
||||
sourceFloorId: number
|
||||
spaces: SgsSceneSpaceMock[]
|
||||
exhibits: SgsSceneExhibitMock[]
|
||||
}
|
||||
|
||||
export const SGS_SCENE_EXPLAIN_DATASET: SgsSceneExplainDatasetMock = {
|
||||
schemaVersion: 'sgs-scene-explain-mock/v1',
|
||||
sourceProject: 'smart-navigation-system/sgs-frontend-map',
|
||||
sourceFiles: [
|
||||
'public/mocks/v2/space-by-floor-1.json',
|
||||
'public/mocks/v2/exhibit-list-by-space-1.json',
|
||||
'src/types/map.ts:SpatialNodeVO, ExhibitItemVO, GuideContentVO'
|
||||
],
|
||||
floorId: 'L-2',
|
||||
floorLabel: 'B2',
|
||||
sourceFloorId: 1,
|
||||
spaces: [
|
||||
{
|
||||
id: 101,
|
||||
name: '地球厅',
|
||||
spaceCategory: 'EXHIBITION',
|
||||
isPublic: true,
|
||||
area: 1200,
|
||||
exhibitCount: 156,
|
||||
boundaryStatus: 'DEFINED',
|
||||
floorId: 'L-2',
|
||||
floorLabel: 'B2',
|
||||
poiId: 'poi_navpoi_0010'
|
||||
},
|
||||
{
|
||||
id: 1001,
|
||||
name: '宝石矿物展区',
|
||||
spaceCategory: 'EXHIBITION',
|
||||
isPublic: true,
|
||||
area: 400,
|
||||
exhibitCount: 80,
|
||||
boundaryStatus: 'DEFINED',
|
||||
floorId: 'L-2',
|
||||
floorLabel: 'B2',
|
||||
poiId: 'modelpoi_L-2_e2ec8ce8ba'
|
||||
},
|
||||
{
|
||||
id: 1002,
|
||||
name: '生命演化展区',
|
||||
spaceCategory: 'EXHIBITION',
|
||||
isPublic: true,
|
||||
area: 600,
|
||||
exhibitCount: 76,
|
||||
boundaryStatus: 'DEFINED',
|
||||
floorId: 'L-2',
|
||||
floorLabel: 'B2'
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
name: '4D影院',
|
||||
spaceCategory: 'EXPERIENCE',
|
||||
isPublic: true,
|
||||
area: 220,
|
||||
boundaryStatus: 'DEFINED',
|
||||
floorId: 'L-2',
|
||||
floorLabel: 'B2'
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
name: '中央服务台',
|
||||
spaceCategory: 'SERVICE',
|
||||
isPublic: true,
|
||||
area: 60,
|
||||
boundaryStatus: 'DEFINED',
|
||||
floorId: 'L-2',
|
||||
floorLabel: 'B2'
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
name: '文创商店',
|
||||
spaceCategory: 'COMMERCIAL',
|
||||
isPublic: true,
|
||||
area: 150,
|
||||
boundaryStatus: 'DEFINED',
|
||||
floorId: 'L-2',
|
||||
floorLabel: 'B2'
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
name: '科普工坊',
|
||||
spaceCategory: 'EDUCATION',
|
||||
isPublic: true,
|
||||
area: 180,
|
||||
boundaryStatus: 'DEFINED',
|
||||
floorId: 'L-2',
|
||||
floorLabel: 'B2'
|
||||
},
|
||||
{
|
||||
id: 106,
|
||||
name: '中央大厅',
|
||||
spaceCategory: 'CIRCULATION',
|
||||
isPublic: true,
|
||||
area: 800,
|
||||
boundaryStatus: 'UNDEFINED',
|
||||
floorId: 'L-2',
|
||||
floorLabel: 'B2'
|
||||
}
|
||||
],
|
||||
exhibits: [
|
||||
{
|
||||
id: 3001,
|
||||
name: '橄榄岩',
|
||||
code: 'EX-F1-001',
|
||||
category: '岩石标本',
|
||||
era: '前寒武纪',
|
||||
origin: '中国内蒙古',
|
||||
description: '橄榄岩是一种超基性深成岩,主要由橄榄石和辉石组成。它是地球上地幔的主要岩石类型,也是研究地球深部物质的重要窗口。',
|
||||
descriptionEn: "Peridotite is an ultramafic igneous rock consisting primarily of olivine and pyroxene. It is the dominant rock of the Earth's upper mantle.",
|
||||
audioUrl: '/audio/exhibits/peridotite.mp3',
|
||||
videoUrl: '/video/exhibits/peridotite.mp4',
|
||||
coverImageUrl: '/images/exhibits/peridotite.jpg',
|
||||
modelUrl: '/models/exhibits/peridotite.glb',
|
||||
spaceId: 101,
|
||||
spaceName: '地球厅',
|
||||
zoneId: 1001,
|
||||
zoneName: '宝石矿物单元',
|
||||
poiId: 'modelpoi_L-2_e2ec8ce8ba'
|
||||
},
|
||||
{
|
||||
id: 3002,
|
||||
name: '辉石',
|
||||
code: 'EX-F1-002',
|
||||
category: '矿物标本',
|
||||
era: '古生代',
|
||||
origin: '中国辽宁',
|
||||
description: '辉石是主要的造岩矿物之一,属链状硅酸盐矿物。它的晶体通常呈短柱状,具有两组近于垂直的解理,是火成岩和变质岩的常见组分。',
|
||||
descriptionEn: 'Pyroxene is a major group of rock-forming silicate minerals. They are common components of both igneous and metamorphic rocks.',
|
||||
audioUrl: '',
|
||||
videoUrl: '',
|
||||
coverImageUrl: '/images/exhibits/pyroxene.jpg',
|
||||
modelUrl: '/models/exhibits/pyroxene.glb',
|
||||
spaceId: 101,
|
||||
spaceName: '地球厅',
|
||||
zoneId: 1001,
|
||||
zoneName: '宝石矿物单元',
|
||||
poiId: 'modelpoi_L-2_e2ec8ce8ba'
|
||||
},
|
||||
{
|
||||
id: 3003,
|
||||
name: '石英',
|
||||
code: 'EX-F1-003',
|
||||
category: '矿物标本',
|
||||
era: '中生代',
|
||||
origin: '中国江苏',
|
||||
description: '石英是地球表面分布最广的矿物之一,化学成分为二氧化硅。它硬度高,化学性质稳定,晶体形态多样,是许多工业和工艺品的重要原料。',
|
||||
descriptionEn: 'Quartz is a mineral composed of silicon and oxygen atoms in a continuous framework of SiO4 silicon–oxygen tetrahedra.',
|
||||
audioUrl: '',
|
||||
videoUrl: '',
|
||||
coverImageUrl: '/images/exhibits/quartz.jpg',
|
||||
modelUrl: '',
|
||||
spaceId: 101,
|
||||
spaceName: '地球厅',
|
||||
zoneId: 1001,
|
||||
zoneName: '宝石矿物单元',
|
||||
poiId: 'modelpoi_L-2_e2ec8ce8ba'
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,172 +3,109 @@ import type {
|
||||
MuseumHall
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
formatNavFloorLabel
|
||||
} from '@/data/adapters/navAssetsAdapter'
|
||||
import {
|
||||
defaultStaticNavAssetsProvider,
|
||||
type StaticNavAssetsProvider,
|
||||
type StaticNavPoiPayload
|
||||
} from '@/data/providers/staticNavAssetsProvider'
|
||||
SGS_SCENE_EXPLAIN_DATASET,
|
||||
type SgsSceneExhibitMock,
|
||||
type SgsSceneSpaceMock
|
||||
} from '@/data/mock/sgsSceneExplainData'
|
||||
|
||||
export interface MuseumContentProvider {
|
||||
listExhibits(): Promise<MuseumExhibit[]>
|
||||
listHalls(): Promise<MuseumHall[]>
|
||||
}
|
||||
|
||||
const contentImage = '/static/exhibit-placeholder.jpg'
|
||||
const hallImage = '/static/hall-placeholder.jpg'
|
||||
const normalizeContentId = (prefix: string, value: string | number) => `${prefix}-sgs-${value}`
|
||||
|
||||
const cleanPoiName = (poi: StaticNavPoiPayload) => poi.name
|
||||
.replace(new RegExp(`^${poi.floorId}\\s*`), '')
|
||||
.replace(/^L\d+(?:\.\d+)?\s+/, '')
|
||||
.replace(/^L-\d+(?:\.\d+)?\s+/, '')
|
||||
.replace(/^L\d+(?:\.\d+)?\s+/, '')
|
||||
.trim()
|
||||
|
||||
const normalizeContentId = (prefix: string, value: string) => `${prefix}-${value}`
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9一-龥]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
|
||||
const isTouringPoi = (poi: StaticNavPoiPayload) => (
|
||||
poi.primaryCategory === 'touring_poi'
|
||||
|| poi.categories?.some((category) => category.topCategory === 'touring_poi') === true
|
||||
)
|
||||
|
||||
const isHallPoi = (poi: StaticNavPoiPayload) => {
|
||||
const name = cleanPoiName(poi)
|
||||
return /展厅|影院|剧场|报告厅|活动室/.test(name)
|
||||
&& !/展柜|装饰/.test(name)
|
||||
const categoryLabelMap: Record<SgsSceneSpaceMock['spaceCategory'], string> = {
|
||||
EXHIBITION: '展陈空间',
|
||||
EXPERIENCE: '体验空间',
|
||||
SERVICE: '服务空间',
|
||||
COMMERCIAL: '商业空间',
|
||||
EDUCATION: '教育空间',
|
||||
CIRCULATION: '公共交通空间',
|
||||
BACK_OFFICE: '后勤空间'
|
||||
}
|
||||
|
||||
const isExhibitCandidate = (poi: StaticNavPoiPayload) => /展柜|装饰/.test(cleanPoiName(poi))
|
||||
|
||||
const hallKeyFromName = (name: string) => {
|
||||
const match = name.match(/(展厅\s*\d+\s*[^\s展柜装饰]+|临展厅\d+|[^\s]+厅|[^\s]+影院|自然剧场|学术报告厅|博物馆之友活动室)/)
|
||||
return (match?.[1] || name)
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/L\d+\s*/g, '')
|
||||
const boundaryLabelMap: Record<NonNullable<SgsSceneSpaceMock['boundaryStatus']>, string> = {
|
||||
DEFINED: '已定义空间边界',
|
||||
UNDEFINED: '空间边界待完善'
|
||||
}
|
||||
|
||||
const exhibitNameFromPoi = (poi: StaticNavPoiPayload) => cleanPoiName(poi)
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/^(展厅\s*\d+\s*)/, '')
|
||||
.trim()
|
||||
const publicSceneSpaces = SGS_SCENE_EXPLAIN_DATASET.spaces.filter((space) => space.isPublic)
|
||||
const publicSpaceById = new Map(publicSceneSpaces.map((space) => [space.id, space]))
|
||||
|
||||
const hallDescription = (hallName: string, floorLabel: string) => (
|
||||
`${hallName}位于${floorLabel},来源于当前导览资源包中的游览 POI。当前内容用于讲解业务结构占位,正式展陈文案、音频和图文媒体待 CMS/API 接入后替换。`
|
||||
)
|
||||
const formatArea = (area?: number) => (typeof area === 'number' ? `${area}㎡` : undefined)
|
||||
|
||||
const exhibitDescription = (exhibitName: string, hallName: string, floorLabel: string) => (
|
||||
`${exhibitName}是${hallName}的讲解点,位置关联到${floorLabel}的导览 POI。当前仅展示内容结构和位置关联,不代表正式展陈文案或可播放音频。`
|
||||
)
|
||||
const hallDescription = (space: SgsSceneSpaceMock) => {
|
||||
const category = categoryLabelMap[space.spaceCategory]
|
||||
const area = formatArea(space.area)
|
||||
const boundary = space.boundaryStatus ? boundaryLabelMap[space.boundaryStatus] : undefined
|
||||
const facts = [space.floorLabel, category, area, boundary].filter(Boolean).join(' · ')
|
||||
|
||||
return `${space.name}来自 SGS 前端地图项目“场景设置”的空间管理数据。${facts ? `当前空间信息:${facts}。` : ''}讲解业务据此展示展厅/展区内容,并通过稳定空间、展品与 POI ID 关联到位置预览。`
|
||||
}
|
||||
|
||||
const exhibitDescription = (exhibit: SgsSceneExhibitMock, hall: SgsSceneSpaceMock | undefined) => {
|
||||
const facts = [
|
||||
exhibit.category,
|
||||
exhibit.era,
|
||||
exhibit.origin,
|
||||
exhibit.zoneName
|
||||
].filter(Boolean).join(' · ')
|
||||
|
||||
return [
|
||||
exhibit.description,
|
||||
facts ? `展陈信息:${facts}。` : '',
|
||||
hall ? `所属空间:${hall.name}(${hall.floorLabel})。` : ''
|
||||
].filter(Boolean).join('\n\n')
|
||||
}
|
||||
|
||||
const tagsForExhibit = (exhibit: SgsSceneExhibitMock, hall: SgsSceneSpaceMock | undefined) => [
|
||||
'SGS场景设置',
|
||||
exhibit.category,
|
||||
exhibit.zoneName,
|
||||
exhibit.audioUrl ? '源数据含音频地址' : '图文讲解',
|
||||
hall ? categoryLabelMap[hall.spaceCategory] : undefined
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
const toMuseumHall = (space: SgsSceneSpaceMock): MuseumHall => ({
|
||||
id: normalizeContentId('hall', space.id),
|
||||
name: space.name,
|
||||
floorId: space.floorId,
|
||||
floorLabel: space.floorLabel,
|
||||
description: hallDescription(space),
|
||||
image: '',
|
||||
exhibitCount: SGS_SCENE_EXPLAIN_DATASET.exhibits.filter((exhibit) => exhibit.spaceId === space.id).length || space.exhibitCount || 0,
|
||||
area: formatArea(space.area),
|
||||
poiId: space.poiId
|
||||
})
|
||||
|
||||
const toMuseumExhibit = (exhibit: SgsSceneExhibitMock): MuseumExhibit => {
|
||||
const hall = publicSpaceById.get(exhibit.spaceId)
|
||||
|
||||
return {
|
||||
id: normalizeContentId('exhibit', exhibit.id),
|
||||
name: exhibit.name,
|
||||
hallId: hall ? normalizeContentId('hall', hall.id) : undefined,
|
||||
hallName: hall?.name || exhibit.spaceName,
|
||||
floorId: hall?.floorId || SGS_SCENE_EXPLAIN_DATASET.floorId,
|
||||
floorLabel: hall?.floorLabel || SGS_SCENE_EXPLAIN_DATASET.floorLabel,
|
||||
image: '',
|
||||
description: exhibitDescription(exhibit, hall),
|
||||
poiId: exhibit.poiId || hall?.poiId,
|
||||
year: exhibit.era,
|
||||
material: exhibit.origin,
|
||||
size: exhibit.code,
|
||||
tags: tagsForExhibit(exhibit, hall)
|
||||
}
|
||||
}
|
||||
|
||||
export class StaticMuseumContentProvider implements MuseumContentProvider {
|
||||
private hallCache: MuseumHall[] | null = null
|
||||
private exhibitCache: MuseumExhibit[] | null = null
|
||||
private loading: Promise<void> | null = null
|
||||
|
||||
constructor(
|
||||
private readonly navAssets: StaticNavAssetsProvider = defaultStaticNavAssetsProvider
|
||||
) {}
|
||||
|
||||
async listExhibits() {
|
||||
await this.ensureLoaded()
|
||||
return this.exhibitCache || []
|
||||
return SGS_SCENE_EXPLAIN_DATASET.exhibits.map(toMuseumExhibit)
|
||||
}
|
||||
|
||||
async listHalls() {
|
||||
await this.ensureLoaded()
|
||||
return this.hallCache || []
|
||||
}
|
||||
|
||||
private async ensureLoaded() {
|
||||
if (this.hallCache && this.exhibitCache) return
|
||||
if (this.loading) return this.loading
|
||||
|
||||
this.loading = this.navAssets.loadPoiIndex()
|
||||
.then((pois) => {
|
||||
const touringPois = pois.filter(isTouringPoi)
|
||||
const hallPois = touringPois.filter(isHallPoi)
|
||||
const hallByKey = new Map<string, MuseumHall>()
|
||||
|
||||
hallPois.forEach((poi) => {
|
||||
const hallName = cleanPoiName(poi)
|
||||
const key = `${poi.floorId}:${hallKeyFromName(hallName)}`
|
||||
const floorLabel = formatNavFloorLabel(poi.floorId)
|
||||
hallByKey.set(key, {
|
||||
id: normalizeContentId('hall', poi.id),
|
||||
name: hallName,
|
||||
floorId: poi.floorId,
|
||||
floorLabel,
|
||||
description: hallDescription(hallName, floorLabel),
|
||||
image: hallImage,
|
||||
exhibitCount: 0,
|
||||
area: '以导览资源包 POI 为准',
|
||||
poiId: poi.id
|
||||
})
|
||||
})
|
||||
|
||||
const fallbackHallFor = (poi: StaticNavPoiPayload) => {
|
||||
const poiName = cleanPoiName(poi)
|
||||
const key = `${poi.floorId}:${hallKeyFromName(poiName)}`
|
||||
const floorLabel = formatNavFloorLabel(poi.floorId)
|
||||
const hallName = hallKeyFromName(poiName)
|
||||
const existing = hallByKey.get(key)
|
||||
if (existing) return existing
|
||||
|
||||
const hall: MuseumHall = {
|
||||
id: normalizeContentId('hall', `${poi.floorId}-${hallName}`),
|
||||
name: hallName,
|
||||
floorId: poi.floorId,
|
||||
floorLabel,
|
||||
description: hallDescription(hallName, floorLabel),
|
||||
image: hallImage,
|
||||
exhibitCount: 0,
|
||||
area: '以导览资源包 POI 为准'
|
||||
}
|
||||
hallByKey.set(key, hall)
|
||||
return hall
|
||||
}
|
||||
|
||||
const exhibits = touringPois
|
||||
.filter(isExhibitCandidate)
|
||||
.map((poi): MuseumExhibit => {
|
||||
const hall = fallbackHallFor(poi)
|
||||
const exhibitName = exhibitNameFromPoi(poi)
|
||||
return {
|
||||
id: normalizeContentId('exhibit', poi.id),
|
||||
name: exhibitName,
|
||||
hallId: hall.id,
|
||||
hallName: hall.name,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: formatNavFloorLabel(poi.floorId),
|
||||
image: contentImage,
|
||||
description: exhibitDescription(exhibitName, hall.name, formatNavFloorLabel(poi.floorId)),
|
||||
poiId: poi.id,
|
||||
tags: ['资源包POI', poi.primaryCategoryZh, poi.iconType || '展项'].filter(Boolean)
|
||||
}
|
||||
})
|
||||
|
||||
const exhibitCountByHallId = exhibits.reduce((result, exhibit) => {
|
||||
if (exhibit.hallId) {
|
||||
result.set(exhibit.hallId, (result.get(exhibit.hallId) || 0) + 1)
|
||||
}
|
||||
return result
|
||||
}, new Map<string, number>())
|
||||
|
||||
this.hallCache = Array.from(hallByKey.values()).map((hall) => ({
|
||||
...hall,
|
||||
exhibitCount: exhibitCountByHallId.get(hall.id) || hall.exhibitCount || 0
|
||||
}))
|
||||
this.exhibitCache = exhibits
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = null
|
||||
})
|
||||
|
||||
return this.loading
|
||||
return publicSceneSpaces.map(toMuseumHall)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user