feat: refine guide explain UX and QA docs
This commit is contained in:
@@ -2,121 +2,173 @@ import type {
|
||||
MuseumExhibit,
|
||||
MuseumHall
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
formatNavFloorLabel
|
||||
} from '@/data/adapters/navAssetsAdapter'
|
||||
import {
|
||||
defaultStaticNavAssetsProvider,
|
||||
type StaticNavAssetsProvider,
|
||||
type StaticNavPoiPayload
|
||||
} from '@/data/providers/staticNavAssetsProvider'
|
||||
|
||||
export interface MuseumContentProvider {
|
||||
listExhibits(): Promise<MuseumExhibit[]>
|
||||
listHalls(): Promise<MuseumHall[]>
|
||||
}
|
||||
|
||||
const transitionalHalls: MuseumHall[] = [
|
||||
{
|
||||
id: 'hall-evolution',
|
||||
name: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
description: '自然演化主题内容。当前为内容仓储过渡数据,待 CMS/API 接入后替换。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 3,
|
||||
area: '待补充'
|
||||
},
|
||||
{
|
||||
id: 'hall-dinosaur',
|
||||
name: '恐龙厅',
|
||||
floorId: 'L2',
|
||||
floorLabel: '2F',
|
||||
description: '恐龙与古生物主题内容。当前为内容仓储过渡数据,待 CMS/API 接入后替换。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 2,
|
||||
area: '待补充'
|
||||
},
|
||||
{
|
||||
id: 'hall-biology',
|
||||
name: '生物厅',
|
||||
floorId: 'L3',
|
||||
floorLabel: '3F',
|
||||
description: '生物多样性主题内容。当前为内容仓储过渡数据,待 CMS/API 接入后替换。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 2,
|
||||
area: '待补充'
|
||||
}
|
||||
]
|
||||
const contentImage = '/static/exhibit-placeholder.jpg'
|
||||
const hallImage = '/static/hall-placeholder.jpg'
|
||||
|
||||
const transitionalExhibits: MuseumExhibit[] = [
|
||||
{
|
||||
id: 'exhibit-ginkgo-fossil',
|
||||
name: '银杏化石',
|
||||
hallId: 'hall-evolution',
|
||||
hallName: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '银杏化石内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['化石', '古植物']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-trilobite-fossil',
|
||||
name: '三叶虫化石',
|
||||
hallId: 'hall-evolution',
|
||||
hallName: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '三叶虫化石内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['化石', '古生物']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-brachiosaurus',
|
||||
name: '腕龙骨骼',
|
||||
hallId: 'hall-dinosaur',
|
||||
hallName: '恐龙厅',
|
||||
floorId: 'L2',
|
||||
floorLabel: '2F',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '腕龙骨骼内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['恐龙', '骨骼']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-archaeopteryx',
|
||||
name: '始祖鸟标本',
|
||||
hallId: 'hall-evolution',
|
||||
hallName: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '始祖鸟标本内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['演化', '标本']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-mammoth-tusk',
|
||||
name: '猛犸象象牙',
|
||||
hallId: 'hall-biology',
|
||||
hallName: '生物厅',
|
||||
floorId: 'L3',
|
||||
floorLabel: '3F',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '猛犸象象牙内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['古生物', '标本']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-coral-reef',
|
||||
name: '珊瑚礁标本',
|
||||
hallId: 'hall-biology',
|
||||
hallName: '生物厅',
|
||||
floorId: 'L3',
|
||||
floorLabel: '3F',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '珊瑚礁标本内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['海洋生物', '标本']
|
||||
}
|
||||
]
|
||||
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 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 exhibitNameFromPoi = (poi: StaticNavPoiPayload) => cleanPoiName(poi)
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/^(展厅\s*\d+\s*)/, '')
|
||||
.trim()
|
||||
|
||||
const hallDescription = (hallName: string, floorLabel: string) => (
|
||||
`${hallName}位于${floorLabel},来源于当前导览资源包中的游览 POI。当前内容用于讲解业务结构占位,正式展陈文案、音频和图文媒体待 CMS/API 接入后替换。`
|
||||
)
|
||||
|
||||
const exhibitDescription = (exhibitName: string, hallName: string, floorLabel: string) => (
|
||||
`${exhibitName}是${hallName}的讲解点,位置关联到${floorLabel}的导览 POI。当前仅展示内容结构和位置关联,不代表正式展陈文案或可播放音频。`
|
||||
)
|
||||
|
||||
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() {
|
||||
return transitionalExhibits
|
||||
await this.ensureLoaded()
|
||||
return this.exhibitCache || []
|
||||
}
|
||||
|
||||
async listHalls() {
|
||||
return transitionalHalls
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user