提交室内导览交互与讲解优化
This commit is contained in:
@@ -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