统一首页点位详情目标解析

This commit is contained in:
cxk
2026-07-20 08:09:58 +08:00
parent 153be754a9
commit 298e7d7606
5 changed files with 388 additions and 133 deletions

View File

@@ -0,0 +1,159 @@
import type {
MuseumCategory,
MuseumHall,
MuseumPoiEntrance,
MuseumPoiKind
} from '@/domain/museum'
import { explainUseCase } from '@/usecases/explainUseCase'
import { explainGuideStopListUrl } from '@/utils/explainNavigation'
export type PoiDetailType = 'hall' | 'exhibit' | 'facility'
export type PoiDetailSource = 'map' | 'search'
export interface PoiDetailSourcePoi {
id: string
name: string
floorId: string
floorLabel?: string
primaryCategory?: MuseumCategory | string
kind?: MuseumPoiKind
hallId?: string
hallName?: string
exhibitId?: string
sourcePlaceId?: string
entrances?: MuseumPoiEntrance[]
}
export interface PoiDetailTarget {
poiId: string
floorId: string
detailType: PoiDetailType
detailId: string
hallId?: string
exhibitId?: string
url: string
failureMessage: string
}
const normalizeMatchText = (value?: string) => (value || '')
.trim()
.replace(/[\s()【】[\]_-]/g, '')
.replace(/^展厅\d*/, '')
.toLowerCase()
const categoryIdOf = (poi: PoiDetailSourcePoi) => (
typeof poi.primaryCategory === 'string'
? poi.primaryCategory
: poi.primaryCategory?.id || ''
)
const isHallNameFallbackAllowed = (poi: PoiDetailSourcePoi) => {
const categoryId = categoryIdOf(poi)
return (
poi.kind === 'hall'
|| poi.kind === 'hall_entrance'
|| categoryId === 'touring_poi'
|| categoryId === 'exhibition_hall'
|| categoryId === 'exhibition_hall_entrance'
)
}
const appendQuery = (url: string, values: Record<string, string | number>) => {
const [path, query = ''] = url.split('?')
const params = new URLSearchParams(query)
Object.entries(values).forEach(([key, value]) => params.set(key, String(value)))
return `${path}?${params.toString()}`
}
const createCanonicalDetailUrl = (url: string, poi: PoiDetailSourcePoi) => (
// 详情 URL 只承载稳定目标,搜索筛选等首页状态由调用方单独保存。
appendQuery(url, {
source: 'guide-poi',
poiId: poi.id,
floorId: poi.floorId
})
)
const resolveHallByStableRelationship = (
poi: PoiDetailSourcePoi,
halls: MuseumHall[]
) => {
if (poi.hallId) {
const hall = halls.find((item) => item.id === poi.hallId)
if (hall) return hall
}
const stablePoiIds = new Set([
poi.id,
poi.sourcePlaceId,
...(poi.entrances || []).flatMap((entrance) => [
entrance.id,
entrance.sourcePlaceId
])
].filter((id): id is string => Boolean(id)))
return halls.find((hall) => (
(hall.poiId ? stablePoiIds.has(hall.poiId) : false)
|| (hall.location?.poiId ? stablePoiIds.has(hall.location.poiId) : false)
)) || null
}
const resolveHallByControlledNameFallback = (
poi: PoiDetailSourcePoi,
halls: MuseumHall[]
) => {
// 名称只对明确的展厅语义兜底,避免普通空间误入讲解列表。
if (!isHallNameFallbackAllowed(poi)) return null
const poiHallName = normalizeMatchText(poi.hallName || poi.name)
if (!poiHallName) return null
return halls.find((hall) => normalizeMatchText(hall.name) === poiHallName) || null
}
export class PoiDetailUseCase {
async resolve(poi: PoiDetailSourcePoi): Promise<PoiDetailTarget> {
if (poi.exhibitId) {
return {
poiId: poi.id,
floorId: poi.floorId,
detailType: 'exhibit',
detailId: poi.exhibitId,
exhibitId: poi.exhibitId,
url: createCanonicalDetailUrl(`/pages/exhibit/detail?id=${encodeURIComponent(poi.exhibitId)}`, poi),
failureMessage: '展品详情打开失败,请重试'
}
}
const halls = await explainUseCase.listHalls().catch((error) => {
console.warn('展厅关联数据加载失败,点位详情降级为普通设施:', error)
return []
})
const hall = resolveHallByStableRelationship(poi, halls)
|| resolveHallByControlledNameFallback(poi, halls)
if (hall) {
return {
poiId: poi.id,
floorId: poi.floorId,
detailType: 'hall',
detailId: hall.id,
hallId: hall.id,
url: createCanonicalDetailUrl(explainGuideStopListUrl(hall.id, hall.name), poi),
failureMessage: '展厅讲解列表打开失败,请重试'
}
}
return {
poiId: poi.id,
floorId: poi.floorId,
detailType: 'facility',
detailId: poi.id,
url: createCanonicalDetailUrl(
`/pages/facility/detail?id=${encodeURIComponent(poi.id)}&target=${encodeURIComponent(poi.name)}`,
poi
),
failureMessage: '点位详情打开失败,请重试'
}
}
}
export const poiDetailUseCase = new PoiDetailUseCase()