288
src/domain/poiCategories.ts
Normal file
288
src/domain/poiCategories.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import type {
|
||||
MuseumCategory,
|
||||
MuseumPoi
|
||||
} from '@/domain/museum'
|
||||
|
||||
export type PoiCategoryId =
|
||||
| 'exhibition-hall'
|
||||
| 'cinema'
|
||||
| 'ticket-office'
|
||||
| 'service-center'
|
||||
| 'dining'
|
||||
| 'souvenir'
|
||||
| 'nursing-room'
|
||||
| 'restroom'
|
||||
| 'elevator'
|
||||
| 'stairs'
|
||||
| 'escalator'
|
||||
|
||||
export interface PoiCategoryDefinition {
|
||||
id: PoiCategoryId
|
||||
label: string
|
||||
icon: string
|
||||
order: number
|
||||
homeVisible: boolean
|
||||
keywords: readonly string[]
|
||||
categoryIds: readonly string[]
|
||||
iconTypes: readonly string[]
|
||||
}
|
||||
|
||||
const definitions: PoiCategoryDefinition[] = [
|
||||
{
|
||||
id: 'exhibition-hall',
|
||||
label: '展厅',
|
||||
icon: 'exhibition',
|
||||
order: 1,
|
||||
homeVisible: true,
|
||||
keywords: ['展厅', '展览', '展馆', 'exhibition_hall', 'touring_poi', 'hall'],
|
||||
categoryIds: ['exhibition_hall', 'exhibition_hall_entrance', 'touring_poi'],
|
||||
iconTypes: ['exhibition_hall', 'hall_entrance', 'exhibition']
|
||||
},
|
||||
{
|
||||
id: 'cinema',
|
||||
label: '影院',
|
||||
icon: 'cinema',
|
||||
order: 2,
|
||||
homeVisible: false,
|
||||
keywords: ['影院', '影厅', '剧场', '报告厅', 'cinema', 'theater'],
|
||||
categoryIds: ['space_theater'],
|
||||
iconTypes: ['cinema', 'theater']
|
||||
},
|
||||
{
|
||||
id: 'ticket-office',
|
||||
label: '售票处',
|
||||
icon: 'ticket',
|
||||
order: 3,
|
||||
homeVisible: false,
|
||||
keywords: ['售票处', '售票', '票务', 'ticket_office'],
|
||||
categoryIds: [],
|
||||
iconTypes: ['ticket_office']
|
||||
},
|
||||
{
|
||||
id: 'service-center',
|
||||
label: '服务中心',
|
||||
icon: 'service',
|
||||
order: 4,
|
||||
homeVisible: false,
|
||||
keywords: ['服务中心', '服务台', '咨询台', 'service_desk'],
|
||||
categoryIds: [],
|
||||
iconTypes: ['service_desk']
|
||||
},
|
||||
{
|
||||
id: 'dining',
|
||||
label: '餐饮',
|
||||
icon: 'restaurant',
|
||||
order: 5,
|
||||
homeVisible: false,
|
||||
keywords: ['餐饮', '餐厅', '快餐', '咖啡', 'restaurant', 'dining', 'food', 'cafe'],
|
||||
categoryIds: [],
|
||||
iconTypes: ['restaurant', 'dining', 'food', 'cafe']
|
||||
},
|
||||
{
|
||||
id: 'souvenir',
|
||||
label: '文创',
|
||||
icon: 'bag',
|
||||
order: 6,
|
||||
homeVisible: false,
|
||||
keywords: ['文创', '纪念品', '商店', 'souvenir', 'gift', 'shop'],
|
||||
categoryIds: [],
|
||||
iconTypes: ['souvenir', 'gift', 'shop', 'cultural_creative']
|
||||
},
|
||||
{
|
||||
id: 'nursing-room',
|
||||
label: '母婴室',
|
||||
icon: 'nursing',
|
||||
order: 7,
|
||||
homeVisible: true,
|
||||
keywords: ['母婴室', '母婴间', 'mother_baby_room', 'nursing_room'],
|
||||
categoryIds: [],
|
||||
iconTypes: ['mother_baby_room', 'nursing_room']
|
||||
},
|
||||
{
|
||||
id: 'restroom',
|
||||
label: '卫生间',
|
||||
icon: 'restroom',
|
||||
order: 8,
|
||||
homeVisible: true,
|
||||
keywords: ['卫生间', '洗手间', '厕所', 'toilet', 'accessible_toilet', 'restroom'],
|
||||
categoryIds: [],
|
||||
iconTypes: ['toilet', 'accessible_toilet', 'restroom', 'restroom_accessible']
|
||||
},
|
||||
{
|
||||
id: 'elevator',
|
||||
label: '电梯',
|
||||
icon: 'elevator',
|
||||
order: 9,
|
||||
homeVisible: true,
|
||||
keywords: ['电梯', 'elevator'],
|
||||
categoryIds: [],
|
||||
iconTypes: ['elevator']
|
||||
},
|
||||
{
|
||||
id: 'stairs',
|
||||
label: '楼梯',
|
||||
icon: 'stairs',
|
||||
order: 10,
|
||||
homeVisible: true,
|
||||
keywords: ['楼梯', 'stairs', 'stair'],
|
||||
categoryIds: [],
|
||||
iconTypes: ['stairs', 'stair']
|
||||
},
|
||||
{
|
||||
id: 'escalator',
|
||||
label: '扶梯',
|
||||
icon: 'escalator',
|
||||
order: 11,
|
||||
homeVisible: true,
|
||||
keywords: ['扶梯', 'escalator'],
|
||||
categoryIds: [],
|
||||
iconTypes: ['escalator']
|
||||
}
|
||||
]
|
||||
|
||||
export const POI_CATEGORIES = Object.freeze(
|
||||
[...definitions].sort((left, right) => left.order - right.order)
|
||||
)
|
||||
|
||||
const homeCategoryOrder: readonly PoiCategoryId[] = [
|
||||
'exhibition-hall',
|
||||
'restroom',
|
||||
'nursing-room',
|
||||
'elevator',
|
||||
'stairs',
|
||||
'escalator'
|
||||
]
|
||||
|
||||
export const HOME_POI_CATEGORIES = Object.freeze(
|
||||
homeCategoryOrder.map((categoryId) => (
|
||||
POI_CATEGORIES.find((category) => category.id === categoryId)!
|
||||
))
|
||||
)
|
||||
|
||||
type PoiCategorySource = Pick<MuseumPoi, 'name' | 'primaryCategory' | 'categories'>
|
||||
|
||||
const normalizeValue = (value?: string | null) => (value || '').trim().toLowerCase()
|
||||
|
||||
const categoryValues = (categories: MuseumCategory[]) => categories.flatMap((category) => [
|
||||
category.id,
|
||||
category.label,
|
||||
category.iconType || ''
|
||||
])
|
||||
|
||||
export const matchesPoiCategory = (
|
||||
poi: PoiCategorySource,
|
||||
definition: PoiCategoryDefinition
|
||||
) => {
|
||||
const categories = [poi.primaryCategory, ...(poi.categories || [])]
|
||||
const normalizedCategoryIds = new Set(categories.map((category) => normalizeValue(category.id)))
|
||||
const normalizedIconTypes = new Set(categories.map((category) => normalizeValue(category.iconType)))
|
||||
|
||||
if (definition.categoryIds.some((categoryId) => normalizedCategoryIds.has(normalizeValue(categoryId)))) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (definition.iconTypes.some((iconType) => normalizedIconTypes.has(normalizeValue(iconType)))) {
|
||||
return true
|
||||
}
|
||||
|
||||
const searchableText = [
|
||||
poi.name,
|
||||
...categoryValues(categories)
|
||||
]
|
||||
.map(normalizeValue)
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return definition.keywords.some((keyword) => searchableText.includes(normalizeValue(keyword)))
|
||||
}
|
||||
|
||||
export const getPoiCategoryById = (categoryId?: string | null) => (
|
||||
POI_CATEGORIES.find((category) => category.id === categoryId) || null
|
||||
)
|
||||
|
||||
export const findPoiCategoryByKeyword = (keyword: string) => {
|
||||
const normalizedKeyword = normalizeValue(keyword)
|
||||
if (!normalizedKeyword) return null
|
||||
|
||||
return POI_CATEGORIES.find((category) => (
|
||||
normalizeValue(category.label) === normalizedKeyword
|
||||
|| category.keywords.some((item) => normalizeValue(item) === normalizedKeyword)
|
||||
)) || null
|
||||
}
|
||||
|
||||
export const resolvePoiCategory = (poi: PoiCategorySource) => (
|
||||
POI_CATEGORIES.find((category) => matchesPoiCategory(poi, category)) || null
|
||||
)
|
||||
|
||||
export const isPoiSearchCategorySupported = (poi: PoiCategorySource) => Boolean(resolvePoiCategory(poi))
|
||||
|
||||
export type PoiDataIssueCode =
|
||||
| 'missing-id'
|
||||
| 'missing-name'
|
||||
| 'missing-floor'
|
||||
| 'missing-position'
|
||||
| 'invalid-position'
|
||||
| 'unsupported-category'
|
||||
|
||||
export interface PoiDataIssue {
|
||||
code: PoiDataIssueCode
|
||||
poiId: string
|
||||
poiName: string
|
||||
}
|
||||
|
||||
const hasFinitePosition = (position?: [number, number, number]) => (
|
||||
Array.isArray(position)
|
||||
&& position.length === 3
|
||||
&& position.every((value) => Number.isFinite(value))
|
||||
)
|
||||
|
||||
export const getPoiDataIssues = (poi: MuseumPoi): PoiDataIssue[] => {
|
||||
const issues: PoiDataIssue[] = []
|
||||
const issueBase = {
|
||||
poiId: poi.id || '',
|
||||
poiName: poi.name || ''
|
||||
}
|
||||
|
||||
if (!poi.id?.trim()) issues.push({ code: 'missing-id', ...issueBase })
|
||||
if (!poi.name?.trim()) issues.push({ code: 'missing-name', ...issueBase })
|
||||
if (!poi.floorId?.trim() || !poi.floorLabel?.trim()) issues.push({ code: 'missing-floor', ...issueBase })
|
||||
if (!poi.positionGltf) {
|
||||
issues.push({ code: 'missing-position', ...issueBase })
|
||||
} else if (!hasFinitePosition(poi.positionGltf)) {
|
||||
issues.push({ code: 'invalid-position', ...issueBase })
|
||||
}
|
||||
if (!isPoiSearchCategorySupported(poi)) issues.push({ code: 'unsupported-category', ...issueBase })
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
export interface PoiCollectionInspection {
|
||||
issues: PoiDataIssue[]
|
||||
duplicateIds: string[]
|
||||
}
|
||||
|
||||
export const inspectPoiCollection = (pois: MuseumPoi[]): PoiCollectionInspection => {
|
||||
const seenIds = new Set<string>()
|
||||
const duplicateIds = new Set<string>()
|
||||
|
||||
pois.forEach((poi) => {
|
||||
if (!poi.id) return
|
||||
if (seenIds.has(poi.id)) duplicateIds.add(poi.id)
|
||||
seenIds.add(poi.id)
|
||||
})
|
||||
|
||||
return {
|
||||
issues: pois.flatMap(getPoiDataIssues),
|
||||
duplicateIds: Array.from(duplicateIds)
|
||||
}
|
||||
}
|
||||
|
||||
export const warnPoiCollectionIssues = (source: string, pois: MuseumPoi[]) => {
|
||||
if (!import.meta.env.DEV) return
|
||||
|
||||
const inspection = inspectPoiCollection(pois)
|
||||
if (!inspection.issues.length && !inspection.duplicateIds.length) return
|
||||
|
||||
// 开发期集中输出数据契约问题,避免在页面组件中散落源数据校验。
|
||||
console.warn(`[POI 数据校验] ${source}`, inspection)
|
||||
}
|
||||
Reference in New Issue
Block a user