feat: wire guide data source and POI focus UI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lyf
2026-06-12 09:25:22 +08:00
parent a6bfda30e1
commit 2055b13b90
58 changed files with 5768 additions and 1898 deletions

View File

@@ -0,0 +1,120 @@
import type {
ExplainTrack,
MuseumExhibit,
MuseumHall,
SearchIndexItem
} from '@/domain/museum'
import {
mediaRepository,
type MediaRepository
} from '@/repositories/MediaRepository'
import {
museumContentRepository,
type MuseumContentRepository
} from '@/repositories/MuseumContentRepository'
export interface ExplainRepository {
listExhibits(): Promise<MuseumExhibit[]>
getExhibitById(id: string): Promise<MuseumExhibit | null>
listHalls(): Promise<MuseumHall[]>
getHallById(id: string): Promise<MuseumHall | null>
listTracks(): Promise<ExplainTrack[]>
getTrackByExhibitId(exhibitId: string): Promise<ExplainTrack | null>
searchExplain(keyword?: string): Promise<SearchIndexItem[]>
}
const trackTitleFor = (exhibit: MuseumExhibit) => `${exhibit.name}讲解`
export class DefaultExplainRepository implements ExplainRepository {
constructor(
private readonly content: MuseumContentRepository = museumContentRepository,
private readonly media: MediaRepository = mediaRepository
) {}
listExhibits() {
return this.content.listExhibits()
}
getExhibitById(id: string) {
return this.content.getExhibitById(id)
}
listHalls() {
return this.content.listHalls()
}
getHallById(id: string) {
return this.content.getHallById(id)
}
async listTracks() {
const exhibits = await this.content.listExhibits()
return Promise.all(exhibits.map(async (exhibit): Promise<ExplainTrack> => {
const media = await this.media.getMediaForExplainTrack(exhibit.id)
return {
id: `track-${exhibit.id}`,
exhibitId: exhibit.id,
hallId: exhibit.hallId,
title: trackTitleFor(exhibit),
summary: exhibit.description,
mediaId: media?.id,
coverImage: exhibit.image,
poiId: exhibit.poiId,
floorId: exhibit.floorId,
available: media?.available === true
}
}))
}
async getTrackByExhibitId(exhibitId: string) {
const tracks = await this.listTracks()
return tracks.find((track) => track.exhibitId === exhibitId) || null
}
async searchExplain(keyword = '') {
const normalizedKeyword = keyword.trim().toLowerCase()
const [exhibits, halls] = await Promise.all([
this.content.listExhibits(),
this.content.listHalls()
])
const exhibitItems = exhibits
.filter((exhibit) => !normalizedKeyword || [
exhibit.name,
exhibit.hallName,
exhibit.floorLabel,
...(exhibit.tags || [])
].filter(Boolean).join(' ').toLowerCase().includes(normalizedKeyword))
.map<SearchIndexItem>((exhibit) => ({
id: exhibit.id,
name: exhibit.name,
desc: `${exhibit.hallName || '展项'} · ${exhibit.floorLabel || ''}`.trim(),
type: 'exhibit',
exhibitId: exhibit.id,
hallId: exhibit.hallId,
poiId: exhibit.poiId,
floorId: exhibit.floorId
}))
const hallItems = halls
.filter((hall) => !normalizedKeyword || [
hall.name,
hall.floorLabel,
hall.description
].filter(Boolean).join(' ').toLowerCase().includes(normalizedKeyword))
.map<SearchIndexItem>((hall) => ({
id: hall.id,
name: hall.name,
desc: `${hall.floorLabel || ''} · ${hall.exhibitCount || 0}件展项`.trim(),
type: 'hall',
hallId: hall.id,
poiId: hall.poiId,
floorId: hall.floorId
}))
return [...exhibitItems, ...hallItems]
}
}
export const explainRepository = new DefaultExplainRepository()