修复讲解详情接口并展示展厅统计
This commit is contained in:
@@ -47,7 +47,7 @@
|
||||
<view class="floor-badge">
|
||||
<text class="floor-badge-text">{{ hall.floorLabel || '楼层待补充' }}</text>
|
||||
</view>
|
||||
<text class="hall-meta-text">{{ hall.explainCount > 0 ? `${hall.explainCount} 条讲解` : '暂无讲解' }}</text>
|
||||
<text class="hall-meta-text">{{ hallMetaText(hall) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -130,6 +130,8 @@ export interface ExplainHallSelectItem {
|
||||
floorLabel?: string
|
||||
image?: string
|
||||
explainCount: number
|
||||
businessUnitCount?: number
|
||||
guideStopCount?: number
|
||||
searchText: string
|
||||
}
|
||||
|
||||
@@ -227,6 +229,12 @@ const hallIconUrl = (hall: ExplainHallSelectItem) => {
|
||||
|
||||
const hallIconText = (name: string) => name.trim().slice(0, 1) || '讲'
|
||||
|
||||
const hallMetaText = (hall: ExplainHallSelectItem) => (
|
||||
typeof hall.businessUnitCount === 'number' || typeof hall.guideStopCount === 'number'
|
||||
? `${hall.businessUnitCount || 0} 个业务单元 · ${hall.guideStopCount || 0} 个讲解点`
|
||||
: hall.explainCount > 0 ? `${hall.explainCount} 个展项` : '展厅'
|
||||
)
|
||||
|
||||
const handleHallClick = (hallId: string) => {
|
||||
emit('hallClick', hallId)
|
||||
}
|
||||
|
||||
@@ -294,17 +294,14 @@ export const toBackendGuideStop = (source: BackendGuideStop): ExplainGuideStop |
|
||||
const id = stringifyId(source.id)
|
||||
if (!id) return null
|
||||
|
||||
const targetType = normalizeAudioTargetType(source.targetType) || 'STOP'
|
||||
const targetId = stringifyId(source.targetId) || id
|
||||
|
||||
return {
|
||||
id,
|
||||
name: firstText(source.name, `讲解点${id}`),
|
||||
hallId: stringifyId(source.hallId) || undefined,
|
||||
hallName: firstText(source.hallName) || undefined,
|
||||
floorId: stringifyId(source.floorId) || undefined,
|
||||
targetType,
|
||||
targetId,
|
||||
targetType: 'STOP',
|
||||
targetId: id,
|
||||
coverImageUrl: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || undefined,
|
||||
description: firstText(source.description) || undefined,
|
||||
hasAudio: source.hasAudio === true,
|
||||
|
||||
15
src/domain/explainDetailTarget.ts
Normal file
15
src/domain/explainDetailTarget.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type {
|
||||
ExplainGuideStop
|
||||
} from './museum'
|
||||
|
||||
export interface ExplainDetailTarget {
|
||||
targetType: 'STOP'
|
||||
targetId: string
|
||||
}
|
||||
|
||||
export const normalizeExplainDetailTargetFromGuideStop = (
|
||||
guideStop: Pick<ExplainGuideStop, 'id'>
|
||||
): ExplainDetailTarget => ({
|
||||
targetType: 'STOP',
|
||||
targetId: String(guideStop.id)
|
||||
})
|
||||
@@ -185,11 +185,15 @@ onLoad(async (options: any = {}) => {
|
||||
if (!exhibitId) return
|
||||
|
||||
const rawTargetType = Array.isArray(options.targetType) ? options.targetType[0] : options.targetType
|
||||
const targetType: AudioPlayTargetType | undefined = rawTargetType === 'ITEM' || rawTargetType === 'STOP'
|
||||
? rawTargetType
|
||||
: undefined
|
||||
const targetType: AudioPlayTargetType | undefined = rawTargetType === 'ITEM'
|
||||
? 'ITEM'
|
||||
: rawTargetType === 'STOP' || rawTargetType === 'GUIDE_STOP'
|
||||
? 'STOP'
|
||||
: undefined
|
||||
const rawTargetId = Array.isArray(options.targetId) ? options.targetId[0] : options.targetId
|
||||
const targetId = rawTargetId ? String(rawTargetId) : undefined
|
||||
const targetId = rawTargetType === 'GUIDE_STOP'
|
||||
? String(exhibitId)
|
||||
: rawTargetId ? String(rawTargetId) : undefined
|
||||
|
||||
try {
|
||||
const exhibitData = await explainUseCase.enterExplainDetail({
|
||||
|
||||
@@ -41,6 +41,9 @@ import ExplainHallSelect, {
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
normalizeExplainDetailTargetFromGuideStop
|
||||
} from '@/domain/explainDetailTarget'
|
||||
import type {
|
||||
ExplainBusinessUnit,
|
||||
MuseumHall
|
||||
@@ -63,13 +66,18 @@ let explainLoadPromise: Promise<void> | null = null
|
||||
|
||||
const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
|
||||
|
||||
const buildExplainHallItems = (halls: MuseumHall[]): ExplainHallSelectItem[] => (
|
||||
const buildExplainHallItems = (
|
||||
halls: MuseumHall[],
|
||||
summaries: Awaited<ReturnType<typeof explainUseCase.loadExplainHallSummaries>> = {}
|
||||
): ExplainHallSelectItem[] => (
|
||||
halls.map((hall) => ({
|
||||
id: hall.id,
|
||||
name: hall.name,
|
||||
floorLabel: hall.floorLabel,
|
||||
image: hall.image,
|
||||
explainCount: hall.exhibitCount || 0,
|
||||
businessUnitCount: summaries[hall.id]?.businessUnitCount,
|
||||
guideStopCount: summaries[hall.id]?.guideStopCount,
|
||||
searchText: normalizeExplainKeyword([
|
||||
hall.name,
|
||||
hall.floorLabel,
|
||||
@@ -115,7 +123,8 @@ const loadExplainHalls = async () => {
|
||||
explainLoadPromise = (async () => {
|
||||
try {
|
||||
const halls = await explainUseCase.loadExplainHalls()
|
||||
explainHallItems.value = buildExplainHallItems(halls)
|
||||
const summaries = await explainUseCase.loadExplainHallSummaries(halls.map((hall) => hall.id))
|
||||
explainHallItems.value = buildExplainHallItems(halls, summaries)
|
||||
} catch (error) {
|
||||
console.error('加载讲解展厅失败:', error)
|
||||
explainError.value = '讲解展厅加载失败,请稍后重试'
|
||||
@@ -178,11 +187,12 @@ const handleExplainBusinessUnitClick = (unitId: string) => {
|
||||
}
|
||||
|
||||
const handleExplainGuideStopClick = (stopId: string) => {
|
||||
const target = normalizeExplainDetailTargetFromGuideStop({ id: stopId })
|
||||
const params = new URLSearchParams({
|
||||
id: stopId,
|
||||
id: target.targetId,
|
||||
tab: 'explain',
|
||||
targetType: 'STOP',
|
||||
targetId: stopId
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId
|
||||
})
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibit/detail?${params.toString()}`
|
||||
|
||||
@@ -250,6 +250,9 @@ import {
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
normalizeExplainDetailTargetFromGuideStop
|
||||
} from '@/domain/explainDetailTarget'
|
||||
import type {
|
||||
GuideRenderPoi
|
||||
} from '@/domain/guideModel'
|
||||
@@ -604,7 +607,8 @@ const loadExplainHalls = async () => {
|
||||
explainLoadPromise = (async () => {
|
||||
try {
|
||||
const halls = await explainUseCase.loadExplainHalls()
|
||||
explainHallItems.value = buildExplainHallItems(halls)
|
||||
const summaries = await explainUseCase.loadExplainHallSummaries(halls.map((hall) => hall.id))
|
||||
explainHallItems.value = buildExplainHallItems(halls, summaries)
|
||||
} catch (error) {
|
||||
console.error('加载讲解内容失败:', error)
|
||||
explainError.value = '讲解内容加载失败,请稍后重试'
|
||||
@@ -621,7 +625,8 @@ const loadExplainHalls = async () => {
|
||||
const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
|
||||
|
||||
const buildExplainHallItems = (
|
||||
halls: Awaited<ReturnType<typeof explainUseCase.loadExplainHalls>>
|
||||
halls: Awaited<ReturnType<typeof explainUseCase.loadExplainHalls>>,
|
||||
summaries: Awaited<ReturnType<typeof explainUseCase.loadExplainHallSummaries>> = {}
|
||||
): ExplainHallSelectItem[] => {
|
||||
return halls.map((hall) => {
|
||||
const keywordParts = [
|
||||
@@ -637,6 +642,8 @@ const buildExplainHallItems = (
|
||||
floorLabel: hall.floorLabel,
|
||||
image: hall.image,
|
||||
explainCount: hall.exhibitCount || 0,
|
||||
businessUnitCount: summaries[hall.id]?.businessUnitCount,
|
||||
guideStopCount: summaries[hall.id]?.guideStopCount,
|
||||
searchText: normalizeExplainKeyword(keywordParts.join(' '))
|
||||
}
|
||||
})
|
||||
@@ -1251,11 +1258,12 @@ const handleExplainBusinessUnitClick = (unitId: string) => {
|
||||
}
|
||||
|
||||
const handleExplainGuideStopClick = (stopId: string) => {
|
||||
const target = normalizeExplainDetailTargetFromGuideStop({ id: stopId })
|
||||
const params = new URLSearchParams({
|
||||
id: stopId,
|
||||
id: target.targetId,
|
||||
tab: 'explain',
|
||||
targetType: 'STOP',
|
||||
targetId: stopId
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId
|
||||
})
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibit/detail?${params.toString()}`
|
||||
|
||||
@@ -131,9 +131,16 @@ const requestJson = <T>(url: string): Promise<T> => new Promise((resolve, reject
|
||||
})
|
||||
})
|
||||
|
||||
let guideAudioApiUnavailable = false
|
||||
const unavailableAudioApiEndpoints = new Set<'stopInfo' | 'playInfo' | 'textInfo'>()
|
||||
|
||||
const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '')
|
||||
const normalizeBaseUrl = (baseUrl: string) => {
|
||||
const trimmed = baseUrl.trim().replace(/\/+$/, '')
|
||||
if (!trimmed || /^https?:\/\//i.test(trimmed) || trimmed.startsWith('/')) {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
return `/${trimmed}`
|
||||
}
|
||||
|
||||
const resolveAudioApiBaseUrl = () => {
|
||||
const baseUrl = normalizeBaseUrl(dataSourceConfig.audioApiBaseUrl)
|
||||
@@ -160,9 +167,12 @@ const isMissingRouteResponse = (response: { code: number; msg?: string }) => (
|
||||
response.code === 404 && /请求地址不存在/.test(response.msg || '')
|
||||
)
|
||||
|
||||
const markGuideAudioApiUnavailable = (response: { code: number; msg?: string }) => {
|
||||
const markGuideAudioApiUnavailable = (
|
||||
endpoint: 'stopInfo' | 'playInfo' | 'textInfo',
|
||||
response: { code: number; msg?: string }
|
||||
) => {
|
||||
if (isMissingRouteResponse(response)) {
|
||||
guideAudioApiUnavailable = true
|
||||
unavailableAudioApiEndpoints.add(endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +186,7 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
||||
private readonly textCache = new Map<string, AudioTextInfo>()
|
||||
|
||||
async getStopInfo(request: GuideStopInfoRequest): Promise<GuideStopInfo> {
|
||||
if (guideAudioApiUnavailable) {
|
||||
if (unavailableAudioApiEndpoints.has('stopInfo')) {
|
||||
throw new Error('讲解展示信息接口暂不可用')
|
||||
}
|
||||
|
||||
@@ -201,7 +211,7 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
||||
const response = await requestJson<GuideStopInfoResponse>(url)
|
||||
|
||||
if (response.code !== 0 || !response.data) {
|
||||
markGuideAudioApiUnavailable(response)
|
||||
markGuideAudioApiUnavailable('stopInfo', response)
|
||||
throw new Error(response.msg || '讲解点展示信息加载失败')
|
||||
}
|
||||
|
||||
@@ -217,7 +227,7 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
||||
lang: request.lang || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||
}
|
||||
const key = audioKey(normalizedRequest)
|
||||
if (guideAudioApiUnavailable) {
|
||||
if (unavailableAudioApiEndpoints.has('playInfo')) {
|
||||
return {
|
||||
playable: false,
|
||||
targetType: normalizedRequest.targetType,
|
||||
@@ -242,7 +252,7 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
||||
const response = await requestJson<AudioPlayInfoResponse>(url)
|
||||
|
||||
if (response.code !== 0 || !response.data) {
|
||||
markGuideAudioApiUnavailable(response)
|
||||
markGuideAudioApiUnavailable('playInfo', response)
|
||||
throw new Error(response.msg || '语音播放解析失败')
|
||||
}
|
||||
|
||||
@@ -262,7 +272,7 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
||||
lang: request.lang || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||
}
|
||||
const key = audioTextKey(normalizedRequest)
|
||||
if (guideAudioApiUnavailable) {
|
||||
if (unavailableAudioApiEndpoints.has('textInfo')) {
|
||||
return {
|
||||
available: false,
|
||||
targetType: normalizedRequest.targetType,
|
||||
@@ -287,7 +297,7 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
||||
const response = await requestJson<AudioTextInfoResponse>(url)
|
||||
|
||||
if (response.code !== 0 || !response.data) {
|
||||
markGuideAudioApiUnavailable(response)
|
||||
markGuideAudioApiUnavailable('textInfo', response)
|
||||
throw new Error(response.msg || '讲解词正文解析失败')
|
||||
}
|
||||
|
||||
@@ -303,7 +313,7 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
||||
this.stopInfoCache.clear()
|
||||
this.cache.clear()
|
||||
this.textCache.clear()
|
||||
guideAudioApiUnavailable = false
|
||||
unavailableAudioApiEndpoints.clear()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ import {
|
||||
import {
|
||||
dataSourceConfig
|
||||
} from '@/config/dataSource'
|
||||
import {
|
||||
normalizeExplainDetailTargetFromGuideStop
|
||||
} from '@/domain/explainDetailTarget'
|
||||
|
||||
export interface ExplainAudioSelection {
|
||||
exhibit: MuseumExhibit
|
||||
@@ -63,6 +66,12 @@ export interface ExhibitDetailAudioOptions {
|
||||
includeText?: boolean
|
||||
}
|
||||
|
||||
export interface ExplainHallSummary {
|
||||
hallId: string
|
||||
businessUnitCount: number
|
||||
guideStopCount: number
|
||||
}
|
||||
|
||||
export class ExplainUseCase {
|
||||
constructor(
|
||||
private readonly explain: ExplainRepository = explainRepository,
|
||||
@@ -355,14 +364,17 @@ export class ExplainUseCase {
|
||||
|
||||
async enterExplainDetail(request: ExplainDetailEntryRequest) {
|
||||
const entryTarget = await this.resolveDetailEntryTarget(request)
|
||||
const shouldLoadFallbackExhibit = entryTarget.targetType === 'ITEM'
|
||||
const [stopInfoResult, fallbackExhibit] = await Promise.all([
|
||||
this.audioPlayInfo.getStopInfo(entryTarget)
|
||||
.then((stopInfo) => ({ stopInfo, error: null }))
|
||||
.catch((error) => ({ stopInfo: null, error })),
|
||||
this.explain.getExhibitById(request.exhibitId)
|
||||
.catch(() => this.explain.listExplainExhibits()
|
||||
.then((items) => items.find((item) => item.id === request.exhibitId) || null)
|
||||
.catch(() => null))
|
||||
shouldLoadFallbackExhibit
|
||||
? this.explain.getExhibitById(request.exhibitId)
|
||||
.catch(() => this.explain.listExplainExhibits()
|
||||
.then((items) => items.find((item) => item.id === request.exhibitId) || null)
|
||||
.catch(() => null))
|
||||
: Promise.resolve(null)
|
||||
])
|
||||
|
||||
if (stopInfoResult.stopInfo) {
|
||||
@@ -419,6 +431,30 @@ export class ExplainUseCase {
|
||||
return this.listHalls()
|
||||
}
|
||||
|
||||
async loadExplainHallSummaries(hallIds: string[]): Promise<Record<string, ExplainHallSummary>> {
|
||||
const uniqueHallIds = Array.from(new Set(hallIds.map((id) => id.trim()).filter(Boolean)))
|
||||
const entries = await Promise.all(uniqueHallIds.map(async (hallId) => {
|
||||
try {
|
||||
const units = await this.loadTemporaryBusinessUnitsByHall(hallId)
|
||||
const guideStopCount = units.reduce((total, unit) => total + unit.guideStopCount, 0)
|
||||
return [hallId, {
|
||||
hallId,
|
||||
businessUnitCount: units.length,
|
||||
guideStopCount
|
||||
}] as const
|
||||
} catch (error) {
|
||||
console.warn('讲解展厅统计加载失败:', hallId, error)
|
||||
return [hallId, {
|
||||
hallId,
|
||||
businessUnitCount: 0,
|
||||
guideStopCount: 0
|
||||
}] as const
|
||||
}
|
||||
}))
|
||||
|
||||
return Object.fromEntries(entries)
|
||||
}
|
||||
|
||||
getHallById(id: string) {
|
||||
return this.explain.getHallById(id)
|
||||
}
|
||||
@@ -447,10 +483,11 @@ export class ExplainUseCase {
|
||||
}
|
||||
|
||||
openGuideStopDetail(stopId: string) {
|
||||
const target = normalizeExplainDetailTargetFromGuideStop({ id: stopId })
|
||||
return this.enterExplainDetail({
|
||||
exhibitId: stopId,
|
||||
targetType: 'STOP',
|
||||
targetId: stopId
|
||||
exhibitId: target.targetId,
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user