feat: wire guide data source and POI focus UI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
75
src/usecases/explainUseCase.ts
Normal file
75
src/usecases/explainUseCase.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type {
|
||||
ExplainTrack,
|
||||
MediaAsset,
|
||||
MuseumExhibit,
|
||||
MuseumHall,
|
||||
SearchIndexItem
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
explainRepository,
|
||||
type ExplainRepository
|
||||
} from '@/repositories/ExplainRepository'
|
||||
import {
|
||||
mediaRepository,
|
||||
type MediaRepository
|
||||
} from '@/repositories/MediaRepository'
|
||||
|
||||
export interface ExplainAudioSelection {
|
||||
exhibit: MuseumExhibit
|
||||
track: ExplainTrack | null
|
||||
media: MediaAsset | null
|
||||
playable: boolean
|
||||
unavailableMessage?: string
|
||||
}
|
||||
|
||||
export class ExplainUseCase {
|
||||
constructor(
|
||||
private readonly explain: ExplainRepository = explainRepository,
|
||||
private readonly media: MediaRepository = mediaRepository
|
||||
) {}
|
||||
|
||||
listExhibits() {
|
||||
return this.explain.listExhibits()
|
||||
}
|
||||
|
||||
getExhibitById(id: string) {
|
||||
return this.explain.getExhibitById(id)
|
||||
}
|
||||
|
||||
listHalls(): Promise<MuseumHall[]> {
|
||||
return this.explain.listHalls()
|
||||
}
|
||||
|
||||
getHallById(id: string) {
|
||||
return this.explain.getHallById(id)
|
||||
}
|
||||
|
||||
async listExhibitsByHallId(hallId: string) {
|
||||
const exhibits = await this.explain.listExhibits()
|
||||
return exhibits.filter((exhibit) => exhibit.hallId === hallId)
|
||||
}
|
||||
|
||||
searchExplain(keyword?: string): Promise<SearchIndexItem[]> {
|
||||
return this.explain.searchExplain(keyword)
|
||||
}
|
||||
|
||||
async selectAudioForExhibit(exhibitId: string): Promise<ExplainAudioSelection | null> {
|
||||
const exhibit = await this.explain.getExhibitById(exhibitId)
|
||||
if (!exhibit) return null
|
||||
|
||||
const track = await this.explain.getTrackByExhibitId(exhibitId)
|
||||
const media = track?.mediaId
|
||||
? await this.media.getMediaById(track.mediaId)
|
||||
: null
|
||||
|
||||
return {
|
||||
exhibit,
|
||||
track,
|
||||
media,
|
||||
playable: media?.available === true && Boolean(media.url),
|
||||
unavailableMessage: media?.unavailableReason || '该展项暂无讲解音频'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const explainUseCase = new ExplainUseCase()
|
||||
124
src/usecases/guideUseCase.ts
Normal file
124
src/usecases/guideUseCase.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type {
|
||||
GuideLocationPreviewPlan,
|
||||
GuidePreviewStep,
|
||||
GuideStartLocation,
|
||||
MuseumPoi
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
guideRepository,
|
||||
type GuideRepository
|
||||
} from '@/repositories/GuideRepository'
|
||||
import {
|
||||
guideModelRepository,
|
||||
type GuideModelRepository
|
||||
} from '@/repositories/GuideModelRepository'
|
||||
|
||||
const startSourceLabels: Record<string, string> = {
|
||||
'facility-detail': '设施详情选择',
|
||||
manual: '手动选择'
|
||||
}
|
||||
|
||||
const formatStartSourceLabel = (source?: string) => (
|
||||
source ? startSourceLabels[source] || source : ''
|
||||
)
|
||||
|
||||
const formatStartStepText = (startLocation: GuideStartLocation) => {
|
||||
const sourceLabel = formatStartSourceLabel(startLocation.source)
|
||||
const floorSuffix = startLocation.floor && !startLocation.label.includes(startLocation.floor)
|
||||
? ` · ${startLocation.floor}`
|
||||
: ''
|
||||
const sourceSuffix = sourceLabel ? `(${sourceLabel})` : ''
|
||||
|
||||
return `起点:${startLocation.label}${floorSuffix}${sourceSuffix}`
|
||||
}
|
||||
|
||||
const createPreviewSteps = (
|
||||
poi: MuseumPoi | null,
|
||||
startLocation: GuideStartLocation | null,
|
||||
routeUnavailableMessage: string
|
||||
): GuidePreviewStep[] => {
|
||||
if (startLocation) {
|
||||
return [
|
||||
{
|
||||
id: 'start',
|
||||
text: formatStartStepText(startLocation)
|
||||
},
|
||||
{
|
||||
id: 'target',
|
||||
text: poi
|
||||
? `目标:${poi.floorLabel} · ${poi.primaryCategory.label} · 位置预览`
|
||||
: '目标来自 clean 导览数据,暂仅支持位置预览'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'floor',
|
||||
text: poi ? `目标位于 ${poi.floorLabel} · ${poi.primaryCategory.label}` : '目标来自 clean 导览数据'
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
text: routeUnavailableMessage
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export class GuideUseCase {
|
||||
constructor(
|
||||
private readonly guide: GuideRepository = guideRepository,
|
||||
private readonly guideModel: GuideModelRepository = guideModelRepository
|
||||
) {}
|
||||
|
||||
getRouteReadiness() {
|
||||
return this.guide.getRouteReadiness()
|
||||
}
|
||||
|
||||
getFloors() {
|
||||
return this.guide.getFloors()
|
||||
}
|
||||
|
||||
getAssetBaseUrl() {
|
||||
return this.guide.getAssetBaseUrl()
|
||||
}
|
||||
|
||||
getModelSource() {
|
||||
return this.guideModel
|
||||
}
|
||||
|
||||
normalizeFloorId(labelOrId: string) {
|
||||
return this.guide.normalizeFloorId(labelOrId)
|
||||
}
|
||||
|
||||
searchPois(keyword?: string) {
|
||||
return this.guide.searchPois(keyword)
|
||||
}
|
||||
|
||||
getPoiById(id: string) {
|
||||
return this.guide.getPoiById(id)
|
||||
}
|
||||
|
||||
async createLocationPreviewPlan(
|
||||
facilityId: string,
|
||||
target: string,
|
||||
startLocation: GuideStartLocation | null = null
|
||||
): Promise<GuideLocationPreviewPlan> {
|
||||
const poi = facilityId ? await this.guide.getPoiById(facilityId) : null
|
||||
const routeReadiness = this.guide.getRouteReadiness()
|
||||
const targetLocation = poi ? await this.guide.getLocationPreview(poi.id) : null
|
||||
|
||||
return {
|
||||
id: `route-${facilityId || 'custom'}`,
|
||||
facilityId,
|
||||
target: target || poi?.name || '目标地点',
|
||||
summary: poi
|
||||
? `${poi.floorLabel} · ${poi.primaryCategory.label} · 位置预览`
|
||||
: '位置预览 · 尚未接入正式路线图',
|
||||
steps: createPreviewSteps(poi, startLocation, routeReadiness.message),
|
||||
targetLocation,
|
||||
startLocation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const guideUseCase = new GuideUseCase()
|
||||
Reference in New Issue
Block a user