feat: refine guide explain UX and QA docs

This commit is contained in:
lyf
2026-06-12 17:20:39 +08:00
parent 2055b13b90
commit a7c1879f60
27 changed files with 2831 additions and 1362 deletions

View File

@@ -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
}
}