From 4b03cc281c4a3e8e92a551b08e63315945295b3d Mon Sep 17 00:00:00 2001 From: lyf <2514544224@qq.com> Date: Tue, 7 Jul 2026 01:31:06 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E8=AE=B2=E8=A7=A3=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E6=98=A0=E5=B0=84=E5=B9=B6=E4=BC=98=E5=85=88=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E6=8E=A5=E5=8F=A3=E5=B1=95=E5=8E=85=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/explain/ExplainHallSelect.vue | 5 +- .../adapters/backendExplainDataAdapter.ts | 25 +++- src/data/adapters/guideStopInfoAdapter.ts | 29 ++++- src/domain/museum.ts | 10 ++ src/pages/exhibit/detail.vue | 111 +++++++++++++++++- src/pages/explain/list.vue | 3 + src/pages/index/index.vue | 3 + src/usecases/explainUseCase.ts | 8 +- src/view-models/explainViewModels.ts | 22 +++- 9 files changed, 199 insertions(+), 17 deletions(-) diff --git a/src/components/explain/ExplainHallSelect.vue b/src/components/explain/ExplainHallSelect.vue index b3f0ad2..779aceb 100644 --- a/src/components/explain/ExplainHallSelect.vue +++ b/src/components/explain/ExplainHallSelect.vue @@ -160,6 +160,9 @@ export interface ExplainGuideStopSelectItem { hallId?: string hallName?: string floorId?: string + poiId?: string + mapX?: number + mapY?: number coverImageUrl?: string description?: string hasAudio?: boolean @@ -239,7 +242,7 @@ const emptyDescription = computed(() => { }) const hallIconUrl = (hall: ExplainHallSelectItem) => { - return hallIconMap[hall.name.trim()] || hall.image || '' + return hall.image || hallIconMap[hall.name.trim()] || '' } const hallIconText = (name: string) => name.trim().slice(0, 1) || '讲' diff --git a/src/data/adapters/backendExplainDataAdapter.ts b/src/data/adapters/backendExplainDataAdapter.ts index 0f1d5ba..2a9d000 100644 --- a/src/data/adapters/backendExplainDataAdapter.ts +++ b/src/data/adapters/backendExplainDataAdapter.ts @@ -83,6 +83,8 @@ export interface BackendGuideStop { id?: string | number | null name?: string | null floorId?: string | number | null + mapX?: number | string | null + mapY?: number | string | null targetType?: string | null targetId?: string | number | null coverImageUrl?: string | null @@ -128,6 +130,7 @@ export interface BackendCatalogLinkedExhibitItem { code?: string | null exhibitCode?: string | null coverImageUrl?: string | null + galleryUrls?: string | string[] | null sortOrder?: number | null } @@ -135,6 +138,10 @@ export interface BackendCatalogStopItem { stopId?: string | number | null id?: string | number | null outlineId?: string | number | null + poiId?: string | number | null + floorId?: string | number | null + mapX?: number | string | null + mapY?: number | string | null name?: string | null guideLevel?: string | null description?: string | null @@ -168,7 +175,7 @@ const firstText = (...values: Array) => ( .find(Boolean) || '' ) -const parseGalleryUrls = (value: BackendExhibit['galleryUrls']) => { +const parseGalleryUrls = (value: BackendExhibit['galleryUrls'] | string[]) => { if (!value) return [] if (Array.isArray(value)) return value.map(String).filter(Boolean) @@ -308,8 +315,11 @@ const toCatalogLinkedExhibitSummary = (source: BackendCatalogLinkedExhibitItem) id, name: firstText(source.name, source.exhibitCode, source.code, `展品 ${id}`), nameEn: firstText(source.nameEn) || undefined, + code: firstText(source.code, source.exhibitCode) || undefined, exhibitCode: firstText(source.exhibitCode, source.code) || undefined, - coverImageUrl: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || undefined + coverImageUrl: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || undefined, + galleryUrls: parseGalleryUrls(source.galleryUrls), + sortOrder: normalizeNumber(source.sortOrder) } } @@ -331,7 +341,7 @@ export const toCatalogGuideStop = ( name: firstText(source.name, `讲解点${id}`), hallId: hall?.id, hallName: hall?.name, - floorId: hall?.floorId, + floorId: stringifyId(source.floorId) || hall?.floorId, targetType: playTargetType, targetId: playTargetId, coverImageUrl: canUseStopImage @@ -342,6 +352,9 @@ export const toCatalogGuideStop = ( audioStatus: normalizeCatalogAudioStatus(source.audioStatus), supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages), hasTextRecord: source.hasTextRecord === true, + poiId: stringifyId(source.poiId) || undefined, + mapX: normalizeNumber(source.mapX), + mapY: normalizeNumber(source.mapY), outlineId: stringifyId(source.outlineId) || outline?.id, outlineName: outline?.name, sort: typeof source.sort === 'number' ? source.sort : undefined, @@ -374,6 +387,10 @@ export const toCatalogMuseumExhibitFromStop = ( image: stop.coverImageUrl || undefined, description: stop.description || linkedPrimary?.name || '该讲解点暂无简介。', tags: [stop.outlineName, linkedPrimary?.exhibitCode].filter(Boolean) as string[], + poiId: stop.poiId, + sourcePoiId: stop.poiId, + mapX: stop.mapX, + mapY: stop.mapY, guideTitle: stop.name, guideText: stop.description, audioAvailable: audioReady, @@ -611,6 +628,8 @@ export const toBackendGuideStop = (source: BackendGuideStop): ExplainGuideStop | description: firstText(source.description) || undefined, hasAudio: source.hasAudio === true, poiId: stringifyId(source.poiId) || undefined, + mapX: normalizeNumber(source.mapX), + mapY: normalizeNumber(source.mapY), outlineId: stringifyId(source.outlineId) || undefined, outlineName: firstText(source.outlineName) || undefined, sort: typeof source.sort === 'number' ? source.sort : undefined diff --git a/src/data/adapters/guideStopInfoAdapter.ts b/src/data/adapters/guideStopInfoAdapter.ts index 732598f..fac0114 100644 --- a/src/data/adapters/guideStopInfoAdapter.ts +++ b/src/data/adapters/guideStopInfoAdapter.ts @@ -9,8 +9,11 @@ export interface BackendGuideStopLinkedExhibit { id?: string | number | null name?: string | null nameEn?: string | null + code?: string | null exhibitCode?: string | null coverImageUrl?: string | null + galleryUrls?: string | string[] | null + sortOrder?: number | string | null } export interface BackendGuideStopInfo { @@ -19,6 +22,10 @@ export interface BackendGuideStopInfo { targetId?: string | number | null resolvedStopId?: string | number | null stopId?: string | number | null + poiId?: string | number | null + floorId?: string | number | null + mapX?: number | string | null + mapY?: number | string | null lang?: string | null title?: string | null description?: string | null @@ -74,8 +81,11 @@ export interface GuideStopLinkedExhibit { id: string name: string nameEn?: string + code?: string exhibitCode?: string coverImageUrl?: string + galleryUrls?: string[] + sortOrder?: number } export interface GuideStopInfo { @@ -83,6 +93,10 @@ export interface GuideStopInfo { targetType: AudioPlayTargetType targetId: string resolvedStopId?: string + poiId?: string + floorId?: string + mapX?: number + mapY?: number lang: 'zh-CN' | 'en-US' title: string description?: string @@ -156,7 +170,7 @@ const normalizeLanguage = (value: string | null | undefined): 'zh-CN' | 'en-US' value === 'en-US' ? 'en-US' : 'zh-CN' ) -const parseGalleryUrls = (value: BackendGuideStopInfo['galleryUrls']) => { +const parseGalleryUrls = (value: BackendGuideStopInfo['galleryUrls'] | BackendGuideStopLinkedExhibit['galleryUrls']) => { if (!value) return [] if (Array.isArray(value)) { return value.map((entry) => normalizeSameOriginPublicUrl(String(entry))).filter(Boolean) @@ -192,10 +206,13 @@ const normalizeLinkedExhibits = (items: BackendGuideStopLinkedExhibit[] | null | return { id, - name: item.name?.trim() || item.exhibitCode?.trim() || `展品 ${id}`, + name: item.name?.trim() || item.exhibitCode?.trim() || item.code?.trim() || `展品 ${id}`, nameEn: item.nameEn?.trim() || undefined, - exhibitCode: item.exhibitCode?.trim() || undefined, - coverImageUrl: normalizeSameOriginPublicUrl(item.coverImageUrl) || undefined + code: item.code?.trim() || item.exhibitCode?.trim() || undefined, + exhibitCode: item.exhibitCode?.trim() || item.code?.trim() || undefined, + coverImageUrl: normalizeSameOriginPublicUrl(item.coverImageUrl) || undefined, + galleryUrls: parseGalleryUrls(item.galleryUrls), + sortOrder: normalizeNumber(item.sortOrder) } }) .filter(Boolean) as GuideStopLinkedExhibit[] @@ -225,6 +242,10 @@ export const toGuideStopInfo = ( targetType, targetId, resolvedStopId: stringifyId(source.resolvedStopId) || stringifyId(source.stopId) || undefined, + poiId: stringifyId(source.poiId) || undefined, + floorId: stringifyId(source.floorId) || undefined, + mapX: normalizeNumber(source.mapX), + mapY: normalizeNumber(source.mapY), lang: normalizeLanguage(source.lang || fallback.lang), title: source.title?.trim() || '讲解内容', description: source.description?.trim() || undefined, diff --git a/src/domain/museum.ts b/src/domain/museum.ts index 35067a8..6b1b03e 100644 --- a/src/domain/museum.ts +++ b/src/domain/museum.ts @@ -152,6 +152,8 @@ export interface MuseumExhibit { description?: string poiId?: string sourcePoiId?: string + mapX?: number + mapY?: number location?: GuideLocationResolution artist?: string year?: string @@ -183,8 +185,11 @@ export interface MuseumExhibit { id: string name: string nameEn?: string + code?: string exhibitCode?: string coverImageUrl?: string + galleryUrls?: string[] + sortOrder?: number }> stopInfoAvailable?: boolean stopInfoReason?: string @@ -208,6 +213,8 @@ export interface ExplainGuideStop { supportedLanguages?: string[] hasTextRecord?: boolean poiId?: string + mapX?: number + mapY?: number outlineId?: string outlineName?: string sort?: number @@ -219,8 +226,11 @@ export interface ExplainGuideStop { id: string name: string nameEn?: string + code?: string exhibitCode?: string coverImageUrl?: string + galleryUrls?: string[] + sortOrder?: number }> playTargetType?: AudioPlayTargetType playTargetId?: string diff --git a/src/pages/exhibit/detail.vue b/src/pages/exhibit/detail.vue index aa8ee1e..7df3950 100644 --- a/src/pages/exhibit/detail.vue +++ b/src/pages/exhibit/detail.vue @@ -27,10 +27,13 @@ {{ exhibit.title }} {{ detailMeta }} - - + + {{ exhibit.linkedExhibitStatusText }} + + {{ exhibit.galleryStatusText }} + {{ textError }} + + + 关联展品 + + + + + 展品 + + + {{ linked.name }} + + {{ linked.exhibitCode || linked.code }} + + + 图集 {{ linked.galleryUrls.length }} 张 + + + + + @@ -168,6 +201,10 @@ const canExpandText = computed(() => ( && !textExpanded.value && !textLoading.value )) +const visibleLinkedExhibits = computed(() => ( + [...(exhibit.value.linkedExhibits || [])] + .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)) +)) // 讲解详情页只定位到所属展厅,不再追踪具体展品点位。 const resolveHallGuidePoi = async () => { @@ -641,6 +678,76 @@ const handleBack = () => { color: #8a4a31; } +.linked-exhibit-list { + margin-top: 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.linked-exhibit-item { + min-height: 72px; + padding: 10px; + display: flex; + align-items: center; + gap: 12px; + box-sizing: border-box; + background: #ffffff; + border: 1px solid #e1e5dc; + border-radius: 8px; +} + +.linked-exhibit-image { + flex-shrink: 0; + width: 52px; + height: 52px; + border-radius: 6px; + background: #eef1e8; +} + +.linked-exhibit-image.placeholder { + display: flex; + align-items: center; + justify-content: center; +} + +.linked-exhibit-placeholder, +.linked-exhibit-name, +.linked-exhibit-code, +.linked-exhibit-gallery { + display: block; +} + +.linked-exhibit-placeholder { + font-size: 12px; + line-height: 16px; + font-weight: 700; + color: #68725d; +} + +.linked-exhibit-main { + min-width: 0; + flex: 1; +} + +.linked-exhibit-name { + font-size: 15px; + line-height: 21px; + font-weight: 800; + color: #151713; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.linked-exhibit-code, +.linked-exhibit-gallery { + margin-top: 3px; + font-size: 12px; + line-height: 17px; + color: #666d61; +} + .action-bar { position: absolute; left: 0; diff --git a/src/pages/explain/list.vue b/src/pages/explain/list.vue index 9560e29..944e78e 100644 --- a/src/pages/explain/list.vue +++ b/src/pages/explain/list.vue @@ -107,6 +107,9 @@ const explainGuideStopItems = computed(() => ( hallId: stop.hallId, hallName: stop.hallName, floorId: stop.floorId, + poiId: stop.poiId, + mapX: stop.mapX, + mapY: stop.mapY, coverImageUrl: stop.coverImageUrl, description: stop.description, hasAudio: stop.hasAudio, diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index f9ce23c..70d202e 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -723,6 +723,9 @@ const explainGuideStopItems = computed(() => ( hallId: stop.hallId, hallName: stop.hallName, floorId: stop.floorId, + poiId: stop.poiId, + mapX: stop.mapX, + mapY: stop.mapY, coverImageUrl: stop.coverImageUrl, description: stop.description, hasAudio: stop.hasAudio, diff --git a/src/usecases/explainUseCase.ts b/src/usecases/explainUseCase.ts index 50e71a4..e2a97d0 100644 --- a/src/usecases/explainUseCase.ts +++ b/src/usecases/explainUseCase.ts @@ -135,12 +135,14 @@ export class ExplainUseCase { name: stopInfo.title || fallback?.name || linkedPrimary?.name || '讲解内容', hallId: fallback?.hallId, hallName: fallback?.hallName, - floorId: fallback?.floorId, + floorId: stopInfo.floorId || fallback?.floorId, floorLabel: fallback?.floorLabel, image: coverImage, description, - poiId: fallback?.poiId, - sourcePoiId: fallback?.sourcePoiId, + poiId: stopInfo.poiId || fallback?.poiId, + sourcePoiId: stopInfo.poiId || fallback?.sourcePoiId, + mapX: stopInfo.mapX ?? fallback?.mapX, + mapY: stopInfo.mapY ?? fallback?.mapY, location: fallback?.location, year: fallback?.year, material: fallback?.material, diff --git a/src/view-models/explainViewModels.ts b/src/view-models/explainViewModels.ts index 72a547b..944961f 100644 --- a/src/view-models/explainViewModels.ts +++ b/src/view-models/explainViewModels.ts @@ -46,7 +46,11 @@ export interface ExplainDetailPageViewModel { coverImages: string[] hallId?: string hallName?: string + floorId?: string floorLabel?: string + poiId?: string + mapX?: number + mapY?: number summary: string body: string audio: { @@ -61,6 +65,7 @@ export interface ExplainDetailPageViewModel { statusText?: string } imageStatus?: string + galleryStatusText?: string linkedExhibitCount?: number isSharedStop?: boolean linkedExhibitStatusText?: string @@ -68,8 +73,11 @@ export interface ExplainDetailPageViewModel { id: string name: string nameEn?: string + code?: string exhibitCode?: string coverImageUrl?: string + galleryUrls?: string[] + sortOrder?: number }> chapters: Array<{ id: string @@ -171,6 +179,10 @@ export const toExplainExhibitViewModel = (exhibit: MuseumExhibit): ExplainExhibi export const toExplainDetailPageViewModel = (exhibit: MuseumExhibit): ExplainDetailPageViewModel => { const hasPlayableAudio = isAudioReady(exhibit) + const coverImages = [ + exhibit.image || EXPLAIN_DETAIL_PLACEHOLDER_IMAGE, + ...(exhibit.galleryUrls || []) + ].filter(Boolean) const linkedExhibitCount = typeof exhibit.linkedExhibitCount === 'number' ? exhibit.linkedExhibitCount : exhibit.linkedExhibits?.length @@ -195,13 +207,14 @@ export const toExplainDetailPageViewModel = (exhibit: MuseumExhibit): ExplainDet title: exhibit.name, subtitle: buildSubtitle(exhibit), contentType: contentTypeFor(exhibit), - coverImages: [ - exhibit.image || EXPLAIN_DETAIL_PLACEHOLDER_IMAGE, - ...(exhibit.galleryUrls || []) - ].filter(Boolean), + coverImages, hallId: exhibit.hallId, hallName: exhibit.hallName, + floorId: exhibit.floorId, floorLabel: exhibit.floorLabel, + poiId: exhibit.poiId, + mapX: exhibit.mapX, + mapY: exhibit.mapY, summary: transcript || '该展项暂无讲解文稿。', body, audio: { @@ -218,6 +231,7 @@ export const toExplainDetailPageViewModel = (exhibit: MuseumExhibit): ExplainDet statusText: hasPlayableAudio ? '可播放' : '当前语言暂无语音讲解' }, imageStatus: exhibit.imageStatus, + galleryStatusText: coverImages.length > 1 ? `共 ${coverImages.length} 张讲解图片` : undefined, linkedExhibitCount, isSharedStop: exhibit.isSharedStop, linkedExhibitStatusText,