This commit is contained in:
@@ -3,10 +3,11 @@
|
|||||||
|
|
||||||
VITE_APP_PUBLIC_BASE=/
|
VITE_APP_PUBLIC_BASE=/
|
||||||
VITE_DATA_SOURCE_MODE=sdk
|
VITE_DATA_SOURCE_MODE=sdk
|
||||||
VITE_GUIDE_CONTENT_SOURCE_MODE=static
|
VITE_GUIDE_CONTENT_SOURCE_MODE=remote
|
||||||
|
VITE_EXPLAIN_ALLOW_STATIC_FALLBACK=false
|
||||||
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
|
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
|
||||||
VITE_API_BASE_URL=/app-api
|
VITE_API_BASE_URL=/app-api
|
||||||
VITE_AUDIO_API_BASE_URL=/nhm/audio
|
VITE_AUDIO_API_BASE_URL=/app-api
|
||||||
VITE_AUDIO_LANGUAGE=zh-CN
|
VITE_AUDIO_LANGUAGE=zh-CN
|
||||||
VITE_SGS_API_BASE_URL=/app-api
|
VITE_SGS_API_BASE_URL=/app-api
|
||||||
VITE_SGS_MAP_ID=1
|
VITE_SGS_MAP_ID=1
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
VITE_APP_PUBLIC_BASE=/
|
VITE_APP_PUBLIC_BASE=/
|
||||||
VITE_DATA_SOURCE_MODE=sdk
|
VITE_DATA_SOURCE_MODE=sdk
|
||||||
VITE_GUIDE_CONTENT_SOURCE_MODE=static
|
VITE_GUIDE_CONTENT_SOURCE_MODE=remote
|
||||||
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
|
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
|
||||||
VITE_API_BASE_URL=/app-api
|
VITE_API_BASE_URL=/app-api
|
||||||
VITE_AUDIO_API_BASE_URL=/yudao-server
|
VITE_AUDIO_API_BASE_URL=/app-api
|
||||||
VITE_AUDIO_LANGUAGE=zh-CN
|
VITE_AUDIO_LANGUAGE=zh-CN
|
||||||
VITE_SGS_API_BASE_URL=/app-api
|
VITE_SGS_API_BASE_URL=/app-api
|
||||||
VITE_SGS_MAP_ID=1
|
VITE_SGS_MAP_ID=1
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
VITE_APP_PUBLIC_BASE=/
|
VITE_APP_PUBLIC_BASE=/
|
||||||
VITE_DATA_SOURCE_MODE=sdk
|
VITE_DATA_SOURCE_MODE=sdk
|
||||||
VITE_GUIDE_CONTENT_SOURCE_MODE=static
|
VITE_GUIDE_CONTENT_SOURCE_MODE=remote
|
||||||
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
|
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
|
||||||
VITE_API_BASE_URL=/app-api
|
VITE_API_BASE_URL=/app-api
|
||||||
VITE_AUDIO_API_BASE_URL=/yudao-server
|
VITE_AUDIO_API_BASE_URL=/app-api
|
||||||
VITE_AUDIO_LANGUAGE=zh-CN
|
VITE_AUDIO_LANGUAGE=zh-CN
|
||||||
VITE_SGS_API_BASE_URL=/app-api
|
VITE_SGS_API_BASE_URL=/app-api
|
||||||
VITE_SGS_MAP_ID=1
|
VITE_SGS_MAP_ID=1
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
VITE_APP_PUBLIC_BASE=/
|
VITE_APP_PUBLIC_BASE=/
|
||||||
VITE_DATA_SOURCE_MODE=sdk
|
VITE_DATA_SOURCE_MODE=sdk
|
||||||
VITE_GUIDE_CONTENT_SOURCE_MODE=static
|
VITE_GUIDE_CONTENT_SOURCE_MODE=remote
|
||||||
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
|
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
|
||||||
VITE_API_BASE_URL=/app-api
|
VITE_API_BASE_URL=/app-api
|
||||||
VITE_AUDIO_API_BASE_URL=/yudao-server
|
VITE_AUDIO_API_BASE_URL=/app-api
|
||||||
VITE_AUDIO_LANGUAGE=zh-CN
|
VITE_AUDIO_LANGUAGE=zh-CN
|
||||||
VITE_SGS_API_BASE_URL=/app-api
|
VITE_SGS_API_BASE_URL=/app-api
|
||||||
VITE_SGS_MAP_ID=1
|
VITE_SGS_MAP_ID=1
|
||||||
|
|||||||
@@ -101,11 +101,16 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="hall-main">
|
<view class="hall-main">
|
||||||
<text class="hall-name">{{ stop.name }}</text>
|
<view class="stop-title-row">
|
||||||
|
<text class="hall-name">{{ stop.name }}</text>
|
||||||
|
<view v-if="stop.guideLevel" class="guide-level-badge">
|
||||||
|
<text class="guide-level-text">{{ stop.guideLevel }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<text v-if="stop.description" class="stop-desc">{{ stop.description }}</text>
|
<text v-if="stop.description" class="stop-desc">{{ stop.description }}</text>
|
||||||
<view class="hall-meta-row">
|
<view class="hall-meta-row">
|
||||||
<view class="floor-badge">
|
<view class="floor-badge">
|
||||||
<text class="floor-badge-text">{{ stop.hasAudio ? '可讲解' : '图文' }}</text>
|
<text class="floor-badge-text">{{ stop.audioStatus === 'READY' ? '可讲解' : '图文' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="hall-meta-text">{{ stop.floorId || selectedHallName || '楼层待补充' }}</text>
|
<text class="hall-meta-text">{{ stop.floorId || selectedHallName || '楼层待补充' }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -158,6 +163,8 @@ export interface ExplainGuideStopSelectItem {
|
|||||||
coverImageUrl?: string
|
coverImageUrl?: string
|
||||||
description?: string
|
description?: string
|
||||||
hasAudio?: boolean
|
hasAudio?: boolean
|
||||||
|
audioStatus?: string
|
||||||
|
guideLevel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExplainSelectStage = 'hall' | 'unit' | 'stop'
|
export type ExplainSelectStage = 'hall' | 'unit' | 'stop'
|
||||||
@@ -422,6 +429,7 @@ const handleBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hall-name {
|
.hall-name {
|
||||||
|
min-width: 0;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -431,6 +439,38 @@ const handleBack = () => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stop-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-level-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-width: 92px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 7px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #151713;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-level-text {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e0df00;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.hall-meta-row {
|
.hall-meta-row {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -62,20 +62,22 @@
|
|||||||
<view class="home-collapse-bar"></view>
|
<view class="home-collapse-bar"></view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="category-grid">
|
<view class="category-scroll">
|
||||||
<view
|
<view class="category-grid">
|
||||||
v-for="item in primaryFacilities"
|
<view
|
||||||
:key="item.id"
|
v-for="item in primaryFacilityColumns"
|
||||||
class="category-item"
|
:key="item.id"
|
||||||
:class="{ active: searchKeyword === item.label }"
|
class="category-item"
|
||||||
@tap="handleFacilityShortcut(item)"
|
:class="{ active: searchKeyword === item.label }"
|
||||||
>
|
@tap="handleFacilityShortcut(item)"
|
||||||
<view class="line-icon" :class="`icon-${item.icon}`">
|
>
|
||||||
<view class="icon-part a"></view>
|
<view class="line-icon" :class="`icon-${item.icon}`">
|
||||||
<view class="icon-part b"></view>
|
<view class="icon-part a"></view>
|
||||||
<view class="icon-part c"></view>
|
<view class="icon-part b"></view>
|
||||||
|
<view class="icon-part c"></view>
|
||||||
|
</view>
|
||||||
|
<text class="category-label">{{ item.label }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="category-label">{{ item.label }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -182,14 +184,36 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const primaryFacilities: ShortcutItem[] = [
|
const primaryFacilities: ShortcutItem[] = [
|
||||||
{ id: 'service', label: '服务台', icon: 'service', keywords: ['服务台', 'service_desk'] },
|
{ id: 'exhibition', label: '展览', icon: 'exhibition', keywords: ['展览', '展厅', '展馆', 'exhibition_hall', 'temp_exhibition'] },
|
||||||
|
{ id: 'cinema', label: '影院', icon: 'cinema', keywords: ['影院', '剧场', '影厅', 'cinema', 'theater'] },
|
||||||
|
{ id: 'restroom', label: '洗手间', icon: 'restroom', keywords: ['洗手间', '卫生间', 'toilet', 'accessible_toilet'] },
|
||||||
|
{ id: 'restaurant', label: '餐厅', icon: 'restaurant', keywords: ['餐厅', '餐饮', '咖啡', 'restaurant', 'commercial'] },
|
||||||
|
{ id: 'ticket-office', label: '售票处', icon: 'ticket', keywords: ['售票处', '售票', '票务', 'ticket_office'] },
|
||||||
|
{ id: 'nursing', label: '母婴室', icon: 'nursing', keywords: ['母婴室', 'mother_baby_room'] },
|
||||||
|
{ id: 'service-center', label: '服务中心', icon: 'service', keywords: ['服务中心', '服务台', '咨询台', 'service_desk', 'service_space'] },
|
||||||
{ id: 'elevator', label: '电梯', icon: 'elevator', keywords: ['电梯', 'elevator'] },
|
{ id: 'elevator', label: '电梯', icon: 'elevator', keywords: ['电梯', 'elevator'] },
|
||||||
{ id: 'escalator', label: '扶梯', icon: 'escalator', keywords: ['扶梯', 'escalator'] },
|
{ id: 'escalator', label: '扶梯', icon: 'escalator', keywords: ['扶梯', 'escalator'] },
|
||||||
{ id: 'restroom', label: '洗手间', icon: 'restroom', keywords: ['洗手间', '卫生间', 'toilet', 'accessible_toilet'] },
|
{ id: 'stairs', label: '楼梯', icon: 'stairs', keywords: ['楼梯', 'stairs', 'stair'] }
|
||||||
{ id: 'nursing', label: '母婴室', icon: 'nursing', keywords: ['母婴室', 'mother_baby_room'] },
|
|
||||||
{ id: 'entrance', label: '出入口', icon: 'entrance', keywords: ['出入口', '入口', '出口', 'entrance_exit'] }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const primaryFacilityVisibleColumnCount = 4
|
||||||
|
const primaryFacilityRowCount = 2
|
||||||
|
|
||||||
|
const primaryFacilityColumns = computed(() => {
|
||||||
|
const firstScreenItems = primaryFacilities.slice(0, primaryFacilityVisibleColumnCount * primaryFacilityRowCount)
|
||||||
|
const overflowItems = primaryFacilities.slice(primaryFacilityVisibleColumnCount * primaryFacilityRowCount)
|
||||||
|
const orderedItems: ShortcutItem[] = []
|
||||||
|
|
||||||
|
for (let columnIndex = 0; columnIndex < primaryFacilityVisibleColumnCount; columnIndex += 1) {
|
||||||
|
const topItem = firstScreenItems[columnIndex]
|
||||||
|
const bottomItem = firstScreenItems[columnIndex + primaryFacilityVisibleColumnCount]
|
||||||
|
if (topItem) orderedItems.push(topItem)
|
||||||
|
if (bottomItem) orderedItems.push(bottomItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderedItems.concat(overflowItems)
|
||||||
|
})
|
||||||
|
|
||||||
const serviceFacilities: ShortcutItem[] = [
|
const serviceFacilities: ShortcutItem[] = [
|
||||||
{ id: 'drink', label: '饮料售卖机', icon: 'drink', keywords: ['饮料售卖机', '饮料', '售卖机', 'vending'] },
|
{ id: 'drink', label: '饮料售卖机', icon: 'drink', keywords: ['饮料售卖机', '饮料', '售卖机', 'vending'] },
|
||||||
{ id: 'ticket', label: '票务售卖机', icon: 'ticket', keywords: ['票务售卖机', '票务', '售票', 'ticket_office'] },
|
{ id: 'ticket', label: '票务售卖机', icon: 'ticket', keywords: ['票务售卖机', '票务', '售票', 'ticket_office'] },
|
||||||
@@ -678,9 +702,11 @@ defineExpose({
|
|||||||
|
|
||||||
.poi-search-panel {
|
.poi-search-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: #262421;
|
color: #262421;
|
||||||
font-family: '鸿蒙黑体', 'HarmonyOS Sans SC', 'HarmonyOS Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: '鸿蒙黑体', 'HarmonyOS Sans SC', 'HarmonyOS Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.variant-home.expanded {
|
.variant-home.expanded {
|
||||||
@@ -758,6 +784,8 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.variant-home .search-box {
|
.variant-home .search-box {
|
||||||
|
background: #f5f5ed;
|
||||||
|
border-color: rgba(224, 225, 0, 0.72);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,6 +796,8 @@ defineExpose({
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
overscroll-behavior-x: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-category-strip::-webkit-scrollbar {
|
.home-category-strip::-webkit-scrollbar {
|
||||||
@@ -775,18 +805,50 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-category-chip {
|
.home-category-chip {
|
||||||
min-width: 70px;
|
min-width: 112px;
|
||||||
height: 42px;
|
height: 58px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 112px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
padding: 0 10px;
|
padding: 0 11px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: #f4f5ef;
|
background: #e0df00;
|
||||||
border: 1px solid #dde5df;
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(116deg, rgba(255, 255, 255, 0.22) 0 32%, rgba(255, 255, 255, 0) 33%),
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(0, 0, 0, 0.02));
|
||||||
|
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-category-chip:nth-child(2) {
|
||||||
|
background-color: #18c2b6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-category-chip:nth-child(3) {
|
||||||
|
background-color: #b99cf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-category-chip:nth-child(4) {
|
||||||
|
background-color: #8ad9ae;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-category-chip:nth-child(5) {
|
||||||
|
background-color: #ffc0a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-category-chip:nth-child(6) {
|
||||||
|
background-color: #5ed0e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-category-chip:nth-child(7) {
|
||||||
|
background-color: #c9a463;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-category-chip:nth-child(8) {
|
||||||
|
background-color: #f4f5ef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-category-chip:active {
|
.home-category-chip:active {
|
||||||
@@ -794,14 +856,16 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-category-icon {
|
.home-category-icon {
|
||||||
width: 20px;
|
width: 24px;
|
||||||
height: 19px;
|
height: 23px;
|
||||||
flex: 0 0 20px;
|
flex: 0 0 24px;
|
||||||
|
order: 2;
|
||||||
|
color: #050704;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-category-icon.line-icon {
|
.home-category-icon.line-icon {
|
||||||
width: 20px;
|
width: 24px;
|
||||||
height: 19px;
|
height: 23px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-category-icon .icon-part {
|
.home-category-icon .icon-part {
|
||||||
@@ -810,11 +874,12 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-category-label {
|
.home-category-label {
|
||||||
max-width: 58px;
|
min-width: 0;
|
||||||
font-size: 12px;
|
max-width: 70px;
|
||||||
line-height: 16px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
line-height: 18px;
|
||||||
color: #151713;
|
font-weight: 800;
|
||||||
|
color: #050704;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -919,6 +984,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.variant-page .poi-search-content {
|
.variant-page .poi-search-content {
|
||||||
|
margin-top: 14px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -959,30 +1025,63 @@ defineExpose({
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.category-scroll {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
overscroll-behavior-x: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
touch-action: pan-x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-scroll::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.category-grid {
|
.category-grid {
|
||||||
|
--category-gap: 8px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-auto-flow: column;
|
||||||
gap: 8px;
|
grid-template-rows: repeat(2, minmax(70px, auto));
|
||||||
|
grid-auto-columns: calc((100% - 32px) / 5);
|
||||||
|
gap: var(--category-gap);
|
||||||
|
width: calc(125% + 2px);
|
||||||
|
min-width: calc(125% + 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-item {
|
.category-item {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 70px;
|
min-height: 70px;
|
||||||
padding: 9px 6px;
|
position: relative;
|
||||||
|
padding: 9px 6px 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: rgba(255, 255, 255, 0.92);
|
overflow: hidden;
|
||||||
border: 1px solid #e2e4da;
|
background: #fffef2;
|
||||||
|
border: 1px solid #ecebd2;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-item.active {
|
.category-item.active {
|
||||||
background: #151713;
|
background: #fffddc;
|
||||||
border-color: #151713;
|
border-color: #d8d900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
right: 14px;
|
||||||
|
bottom: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--museum-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-icon {
|
.line-icon {
|
||||||
@@ -993,7 +1092,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.category-item.active .line-icon {
|
.category-item.active .line-icon {
|
||||||
color: var(--museum-accent);
|
color: #151713;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-part {
|
.icon-part {
|
||||||
@@ -1002,6 +1101,8 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-service .icon-part.a,
|
.icon-service .icon-part.a,
|
||||||
|
.icon-exhibition .icon-part.a,
|
||||||
|
.icon-cinema .icon-part.a,
|
||||||
.icon-ticket .icon-part.a,
|
.icon-ticket .icon-part.a,
|
||||||
.icon-drink .icon-part.a,
|
.icon-drink .icon-part.a,
|
||||||
.icon-bag .icon-part.a {
|
.icon-bag .icon-part.a {
|
||||||
@@ -1014,6 +1115,8 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-service .icon-part.b,
|
.icon-service .icon-part.b,
|
||||||
|
.icon-exhibition .icon-part.b,
|
||||||
|
.icon-cinema .icon-part.b,
|
||||||
.icon-ticket .icon-part.b,
|
.icon-ticket .icon-part.b,
|
||||||
.icon-drink .icon-part.b,
|
.icon-drink .icon-part.b,
|
||||||
.icon-bag .icon-part.b {
|
.icon-bag .icon-part.b {
|
||||||
@@ -1030,6 +1133,8 @@ defineExpose({
|
|||||||
.icon-escalator .icon-part.a,
|
.icon-escalator .icon-part.a,
|
||||||
.icon-restroom .icon-part.a,
|
.icon-restroom .icon-part.a,
|
||||||
.icon-nursing .icon-part.a,
|
.icon-nursing .icon-part.a,
|
||||||
|
.icon-restaurant .icon-part.a,
|
||||||
|
.icon-stairs .icon-part.a,
|
||||||
.icon-entrance .icon-part.a {
|
.icon-entrance .icon-part.a {
|
||||||
left: 4px;
|
left: 4px;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
@@ -1069,6 +1174,63 @@ defineExpose({
|
|||||||
box-shadow: 8px 0 0 currentColor;
|
box-shadow: 8px 0 0 currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-exhibition .icon-part.c {
|
||||||
|
left: 6px;
|
||||||
|
right: 6px;
|
||||||
|
bottom: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-cinema .icon-part.b {
|
||||||
|
left: 2px;
|
||||||
|
right: 2px;
|
||||||
|
top: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border: 0;
|
||||||
|
border-top: 2px solid currentColor;
|
||||||
|
border-bottom: 2px solid currentColor;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-restaurant .icon-part.b {
|
||||||
|
left: 7px;
|
||||||
|
top: 6px;
|
||||||
|
width: 2px;
|
||||||
|
height: 12px;
|
||||||
|
background: currentColor;
|
||||||
|
box-shadow: 5px 0 0 currentColor, 10px 0 0 currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-restaurant .icon-part.c {
|
||||||
|
right: 3px;
|
||||||
|
top: 5px;
|
||||||
|
width: 7px;
|
||||||
|
height: 15px;
|
||||||
|
border-left: 2px solid currentColor;
|
||||||
|
border-bottom: 2px solid currentColor;
|
||||||
|
border-radius: 0 0 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-stairs .icon-part.b {
|
||||||
|
left: 6px;
|
||||||
|
top: 7px;
|
||||||
|
width: 14px;
|
||||||
|
height: 12px;
|
||||||
|
border-left: 2px solid currentColor;
|
||||||
|
border-bottom: 2px solid currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-stairs .icon-part.c {
|
||||||
|
left: 10px;
|
||||||
|
top: 11px;
|
||||||
|
width: 10px;
|
||||||
|
height: 8px;
|
||||||
|
border-left: 2px solid currentColor;
|
||||||
|
border-bottom: 2px solid currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
.category-label {
|
.category-label {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -1082,12 +1244,24 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.category-item.active .category-label {
|
.category-item.active .category-label {
|
||||||
color: var(--museum-accent);
|
color: #151713;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title-row {
|
.section-title-row {
|
||||||
margin-top: 14px;
|
position: relative;
|
||||||
margin-bottom: 8px;
|
margin-top: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title-row::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 3px;
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--museum-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
@@ -1113,19 +1287,30 @@ defineExpose({
|
|||||||
.facility-chip {
|
.facility-chip {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: rgba(255, 255, 255, 0.94);
|
background: #fffef6;
|
||||||
border: 1px solid #e2e4da;
|
border: 1px solid #e8e7d2;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-chip.active {
|
.facility-chip.active {
|
||||||
background: #151713;
|
background: #fffddc;
|
||||||
border-color: #151713;
|
border-color: #d6d700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facility-chip.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
bottom: -1px;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--museum-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip-icon {
|
.chip-icon {
|
||||||
@@ -1140,7 +1325,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.facility-chip.active .chip-icon {
|
.facility-chip.active .chip-icon {
|
||||||
color: var(--museum-accent);
|
color: #151713;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-chip-text {
|
.facility-chip-text {
|
||||||
@@ -1152,26 +1337,27 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.facility-chip.active .facility-chip-text {
|
.facility-chip.active .facility-chip-text {
|
||||||
color: var(--museum-accent);
|
color: #151713;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-card {
|
.result-card {
|
||||||
margin-top: 12px;
|
margin-top: 14px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: rgba(255, 255, 255, 0.96);
|
background: #fffef7;
|
||||||
border: 1px solid #e2e4da;
|
border: 1px solid #ecebd8;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 24px rgba(75, 78, 60, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-card-header {
|
.result-card-header {
|
||||||
height: 42px;
|
height: 46px;
|
||||||
padding: 0 12px;
|
padding: 0 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-bottom: 1px solid #eceee6;
|
border-bottom: 1px solid #efeedf;
|
||||||
}
|
}
|
||||||
|
|
||||||
.museum-title,
|
.museum-title,
|
||||||
@@ -1196,14 +1382,15 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.floor-tabs {
|
.floor-tabs {
|
||||||
width: 58px;
|
width: 62px;
|
||||||
flex: 0 0 58px;
|
flex: 0 0 62px;
|
||||||
padding: 8px 0;
|
padding: 9px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: #f3f4ee;
|
background: #f6f5e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floor-tab {
|
.floor-tab {
|
||||||
|
position: relative;
|
||||||
height: 31px;
|
height: 31px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1216,6 +1403,16 @@ defineExpose({
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floor-tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 2px;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--museum-accent);
|
||||||
|
}
|
||||||
|
|
||||||
.floor-tab-text {
|
.floor-tab-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
@@ -1227,6 +1424,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.variant-page .category-grid,
|
.variant-page .category-grid,
|
||||||
|
.variant-page .category-scroll,
|
||||||
.variant-page .section-title-row,
|
.variant-page .section-title-row,
|
||||||
.variant-page .facility-chip-row {
|
.variant-page .facility-chip-row {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -1257,6 +1455,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.variant-home.expanded .category-grid,
|
.variant-home.expanded .category-grid,
|
||||||
|
.variant-home.expanded .category-scroll,
|
||||||
.variant-home.expanded .section-title-row,
|
.variant-home.expanded .section-title-row,
|
||||||
.variant-home.expanded .facility-chip-row {
|
.variant-home.expanded .facility-chip-row {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -1282,13 +1481,13 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.result-row {
|
.result-row {
|
||||||
min-height: 54px;
|
min-height: 58px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 9px;
|
gap: 9px;
|
||||||
padding: 9px 10px;
|
padding: 10px 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-bottom: 1px solid #edf0e8;
|
border-bottom: 1px solid #efefe1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-row:last-child {
|
.result-row:last-child {
|
||||||
@@ -1377,12 +1576,7 @@ defineExpose({
|
|||||||
|
|
||||||
@media (max-width: 360px) {
|
@media (max-width: 360px) {
|
||||||
.category-grid {
|
.category-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-rows: repeat(2, minmax(62px, auto));
|
||||||
}
|
|
||||||
|
|
||||||
.variant-page .category-grid,
|
|
||||||
.variant-home.expanded .category-grid {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.variant-page .category-item,
|
.variant-page .category-item,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const defaultSdkTimeoutMs = 5000
|
|||||||
const defaultApiBaseUrl = '/app-api'
|
const defaultApiBaseUrl = '/app-api'
|
||||||
const defaultSgsMapId = '1'
|
const defaultSgsMapId = '1'
|
||||||
const defaultGuideStaticDataBaseUrl = '/static/guide-data'
|
const defaultGuideStaticDataBaseUrl = '/static/guide-data'
|
||||||
const defaultAudioApiBaseUrl = '/yudao-server'
|
const defaultAudioApiBaseUrl = '/app-api'
|
||||||
|
|
||||||
const normalizeMode = (mode: string | undefined): DataSourceMode => {
|
const normalizeMode = (mode: string | undefined): DataSourceMode => {
|
||||||
if (mode && allowedModes.has(mode as DataSourceMode)) {
|
if (mode && allowedModes.has(mode as DataSourceMode)) {
|
||||||
@@ -24,9 +24,13 @@ const normalizeGuideContentMode = (mode: string | undefined): GuideContentDataSo
|
|||||||
return mode as GuideContentDataSourceMode
|
return mode as GuideContentDataSourceMode
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'static'
|
return 'remote'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeBoolean = (value: string | undefined) => (
|
||||||
|
['1', 'true', 'yes', 'on'].includes(value?.trim().toLowerCase() || '')
|
||||||
|
)
|
||||||
|
|
||||||
const normalizeUrl = (url: string | undefined, fallback: string) => {
|
const normalizeUrl = (url: string | undefined, fallback: string) => {
|
||||||
const normalized = url?.trim()
|
const normalized = url?.trim()
|
||||||
return normalized || fallback
|
return normalized || fallback
|
||||||
@@ -50,6 +54,8 @@ const inferOrigin = (url: string) => {
|
|||||||
export const dataSourceConfig = {
|
export const dataSourceConfig = {
|
||||||
mode: normalizeMode(import.meta.env.VITE_DATA_SOURCE_MODE),
|
mode: normalizeMode(import.meta.env.VITE_DATA_SOURCE_MODE),
|
||||||
guideContentMode: normalizeGuideContentMode(import.meta.env.VITE_GUIDE_CONTENT_SOURCE_MODE),
|
guideContentMode: normalizeGuideContentMode(import.meta.env.VITE_GUIDE_CONTENT_SOURCE_MODE),
|
||||||
|
allowExplainStaticFallback: import.meta.env.DEV
|
||||||
|
&& normalizeBoolean(import.meta.env.VITE_EXPLAIN_ALLOW_STATIC_FALLBACK),
|
||||||
guideStaticDataBaseUrl: normalizeUrl(
|
guideStaticDataBaseUrl: normalizeUrl(
|
||||||
import.meta.env.VITE_GUIDE_STATIC_DATA_BASE_URL,
|
import.meta.env.VITE_GUIDE_STATIC_DATA_BASE_URL,
|
||||||
defaultGuideStaticDataBaseUrl
|
defaultGuideStaticDataBaseUrl
|
||||||
|
|||||||
@@ -61,12 +61,22 @@ export interface BackendExhibit {
|
|||||||
export interface BackendHall {
|
export interface BackendHall {
|
||||||
id?: string | number | null
|
id?: string | number | null
|
||||||
name?: string | null
|
name?: string | null
|
||||||
|
poiId?: string | number | null
|
||||||
hallCode?: string | null
|
hallCode?: string | null
|
||||||
nameEn?: string | null
|
nameEn?: string | null
|
||||||
subtitle?: string | null
|
subtitle?: string | null
|
||||||
description?: string | null
|
description?: string | null
|
||||||
coverImageUrl?: string | null
|
coverImageUrl?: string | null
|
||||||
|
floorId?: string | number | null
|
||||||
|
floorCode?: string | null
|
||||||
exhibitCount?: number | null
|
exhibitCount?: number | null
|
||||||
|
outlineCount?: number | null
|
||||||
|
stopCount?: number | null
|
||||||
|
linkedExhibitCount?: number | null
|
||||||
|
audioReadyStopCount?: number | null
|
||||||
|
hasAudio?: boolean | null
|
||||||
|
audioStatus?: string | null
|
||||||
|
supportedLanguages?: string[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BackendGuideStop {
|
export interface BackendGuideStop {
|
||||||
@@ -87,6 +97,61 @@ export interface BackendGuideStop {
|
|||||||
sort?: number | null
|
sort?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BackendCatalogHallItem extends BackendHall {
|
||||||
|
mapId?: string | number | null
|
||||||
|
areaSqm?: string | number | null
|
||||||
|
sortOrder?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackendCatalogOutlineItem {
|
||||||
|
id?: string | number | null
|
||||||
|
parentId?: string | number | null
|
||||||
|
hallId?: string | number | null
|
||||||
|
name?: string | null
|
||||||
|
code?: string | null
|
||||||
|
description?: string | null
|
||||||
|
sort?: number | null
|
||||||
|
level?: number | null
|
||||||
|
children?: BackendCatalogOutlineItem[] | null
|
||||||
|
stopCount?: number | null
|
||||||
|
linkedExhibitCount?: number | null
|
||||||
|
audioReadyStopCount?: number | null
|
||||||
|
hasAudio?: boolean | null
|
||||||
|
audioStatus?: string | null
|
||||||
|
supportedLanguages?: string[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackendCatalogLinkedExhibitItem {
|
||||||
|
id?: string | number | null
|
||||||
|
name?: string | null
|
||||||
|
nameEn?: string | null
|
||||||
|
code?: string | null
|
||||||
|
exhibitCode?: string | null
|
||||||
|
coverImageUrl?: string | null
|
||||||
|
sortOrder?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackendCatalogStopItem {
|
||||||
|
stopId?: string | number | null
|
||||||
|
id?: string | number | null
|
||||||
|
outlineId?: string | number | null
|
||||||
|
name?: string | null
|
||||||
|
guideLevel?: string | null
|
||||||
|
description?: string | null
|
||||||
|
sort?: number | null
|
||||||
|
coverImageUrl?: string | null
|
||||||
|
imageStatus?: string | null
|
||||||
|
linkedExhibitCount?: number | null
|
||||||
|
isSharedStop?: boolean | null
|
||||||
|
linkedExhibits?: BackendCatalogLinkedExhibitItem[] | null
|
||||||
|
hasAudio?: boolean | null
|
||||||
|
audioStatus?: string | null
|
||||||
|
supportedLanguages?: string[] | null
|
||||||
|
hasTextRecord?: boolean | null
|
||||||
|
playTargetType?: string | null
|
||||||
|
playTargetId?: string | number | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface BackendExplainAdapterResult {
|
export interface BackendExplainAdapterResult {
|
||||||
exhibit: MuseumExhibit
|
exhibit: MuseumExhibit
|
||||||
track: ExplainTrack
|
track: ExplainTrack
|
||||||
@@ -130,6 +195,15 @@ const normalizeAudioTargetType = (targetType: string | null | undefined): AudioP
|
|||||||
return normalized === 'ITEM' || normalized === 'STOP' ? normalized : undefined
|
return normalized === 'ITEM' || normalized === 'STOP' ? normalized : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeNumber = (value: number | string | null | undefined) => {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
const numeric = Number(value)
|
||||||
|
return Number.isFinite(numeric) ? numeric : undefined
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const isAudioReady = (value?: string | null) => (
|
const isAudioReady = (value?: string | null) => (
|
||||||
!value || ['READY', 'PUBLISHED', 'AVAILABLE'].includes(value.toUpperCase())
|
!value || ['READY', 'PUBLISHED', 'AVAILABLE'].includes(value.toUpperCase())
|
||||||
)
|
)
|
||||||
@@ -170,6 +244,230 @@ const buildTags = (exhibit: BackendExhibit) => Array.from(new Set([
|
|||||||
firstText(exhibit.origin)
|
firstText(exhibit.origin)
|
||||||
].filter(Boolean)))
|
].filter(Boolean)))
|
||||||
|
|
||||||
|
const normalizeSupportedLanguages = (languages: string[] | null | undefined) => (
|
||||||
|
Array.isArray(languages) ? languages.map(String).filter(Boolean) : []
|
||||||
|
)
|
||||||
|
|
||||||
|
const normalizeCatalogAudioStatus = (status?: string | null) => (
|
||||||
|
firstText(status) || 'MISSING'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const toCatalogHall = (
|
||||||
|
source: BackendCatalogHallItem,
|
||||||
|
fallback?: MuseumHall | null
|
||||||
|
): MuseumHall => {
|
||||||
|
const id = stringifyId(source.id) || fallback?.id || firstText(source.hallCode, source.name)
|
||||||
|
const name = firstText(source.name, fallback?.name, source.hallCode, '展厅')
|
||||||
|
const area = firstText(source.areaSqm)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
floorId: stringifyId(source.floorId) || fallback?.floorId,
|
||||||
|
floorLabel: firstText(source.floorCode, fallback?.floorLabel) || undefined,
|
||||||
|
description: firstText(source.description, source.subtitle, fallback?.description, '该展厅暂无介绍。'),
|
||||||
|
image: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || fallback?.image || HALL_PLACEHOLDER_IMAGE,
|
||||||
|
exhibitCount: normalizeNumber(source.exhibitCount) ?? fallback?.exhibitCount ?? 0,
|
||||||
|
outlineCount: normalizeNumber(source.outlineCount),
|
||||||
|
stopCount: normalizeNumber(source.stopCount),
|
||||||
|
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount),
|
||||||
|
audioReadyStopCount: normalizeNumber(source.audioReadyStopCount),
|
||||||
|
hasAudio: source.hasAudio === true,
|
||||||
|
audioStatus: normalizeCatalogAudioStatus(source.audioStatus),
|
||||||
|
supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages),
|
||||||
|
area: area ? `${area}㎡` : fallback?.area,
|
||||||
|
poiId: stringifyId(source.poiId) || fallback?.poiId,
|
||||||
|
location: fallback?.location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toCatalogLinkedExhibit = (
|
||||||
|
source: BackendCatalogLinkedExhibitItem,
|
||||||
|
hall?: MuseumHall | null
|
||||||
|
): MuseumExhibit | null => {
|
||||||
|
const id = stringifyId(source.id)
|
||||||
|
if (!id) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: firstText(source.name, source.exhibitCode, source.code, `展品${id}`),
|
||||||
|
hallId: hall?.id,
|
||||||
|
hallName: hall?.name,
|
||||||
|
floorId: hall?.floorId,
|
||||||
|
floorLabel: hall?.floorLabel,
|
||||||
|
image: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || undefined,
|
||||||
|
size: firstText(source.exhibitCode, source.code) || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toCatalogLinkedExhibitSummary = (source: BackendCatalogLinkedExhibitItem) => {
|
||||||
|
const id = stringifyId(source.id)
|
||||||
|
if (!id) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: firstText(source.name, source.exhibitCode, source.code, `展品 ${id}`),
|
||||||
|
nameEn: firstText(source.nameEn) || undefined,
|
||||||
|
exhibitCode: firstText(source.exhibitCode, source.code) || undefined,
|
||||||
|
coverImageUrl: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toCatalogGuideStop = (
|
||||||
|
source: BackendCatalogStopItem,
|
||||||
|
hall?: MuseumHall | null,
|
||||||
|
outline?: Pick<ExplainBusinessUnit, 'id' | 'name'> | null
|
||||||
|
): ExplainGuideStop | null => {
|
||||||
|
const id = stringifyId(source.stopId) || stringifyId(source.id)
|
||||||
|
if (!id) return null
|
||||||
|
|
||||||
|
const playTargetType = normalizeAudioTargetType(source.playTargetType) || 'STOP'
|
||||||
|
const playTargetId = stringifyId(source.playTargetId) || id
|
||||||
|
const imageStatus = firstText(source.imageStatus) || 'MISSING'
|
||||||
|
const canUseStopImage = imageStatus === 'READY'
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: firstText(source.name, `讲解点${id}`),
|
||||||
|
hallId: hall?.id,
|
||||||
|
hallName: hall?.name,
|
||||||
|
floorId: hall?.floorId,
|
||||||
|
targetType: playTargetType,
|
||||||
|
targetId: playTargetId,
|
||||||
|
coverImageUrl: canUseStopImage
|
||||||
|
? normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || undefined
|
||||||
|
: undefined,
|
||||||
|
description: firstText(source.description) || undefined,
|
||||||
|
hasAudio: source.hasAudio === true,
|
||||||
|
audioStatus: normalizeCatalogAudioStatus(source.audioStatus),
|
||||||
|
supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages),
|
||||||
|
hasTextRecord: source.hasTextRecord === true,
|
||||||
|
outlineId: stringifyId(source.outlineId) || outline?.id,
|
||||||
|
outlineName: outline?.name,
|
||||||
|
sort: typeof source.sort === 'number' ? source.sort : undefined,
|
||||||
|
guideLevel: firstText(source.guideLevel) || undefined,
|
||||||
|
imageStatus,
|
||||||
|
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount),
|
||||||
|
isSharedStop: source.isSharedStop === true,
|
||||||
|
linkedExhibits: (source.linkedExhibits || [])
|
||||||
|
.map(toCatalogLinkedExhibitSummary)
|
||||||
|
.filter(Boolean) as NonNullable<ExplainGuideStop['linkedExhibits']>,
|
||||||
|
playTargetType,
|
||||||
|
playTargetId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toCatalogMuseumExhibitFromStop = (
|
||||||
|
stop: ExplainGuideStop,
|
||||||
|
hall?: MuseumHall | null
|
||||||
|
): MuseumExhibit => {
|
||||||
|
const linkedPrimary = stop.linkedExhibits?.[0]
|
||||||
|
const audioReady = stop.audioStatus === 'READY'
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: stop.id,
|
||||||
|
name: stop.name,
|
||||||
|
hallId: stop.hallId || hall?.id,
|
||||||
|
hallName: stop.hallName || hall?.name,
|
||||||
|
floorId: stop.floorId || hall?.floorId,
|
||||||
|
floorLabel: hall?.floorLabel,
|
||||||
|
image: stop.coverImageUrl || undefined,
|
||||||
|
description: stop.description || linkedPrimary?.name || '该讲解点暂无简介。',
|
||||||
|
tags: [stop.outlineName, linkedPrimary?.exhibitCode].filter(Boolean) as string[],
|
||||||
|
guideTitle: stop.name,
|
||||||
|
guideText: stop.description,
|
||||||
|
audioAvailable: audioReady,
|
||||||
|
audioStatus: stop.audioStatus,
|
||||||
|
supportedLanguages: stop.supportedLanguages,
|
||||||
|
audioHasText: stop.hasTextRecord,
|
||||||
|
imageStatus: stop.imageStatus,
|
||||||
|
linkedExhibitCount: stop.linkedExhibitCount,
|
||||||
|
isSharedStop: stop.isSharedStop,
|
||||||
|
linkedExhibits: stop.linkedExhibits,
|
||||||
|
stopInfoAvailable: true,
|
||||||
|
resolvedStopId: stop.id,
|
||||||
|
playTargetType: stop.playTargetType || stop.targetType || 'STOP',
|
||||||
|
playTargetId: stop.playTargetId || stop.targetId || stop.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const outlineKey = (outlineId?: string | null) => outlineId || '__unassigned__'
|
||||||
|
|
||||||
|
export const flattenCatalogOutlines = (
|
||||||
|
outlines: BackendCatalogOutlineItem[]
|
||||||
|
): BackendCatalogOutlineItem[] => {
|
||||||
|
const flattened: BackendCatalogOutlineItem[] = []
|
||||||
|
|
||||||
|
const visit = (items: BackendCatalogOutlineItem[]) => {
|
||||||
|
items
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
||||||
|
.forEach((item) => {
|
||||||
|
flattened.push(item)
|
||||||
|
if (Array.isArray(item.children) && item.children.length) {
|
||||||
|
visit(item.children)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(outlines)
|
||||||
|
return flattened
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toCatalogBusinessUnits = (
|
||||||
|
hallId: string,
|
||||||
|
outlines: BackendCatalogOutlineItem[],
|
||||||
|
stops: ExplainGuideStop[]
|
||||||
|
): ExplainBusinessUnit[] => {
|
||||||
|
const flatOutlines = flattenCatalogOutlines(outlines)
|
||||||
|
const stopMap = new Map<string, ExplainGuideStop[]>()
|
||||||
|
stops.forEach((stop) => {
|
||||||
|
const key = outlineKey(stop.outlineId)
|
||||||
|
stopMap.set(key, [...(stopMap.get(key) || []), stop])
|
||||||
|
})
|
||||||
|
|
||||||
|
const outlineChildren = new Set(
|
||||||
|
flatOutlines
|
||||||
|
.map((item) => stringifyId(item.parentId))
|
||||||
|
.filter((parentId) => parentId && parentId !== hallId)
|
||||||
|
)
|
||||||
|
|
||||||
|
const units = flatOutlines
|
||||||
|
.map<ExplainBusinessUnit | null>((outline) => {
|
||||||
|
const id = stringifyId(outline.id)
|
||||||
|
if (!id) return null
|
||||||
|
|
||||||
|
const directStops = (stopMap.get(id) || [])
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
||||||
|
const hasChildOutlines = outlineChildren.has(id)
|
||||||
|
const rawStopCount = normalizeNumber(outline.stopCount) ?? 0
|
||||||
|
if (!directStops.length && hasChildOutlines) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: firstText(outline.name, outline.code, `业务单元${id}`),
|
||||||
|
hallId: stringifyId(outline.hallId) || hallId,
|
||||||
|
parentId: stringifyId(outline.parentId) || undefined,
|
||||||
|
code: firstText(outline.code) || undefined,
|
||||||
|
description: firstText(outline.description) || undefined,
|
||||||
|
sort: typeof outline.sort === 'number' ? outline.sort : undefined,
|
||||||
|
level: typeof outline.level === 'number' ? outline.level : undefined,
|
||||||
|
guideStopCount: directStops.length || rawStopCount,
|
||||||
|
linkedExhibitCount: normalizeNumber(outline.linkedExhibitCount),
|
||||||
|
audioReadyStopCount: normalizeNumber(outline.audioReadyStopCount),
|
||||||
|
hasAudio: outline.hasAudio === true,
|
||||||
|
audioStatus: normalizeCatalogAudioStatus(outline.audioStatus),
|
||||||
|
supportedLanguages: normalizeSupportedLanguages(outline.supportedLanguages),
|
||||||
|
stops: directStops
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean) as ExplainBusinessUnit[]
|
||||||
|
|
||||||
|
if (units.length) return units
|
||||||
|
return groupGuideStopsByOutline(hallId, stops)
|
||||||
|
}
|
||||||
|
|
||||||
export const toBackendMuseumExhibit = (
|
export const toBackendMuseumExhibit = (
|
||||||
source: BackendExhibit,
|
source: BackendExhibit,
|
||||||
hall?: MuseumHall | null,
|
hall?: MuseumHall | null,
|
||||||
@@ -277,15 +575,22 @@ export const toBackendHallFromList = (source: BackendHall, fallback?: MuseumHall
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
floorId: fallback?.floorId,
|
floorId: stringifyId(source.floorId) || fallback?.floorId,
|
||||||
floorLabel: fallback?.floorLabel,
|
floorLabel: firstText(source.floorCode, fallback?.floorLabel) || undefined,
|
||||||
description: firstText(source.description, source.subtitle, fallback?.description, '该展厅暂无介绍。'),
|
description: firstText(source.description, source.subtitle, fallback?.description, '该展厅暂无介绍。'),
|
||||||
image: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || fallback?.image || HALL_PLACEHOLDER_IMAGE,
|
image: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || fallback?.image || HALL_PLACEHOLDER_IMAGE,
|
||||||
exhibitCount: typeof source.exhibitCount === 'number'
|
exhibitCount: typeof source.exhibitCount === 'number'
|
||||||
? source.exhibitCount
|
? source.exhibitCount
|
||||||
: fallback?.exhibitCount || 0,
|
: fallback?.exhibitCount || 0,
|
||||||
|
outlineCount: normalizeNumber(source.outlineCount),
|
||||||
|
stopCount: normalizeNumber(source.stopCount),
|
||||||
|
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount),
|
||||||
|
audioReadyStopCount: normalizeNumber(source.audioReadyStopCount),
|
||||||
|
hasAudio: source.hasAudio === true,
|
||||||
|
audioStatus: normalizeCatalogAudioStatus(source.audioStatus),
|
||||||
|
supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages),
|
||||||
area: fallback?.area,
|
area: fallback?.area,
|
||||||
poiId: fallback?.poiId,
|
poiId: stringifyId(source.poiId) || fallback?.poiId,
|
||||||
location: fallback?.location
|
location: fallback?.location
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface BackendGuideStopInfo {
|
|||||||
targetType?: string | null
|
targetType?: string | null
|
||||||
targetId?: string | number | null
|
targetId?: string | number | null
|
||||||
resolvedStopId?: string | number | null
|
resolvedStopId?: string | number | null
|
||||||
|
stopId?: string | number | null
|
||||||
lang?: string | null
|
lang?: string | null
|
||||||
title?: string | null
|
title?: string | null
|
||||||
description?: string | null
|
description?: string | null
|
||||||
@@ -33,6 +34,40 @@ export interface BackendGuideStopInfo {
|
|||||||
supportedLanguages?: string[] | null
|
supportedLanguages?: string[] | null
|
||||||
audioStatus?: string | null
|
audioStatus?: string | null
|
||||||
reason?: string | null
|
reason?: string | null
|
||||||
|
linkedExhibitCount?: number | string | null
|
||||||
|
isSharedStop?: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackendAudioPlayInfo {
|
||||||
|
playable?: boolean
|
||||||
|
targetType?: string | null
|
||||||
|
targetId?: string | number | null
|
||||||
|
lang?: string | null
|
||||||
|
narrationTier?: 'STANDARD' | 'EXTENDED' | string | null
|
||||||
|
audioId?: string | number | null
|
||||||
|
title?: string | null
|
||||||
|
duration?: number | null
|
||||||
|
format?: string | null
|
||||||
|
playUrl?: string | null
|
||||||
|
expiresAt?: string | null
|
||||||
|
subtitleUrl?: string | null
|
||||||
|
hasText?: boolean
|
||||||
|
fallback?: boolean
|
||||||
|
fallbackReason?: string | null
|
||||||
|
reason?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackendAudioTextInfo {
|
||||||
|
available?: boolean
|
||||||
|
targetType?: string | null
|
||||||
|
targetId?: string | number | null
|
||||||
|
lang?: string | null
|
||||||
|
narrationTier?: 'STANDARD' | 'EXTENDED' | string | null
|
||||||
|
title?: string | null
|
||||||
|
text?: string | null
|
||||||
|
textLength?: number | null
|
||||||
|
textHash?: string | null
|
||||||
|
reason?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GuideStopLinkedExhibit {
|
export interface GuideStopLinkedExhibit {
|
||||||
@@ -63,12 +98,55 @@ export interface GuideStopInfo {
|
|||||||
supportedLanguages: string[]
|
supportedLanguages: string[]
|
||||||
audioStatus: 'READY' | 'MISSING' | string
|
audioStatus: 'READY' | 'MISSING' | string
|
||||||
reason?: string
|
reason?: string
|
||||||
|
linkedExhibitCount?: number
|
||||||
|
isSharedStop?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GuideAudioPlayInfo {
|
||||||
|
playable: boolean
|
||||||
|
targetType: AudioPlayTargetType
|
||||||
|
targetId: string
|
||||||
|
lang: 'zh-CN' | 'en-US'
|
||||||
|
narrationTier?: 'STANDARD' | 'EXTENDED'
|
||||||
|
audioId?: string
|
||||||
|
title?: string
|
||||||
|
duration?: number
|
||||||
|
format?: string
|
||||||
|
playUrl?: string
|
||||||
|
expiresAt?: string
|
||||||
|
subtitleUrl?: string
|
||||||
|
hasText: boolean
|
||||||
|
fallback: boolean
|
||||||
|
fallbackReason?: string
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GuideAudioTextInfo {
|
||||||
|
available: boolean
|
||||||
|
targetType: AudioPlayTargetType
|
||||||
|
targetId: string
|
||||||
|
lang: 'zh-CN' | 'en-US'
|
||||||
|
narrationTier?: 'STANDARD' | 'EXTENDED'
|
||||||
|
title?: string
|
||||||
|
text?: string
|
||||||
|
textLength?: number
|
||||||
|
textHash?: string
|
||||||
|
reason?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringifyId = (value: string | number | null | undefined) => (
|
const stringifyId = (value: string | number | null | undefined) => (
|
||||||
value === null || typeof value === 'undefined' ? '' : String(value)
|
value === null || typeof value === 'undefined' ? '' : String(value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const normalizeNumber = (value: number | string | null | undefined) => {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
const numeric = Number(value)
|
||||||
|
return Number.isFinite(numeric) ? numeric : undefined
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeTargetType = (value: string | null | undefined, fallback: AudioPlayTargetType): AudioPlayTargetType => {
|
const normalizeTargetType = (value: string | null | undefined, fallback: AudioPlayTargetType): AudioPlayTargetType => {
|
||||||
const normalized = value?.toUpperCase()
|
const normalized = value?.toUpperCase()
|
||||||
return normalized === 'ITEM' || normalized === 'STOP' ? normalized : fallback
|
return normalized === 'ITEM' || normalized === 'STOP' ? normalized : fallback
|
||||||
@@ -95,13 +173,15 @@ const parseGalleryUrls = (value: BackendGuideStopInfo['galleryUrls']) => {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 兼容后端历史逗号拼接字段。
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return trimmed
|
return []
|
||||||
.split(',')
|
}
|
||||||
.map((entry) => normalizeSameOriginPublicUrl(entry.trim()))
|
|
||||||
.filter(Boolean)
|
const normalizeNarrationTier = (value: string | null | undefined) => {
|
||||||
|
const normalized = value?.toUpperCase()
|
||||||
|
return normalized === 'STANDARD' || normalized === 'EXTENDED' ? normalized : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeLinkedExhibits = (items: BackendGuideStopLinkedExhibit[] | null | undefined) => (
|
const normalizeLinkedExhibits = (items: BackendGuideStopLinkedExhibit[] | null | undefined) => (
|
||||||
@@ -144,7 +224,7 @@ export const toGuideStopInfo = (
|
|||||||
available: source.available === true,
|
available: source.available === true,
|
||||||
targetType,
|
targetType,
|
||||||
targetId,
|
targetId,
|
||||||
resolvedStopId: stringifyId(source.resolvedStopId) || undefined,
|
resolvedStopId: stringifyId(source.resolvedStopId) || stringifyId(source.stopId) || undefined,
|
||||||
lang: normalizeLanguage(source.lang || fallback.lang),
|
lang: normalizeLanguage(source.lang || fallback.lang),
|
||||||
title: source.title?.trim() || '讲解内容',
|
title: source.title?.trim() || '讲解内容',
|
||||||
description: source.description?.trim() || undefined,
|
description: source.description?.trim() || undefined,
|
||||||
@@ -159,6 +239,64 @@ export const toGuideStopInfo = (
|
|||||||
hasText: source.hasText === true,
|
hasText: source.hasText === true,
|
||||||
supportedLanguages: source.supportedLanguages || [],
|
supportedLanguages: source.supportedLanguages || [],
|
||||||
audioStatus: source.audioStatus || 'MISSING',
|
audioStatus: source.audioStatus || 'MISSING',
|
||||||
|
reason: source.reason || undefined,
|
||||||
|
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount),
|
||||||
|
isSharedStop: source.isSharedStop === true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toGuideAudioPlayInfo = (
|
||||||
|
source: BackendAudioPlayInfo,
|
||||||
|
fallback: {
|
||||||
|
targetType: AudioPlayTargetType
|
||||||
|
targetId: string
|
||||||
|
lang: 'zh-CN' | 'en-US'
|
||||||
|
}
|
||||||
|
): GuideAudioPlayInfo => {
|
||||||
|
const targetType = normalizeTargetType(source.targetType, fallback.targetType)
|
||||||
|
const targetId = stringifyId(source.targetId) || fallback.targetId
|
||||||
|
|
||||||
|
return {
|
||||||
|
playable: source.playable === true,
|
||||||
|
targetType,
|
||||||
|
targetId,
|
||||||
|
lang: normalizeLanguage(source.lang || fallback.lang),
|
||||||
|
narrationTier: normalizeNarrationTier(source.narrationTier),
|
||||||
|
audioId: stringifyId(source.audioId) || undefined,
|
||||||
|
title: source.title?.trim() || undefined,
|
||||||
|
duration: typeof source.duration === 'number' && Number.isFinite(source.duration) ? source.duration : undefined,
|
||||||
|
format: source.format?.trim() || undefined,
|
||||||
|
playUrl: normalizeSameOriginPublicUrl(source.playUrl) || undefined,
|
||||||
|
expiresAt: source.expiresAt || undefined,
|
||||||
|
subtitleUrl: normalizeSameOriginPublicUrl(source.subtitleUrl) || undefined,
|
||||||
|
hasText: source.hasText === true,
|
||||||
|
fallback: source.fallback === true,
|
||||||
|
fallbackReason: source.fallbackReason || undefined,
|
||||||
|
reason: source.reason || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toGuideAudioTextInfo = (
|
||||||
|
source: BackendAudioTextInfo,
|
||||||
|
fallback: {
|
||||||
|
targetType: AudioPlayTargetType
|
||||||
|
targetId: string
|
||||||
|
lang: 'zh-CN' | 'en-US'
|
||||||
|
}
|
||||||
|
): GuideAudioTextInfo => {
|
||||||
|
const targetType = normalizeTargetType(source.targetType, fallback.targetType)
|
||||||
|
const targetId = stringifyId(source.targetId) || fallback.targetId
|
||||||
|
|
||||||
|
return {
|
||||||
|
available: source.available === true,
|
||||||
|
targetType,
|
||||||
|
targetId,
|
||||||
|
lang: normalizeLanguage(source.lang || fallback.lang),
|
||||||
|
narrationTier: normalizeNarrationTier(source.narrationTier),
|
||||||
|
title: source.title?.trim() || undefined,
|
||||||
|
text: source.text || undefined,
|
||||||
|
textLength: typeof source.textLength === 'number' && Number.isFinite(source.textLength) ? source.textLength : undefined,
|
||||||
|
textHash: source.textHash || undefined,
|
||||||
reason: source.reason || undefined
|
reason: source.reason || undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,21 +11,15 @@ import type {
|
|||||||
ExplainContentProvider
|
ExplainContentProvider
|
||||||
} from '@/data/providers/staticMuseumContentProvider'
|
} from '@/data/providers/staticMuseumContentProvider'
|
||||||
import {
|
import {
|
||||||
defaultSgsSdkApiProvider,
|
toCatalogBusinessUnits,
|
||||||
type SgsSdkApiProvider
|
toCatalogGuideStop,
|
||||||
} from '@/data/providers/sgsSdkApiProvider'
|
toCatalogHall,
|
||||||
import {
|
toCatalogMuseumExhibitFromStop,
|
||||||
toBackendExplainTrack,
|
flattenCatalogOutlines,
|
||||||
toBackendHallFromList,
|
type BackendCatalogHallItem,
|
||||||
toBackendMediaAsset,
|
type BackendCatalogOutlineItem,
|
||||||
toBackendMuseumExhibit,
|
type BackendCatalogStopItem
|
||||||
groupGuideStopsByOutline,
|
|
||||||
type BackendExhibit,
|
|
||||||
type BackendHall
|
|
||||||
} from '@/data/adapters/backendExplainDataAdapter'
|
} from '@/data/adapters/backendExplainDataAdapter'
|
||||||
import {
|
|
||||||
toExplainGuideStopFromSgs
|
|
||||||
} from '@/data/adapters/sgsSdkGuideAdapter'
|
|
||||||
|
|
||||||
interface CommonResult<T> {
|
interface CommonResult<T> {
|
||||||
code: number
|
code: number
|
||||||
@@ -79,234 +73,414 @@ const requestJson = <T>(url: string): Promise<T> => new Promise((resolve, reject
|
|||||||
|
|
||||||
const normalizeKeyword = (keyword = '') => keyword.trim()
|
const normalizeKeyword = (keyword = '') => keyword.trim()
|
||||||
|
|
||||||
|
const catalogLang = () => (
|
||||||
|
dataSourceConfig.audioLanguage === 'en-US' ? 'en-US' : 'zh-CN'
|
||||||
|
)
|
||||||
|
|
||||||
|
const cacheKey = (...parts: string[]) => [catalogLang(), ...parts].join(':')
|
||||||
|
|
||||||
|
const requireArrayData = <T>(response: CommonResult<T[]>, message: string) => {
|
||||||
|
if (response.code !== 0) {
|
||||||
|
throw new Error(response.msg || message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(response.data) ? response.data : []
|
||||||
|
}
|
||||||
|
|
||||||
export class BackendExplainContentProvider implements ExplainContentProvider {
|
export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||||
private readonly searchCache = new Map<string, MuseumExhibit[]>()
|
private readonly explainExhibitCache = new Map<string, MuseumExhibit[]>()
|
||||||
private readonly searchInflight = new Map<string, Promise<MuseumExhibit[]>>()
|
private readonly explainExhibitInflight = new Map<string, Promise<MuseumExhibit[]>>()
|
||||||
private readonly detailCache = new Map<string, MuseumExhibit>()
|
private readonly detailCache = new Map<string, MuseumExhibit>()
|
||||||
private readonly detailInflight = new Map<string, Promise<MuseumExhibit | null>>()
|
private readonly hallListCache = new Map<string, MuseumHall[]>()
|
||||||
private hallListCache: MuseumHall[] | null = null
|
private readonly hallListInflight = new Map<string, Promise<MuseumHall[]>>()
|
||||||
private hallListInflight: Promise<MuseumHall[]> | null = null
|
|
||||||
private readonly guideStopCache = new Map<string, ExplainGuideStop[]>()
|
private readonly guideStopCache = new Map<string, ExplainGuideStop[]>()
|
||||||
private readonly guideStopInflight = new Map<string, Promise<ExplainGuideStop[]>>()
|
private readonly guideStopInflight = new Map<string, Promise<ExplainGuideStop[]>>()
|
||||||
|
private readonly outlineCache = new Map<string, BackendCatalogOutlineItem[]>()
|
||||||
|
private readonly outlineInflight = new Map<string, Promise<BackendCatalogOutlineItem[]>>()
|
||||||
|
private readonly businessUnitCache = new Map<string, ExplainBusinessUnit[]>()
|
||||||
|
private readonly businessUnitInflight = new Map<string, Promise<ExplainBusinessUnit[]>>()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly fallbackProvider: ExplainContentProvider,
|
private readonly fallbackProvider?: ExplainContentProvider
|
||||||
private readonly sgsSdkApiProvider: SgsSdkApiProvider = defaultSgsSdkApiProvider
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async requestStaticExplainExhibits() {
|
|
||||||
const cached = this.searchCache.get('')
|
|
||||||
if (cached) return cached
|
|
||||||
|
|
||||||
const exhibits = await this.fallbackProvider.listExplainExhibits()
|
|
||||||
this.searchCache.set('', exhibits)
|
|
||||||
return exhibits
|
|
||||||
}
|
|
||||||
|
|
||||||
private async searchStaticExplainExhibits(keyword = '') {
|
|
||||||
const normalizedKeyword = normalizeKeyword(keyword).toLowerCase()
|
|
||||||
const exhibits = await this.requestStaticExplainExhibits()
|
|
||||||
if (!normalizedKeyword) return exhibits
|
|
||||||
|
|
||||||
return exhibits.filter((exhibit) => [
|
|
||||||
exhibit.name,
|
|
||||||
exhibit.hallName,
|
|
||||||
exhibit.floorLabel,
|
|
||||||
exhibit.description,
|
|
||||||
...(exhibit.tags || [])
|
|
||||||
].filter(Boolean).join(' ').toLowerCase().includes(normalizedKeyword))
|
|
||||||
}
|
|
||||||
|
|
||||||
private async requestSearch(keyword = '') {
|
|
||||||
const normalizedKeyword = normalizeKeyword(keyword)
|
|
||||||
if (!normalizedKeyword) return this.requestStaticExplainExhibits()
|
|
||||||
|
|
||||||
const cacheKey = normalizedKeyword.toLowerCase()
|
|
||||||
const cached = this.searchCache.get(cacheKey)
|
|
||||||
if (cached) return cached
|
|
||||||
|
|
||||||
const inflight = this.searchInflight.get(cacheKey)
|
|
||||||
if (inflight) return inflight
|
|
||||||
|
|
||||||
const promise = (async () => {
|
|
||||||
const params = new URLSearchParams({ keyword: normalizedKeyword })
|
|
||||||
const url = `${resolveAppApiBaseUrl()}/gis/exhibit/search?${params.toString()}`
|
|
||||||
const response = await requestJson<CommonResult<BackendExhibit[]>>(url)
|
|
||||||
if (response.code !== 0 || !Array.isArray(response.data)) {
|
|
||||||
throw new Error(response.msg || '后端讲解搜索失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackHalls = await this.safeFallbackHalls()
|
|
||||||
const fallbackHallById = new Map(fallbackHalls.map((hall) => [hall.id, hall]))
|
|
||||||
const exhibits = response.data.map((item) => toBackendMuseumExhibit(
|
|
||||||
item,
|
|
||||||
fallbackHallById.get(String(item.hallId || '')),
|
|
||||||
{ includeDetail: false }
|
|
||||||
))
|
|
||||||
|
|
||||||
this.searchCache.set(cacheKey, exhibits)
|
|
||||||
return exhibits
|
|
||||||
})()
|
|
||||||
|
|
||||||
this.searchInflight.set(cacheKey, promise)
|
|
||||||
try {
|
|
||||||
return await promise
|
|
||||||
} finally {
|
|
||||||
this.searchInflight.delete(cacheKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async requestDetail(id: string) {
|
|
||||||
const cached = this.detailCache.get(id)
|
|
||||||
if (cached) return cached
|
|
||||||
|
|
||||||
const inflight = this.detailInflight.get(id)
|
|
||||||
if (inflight) return inflight
|
|
||||||
|
|
||||||
const promise = (async () => {
|
|
||||||
const params = new URLSearchParams({ id })
|
|
||||||
const url = `${resolveAppApiBaseUrl()}/gis/exhibit/get?${params.toString()}`
|
|
||||||
const response = await requestJson<CommonResult<BackendExhibit>>(url)
|
|
||||||
if (response.code !== 0 || !response.data) {
|
|
||||||
throw new Error(response.msg || '后端讲解详情失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackHalls = await this.safeFallbackHalls()
|
|
||||||
const fallbackHall = fallbackHalls.find((hall) => hall.id === String(response.data?.hallId || ''))
|
|
||||||
const detail = toBackendMuseumExhibit(response.data, fallbackHall, { includeDetail: true })
|
|
||||||
this.detailCache.set(id, detail)
|
|
||||||
return detail
|
|
||||||
})().catch(async (error) => {
|
|
||||||
console.warn('后端讲解详情加载失败,将使用静态讲解兜底:', error)
|
|
||||||
return this.fallbackProvider.listExhibits()
|
|
||||||
.then((items) => items.find((item) => item.id === id) || null)
|
|
||||||
.catch(() => null)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.detailInflight.set(id, promise)
|
|
||||||
try {
|
|
||||||
return await promise
|
|
||||||
} finally {
|
|
||||||
this.detailInflight.delete(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async safeFallbackHalls() {
|
private async safeFallbackHalls() {
|
||||||
return this.fallbackProvider.listHalls().catch(() => [])
|
if (!this.isStaticFallbackEnabled()) return []
|
||||||
|
|
||||||
|
return this.fallbackProvider?.listHalls().catch(() => []) || []
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requestHallList() {
|
private isStaticFallbackEnabled() {
|
||||||
if (this.hallListCache) return this.hallListCache
|
return Boolean(this.fallbackProvider && dataSourceConfig.allowExplainStaticFallback)
|
||||||
if (this.hallListInflight) return this.hallListInflight
|
}
|
||||||
|
|
||||||
this.hallListInflight = (async () => {
|
private async fallbackOrThrow<T>(
|
||||||
const url = `${resolveAppApiBaseUrl()}/gis/hall/list`
|
error: unknown,
|
||||||
const response = await requestJson<CommonResult<BackendHall[]>>(url)
|
message: string,
|
||||||
if (response.code !== 0 || !Array.isArray(response.data)) {
|
fallback: () => Promise<T>
|
||||||
throw new Error(response.msg || '后端展厅列表加载失败')
|
): Promise<T> {
|
||||||
}
|
if (this.isStaticFallbackEnabled()) {
|
||||||
|
console.warn(`${message},将使用显式静态讲解兜底:`, error)
|
||||||
|
return fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(message, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestCatalogHalls() {
|
||||||
|
const key = cacheKey('halls')
|
||||||
|
const cached = this.hallListCache.get(key)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const inflight = this.hallListInflight.get(key)
|
||||||
|
if (inflight) return inflight
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const params = new URLSearchParams({ lang: catalogLang() })
|
||||||
|
const url = `${resolveAppApiBaseUrl()}/gis/guide/catalog/halls?${params.toString()}`
|
||||||
|
const response = await requestJson<CommonResult<BackendCatalogHallItem[]>>(url)
|
||||||
|
const items = requireArrayData(response, '讲解展厅目录加载失败')
|
||||||
const fallbackHalls = await this.safeFallbackHalls()
|
const fallbackHalls = await this.safeFallbackHalls()
|
||||||
const fallbackHallById = new Map(fallbackHalls.map((hall) => [hall.id, hall]))
|
const fallbackHallById = new Map(fallbackHalls.map((hall) => [hall.id, hall]))
|
||||||
const fallbackHallByName = new Map(fallbackHalls.map((hall) => [hall.name, hall]))
|
const fallbackHallByName = new Map(fallbackHalls.map((hall) => [hall.name, hall]))
|
||||||
const halls = response.data
|
const halls = items
|
||||||
.map((item) => toBackendHallFromList(
|
.map((item) => toCatalogHall(
|
||||||
item,
|
item,
|
||||||
fallbackHallById.get(String(item.id || '')) || fallbackHallByName.get(String(item.name || ''))
|
fallbackHallById.get(String(item.id || '')) || fallbackHallByName.get(String(item.name || ''))
|
||||||
))
|
))
|
||||||
.filter((hall) => hall.id)
|
.filter((hall) => hall.id)
|
||||||
|
|
||||||
this.hallListCache = halls
|
this.hallListCache.set(key, halls)
|
||||||
return halls
|
return halls
|
||||||
})()
|
})()
|
||||||
|
|
||||||
try {
|
this.hallListInflight.set(key, promise)
|
||||||
return await this.hallListInflight
|
|
||||||
} finally {
|
|
||||||
this.hallListInflight = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async listGuideStopsByHall(hallId: string) {
|
|
||||||
const normalizedHallId = hallId.trim()
|
|
||||||
if (!normalizedHallId) return []
|
|
||||||
|
|
||||||
const cached = this.guideStopCache.get(normalizedHallId)
|
|
||||||
if (cached) return cached
|
|
||||||
|
|
||||||
const inflight = this.guideStopInflight.get(normalizedHallId)
|
|
||||||
if (inflight) return inflight
|
|
||||||
|
|
||||||
const promise = (async () => {
|
|
||||||
const stops = (await this.sgsSdkApiProvider.getGuideStopsByHall(normalizedHallId))
|
|
||||||
.map(toExplainGuideStopFromSgs)
|
|
||||||
.filter(Boolean) as ExplainGuideStop[]
|
|
||||||
|
|
||||||
this.guideStopCache.set(normalizedHallId, stops)
|
|
||||||
return stops
|
|
||||||
})()
|
|
||||||
|
|
||||||
this.guideStopInflight.set(normalizedHallId, promise)
|
|
||||||
try {
|
try {
|
||||||
return await promise
|
return await promise
|
||||||
} finally {
|
} finally {
|
||||||
this.guideStopInflight.delete(normalizedHallId)
|
this.hallListInflight.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestCatalogOutlinesByHall(hallId: string) {
|
||||||
|
const normalizedHallId = hallId.trim()
|
||||||
|
if (!normalizedHallId) return []
|
||||||
|
|
||||||
|
const key = cacheKey('hall', normalizedHallId, 'outlines')
|
||||||
|
const cached = this.outlineCache.get(key)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const inflight = this.outlineInflight.get(key)
|
||||||
|
if (inflight) return inflight
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
includeChildren: 'true',
|
||||||
|
lang: catalogLang()
|
||||||
|
})
|
||||||
|
const url = `${resolveAppApiBaseUrl()}/gis/guide/catalog/halls/${encodeURIComponent(normalizedHallId)}/outlines?${params.toString()}`
|
||||||
|
const response = await requestJson<CommonResult<BackendCatalogOutlineItem[]>>(url)
|
||||||
|
const outlines = requireArrayData(response, '讲解单元目录加载失败')
|
||||||
|
this.outlineCache.set(key, outlines)
|
||||||
|
return outlines
|
||||||
|
})()
|
||||||
|
|
||||||
|
this.outlineInflight.set(key, promise)
|
||||||
|
try {
|
||||||
|
return await promise
|
||||||
|
} finally {
|
||||||
|
this.outlineInflight.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestCatalogStopsByHall(hallId: string) {
|
||||||
|
const normalizedHallId = hallId.trim()
|
||||||
|
if (!normalizedHallId) return []
|
||||||
|
|
||||||
|
const key = cacheKey('hall', normalizedHallId, 'stops')
|
||||||
|
const cached = this.guideStopCache.get(key)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const inflight = this.guideStopInflight.get(key)
|
||||||
|
if (inflight) return inflight
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const [halls, outlines] = await Promise.all([
|
||||||
|
this.requestCatalogHalls(),
|
||||||
|
this.requestCatalogOutlinesByHall(normalizedHallId).catch(() => [])
|
||||||
|
])
|
||||||
|
const hall = halls.find((item) => item.id === normalizedHallId) || null
|
||||||
|
const outlineById = new Map(flattenCatalogOutlines(outlines).map((outline) => [String(outline.id || ''), outline]))
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ lang: catalogLang() })
|
||||||
|
const url = `${resolveAppApiBaseUrl()}/gis/guide/catalog/halls/${encodeURIComponent(normalizedHallId)}/stops?${params.toString()}`
|
||||||
|
const response = await requestJson<CommonResult<BackendCatalogStopItem[]>>(url)
|
||||||
|
const stops = requireArrayData(response, '讲解点目录加载失败')
|
||||||
|
.map((item) => {
|
||||||
|
const outline = outlineById.get(String(item.outlineId || ''))
|
||||||
|
return toCatalogGuideStop(item, hall, outline
|
||||||
|
? {
|
||||||
|
id: String(outline.id || ''),
|
||||||
|
name: String(outline.name || '')
|
||||||
|
}
|
||||||
|
: null)
|
||||||
|
})
|
||||||
|
.filter(Boolean) as ExplainGuideStop[]
|
||||||
|
|
||||||
|
this.guideStopCache.set(key, stops)
|
||||||
|
return stops
|
||||||
|
})()
|
||||||
|
|
||||||
|
this.guideStopInflight.set(key, promise)
|
||||||
|
try {
|
||||||
|
return await promise
|
||||||
|
} finally {
|
||||||
|
this.guideStopInflight.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestCatalogStopsByOutline(hallId: string, outlineId: string) {
|
||||||
|
const normalizedHallId = hallId.trim()
|
||||||
|
const normalizedOutlineId = outlineId.trim()
|
||||||
|
if (!normalizedOutlineId) return []
|
||||||
|
|
||||||
|
const hallStopsKey = cacheKey('hall', normalizedHallId, 'stops')
|
||||||
|
const cachedHallStops = this.guideStopCache.get(hallStopsKey)
|
||||||
|
if (cachedHallStops) {
|
||||||
|
return cachedHallStops.filter((stop) => stop.outlineId === normalizedOutlineId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = cacheKey('outline', normalizedOutlineId, 'stops')
|
||||||
|
const cached = this.guideStopCache.get(key)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const inflight = this.guideStopInflight.get(key)
|
||||||
|
if (inflight) return inflight
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const [halls, outlines] = await Promise.all([
|
||||||
|
this.requestCatalogHalls(),
|
||||||
|
normalizedHallId ? this.requestCatalogOutlinesByHall(normalizedHallId).catch(() => []) : Promise.resolve([])
|
||||||
|
])
|
||||||
|
const outline = flattenCatalogOutlines(outlines).find((item) => String(item.id || '') === normalizedOutlineId)
|
||||||
|
const hall = halls.find((item) => item.id === (String(outline?.hallId || '') || normalizedHallId)) || null
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ lang: catalogLang() })
|
||||||
|
const url = `${resolveAppApiBaseUrl()}/gis/guide/catalog/outlines/${encodeURIComponent(normalizedOutlineId)}/stops?${params.toString()}`
|
||||||
|
const response = await requestJson<CommonResult<BackendCatalogStopItem[]>>(url)
|
||||||
|
const stops = requireArrayData(response, '单元讲解点目录加载失败')
|
||||||
|
.map((item) => toCatalogGuideStop(item, hall, outline
|
||||||
|
? {
|
||||||
|
id: String(outline.id || ''),
|
||||||
|
name: String(outline.name || '')
|
||||||
|
}
|
||||||
|
: null))
|
||||||
|
.filter(Boolean) as ExplainGuideStop[]
|
||||||
|
|
||||||
|
this.guideStopCache.set(key, stops)
|
||||||
|
return stops
|
||||||
|
})()
|
||||||
|
|
||||||
|
this.guideStopInflight.set(key, promise)
|
||||||
|
try {
|
||||||
|
return await promise
|
||||||
|
} finally {
|
||||||
|
this.guideStopInflight.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestCatalogExhibits() {
|
||||||
|
const key = cacheKey('exhibits')
|
||||||
|
const cached = this.explainExhibitCache.get(key)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const inflight = this.explainExhibitInflight.get(key)
|
||||||
|
if (inflight) return inflight
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const halls = await this.requestCatalogHalls()
|
||||||
|
const stopsByHall = await Promise.all(halls.map(async (hall) => {
|
||||||
|
const stops = await this.requestCatalogStopsByHall(hall.id)
|
||||||
|
return stops.map((stop) => toCatalogMuseumExhibitFromStop(stop, hall))
|
||||||
|
}))
|
||||||
|
const exhibits = stopsByHall.flat()
|
||||||
|
this.explainExhibitCache.set(key, exhibits)
|
||||||
|
exhibits.forEach((exhibit) => {
|
||||||
|
this.detailCache.set(exhibit.id, exhibit)
|
||||||
|
})
|
||||||
|
return exhibits
|
||||||
|
})()
|
||||||
|
|
||||||
|
this.explainExhibitInflight.set(key, promise)
|
||||||
|
try {
|
||||||
|
return await promise
|
||||||
|
} finally {
|
||||||
|
this.explainExhibitInflight.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async searchCatalogExhibits(keyword = '') {
|
||||||
|
const normalizedKeyword = normalizeKeyword(keyword).toLowerCase()
|
||||||
|
const exhibits = await this.requestCatalogExhibits()
|
||||||
|
if (!normalizedKeyword) return exhibits
|
||||||
|
|
||||||
|
return exhibits.filter((exhibit) => [
|
||||||
|
exhibit.id,
|
||||||
|
exhibit.name,
|
||||||
|
exhibit.hallName,
|
||||||
|
exhibit.floorLabel,
|
||||||
|
exhibit.description,
|
||||||
|
exhibit.playTargetId,
|
||||||
|
...(exhibit.tags || []),
|
||||||
|
...(exhibit.linkedExhibits || []).flatMap((item) => [item.id, item.name, item.exhibitCode])
|
||||||
|
].filter(Boolean).join(' ').toLowerCase().includes(normalizedKeyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
async listGuideStopsByHall(hallId: string) {
|
||||||
|
try {
|
||||||
|
return await this.requestCatalogStopsByHall(hallId)
|
||||||
|
} catch (error) {
|
||||||
|
return this.fallbackOrThrow(error, '讲解点目录加载失败', async () => (
|
||||||
|
this.fallbackProvider?.listGuideStopsByHall?.(hallId) || []
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listTemporaryBusinessUnitsByHall(hallId: string): Promise<ExplainBusinessUnit[]> {
|
async listTemporaryBusinessUnitsByHall(hallId: string): Promise<ExplainBusinessUnit[]> {
|
||||||
const stops = await this.listGuideStopsByHall(hallId)
|
const normalizedHallId = hallId.trim()
|
||||||
return groupGuideStopsByOutline(hallId, stops)
|
if (!normalizedHallId) return []
|
||||||
|
|
||||||
|
const key = cacheKey('hall', normalizedHallId, 'units')
|
||||||
|
const cached = this.businessUnitCache.get(key)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const inflight = this.businessUnitInflight.get(key)
|
||||||
|
if (inflight) return inflight
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const [outlines, stops] = await Promise.all([
|
||||||
|
this.requestCatalogOutlinesByHall(normalizedHallId),
|
||||||
|
this.requestCatalogStopsByHall(normalizedHallId)
|
||||||
|
])
|
||||||
|
const units = toCatalogBusinessUnits(normalizedHallId, outlines, stops)
|
||||||
|
|
||||||
|
await Promise.all(units.map(async (unit) => {
|
||||||
|
if (unit.stops.length) return
|
||||||
|
unit.stops = await this.requestCatalogStopsByOutline(normalizedHallId, unit.id).catch(() => [])
|
||||||
|
unit.guideStopCount = unit.stops.length || unit.guideStopCount
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.businessUnitCache.set(key, units)
|
||||||
|
return units
|
||||||
|
})().catch((error) => {
|
||||||
|
return this.fallbackOrThrow(error, '讲解单元目录加载失败', async () => (
|
||||||
|
this.fallbackProvider?.listTemporaryBusinessUnitsByHall?.(normalizedHallId) || []
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.businessUnitInflight.set(key, promise)
|
||||||
|
try {
|
||||||
|
return await promise
|
||||||
|
} finally {
|
||||||
|
this.businessUnitInflight.delete(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listExplainExhibits() {
|
async listExplainExhibits() {
|
||||||
return this.requestStaticExplainExhibits()
|
try {
|
||||||
|
return await this.requestCatalogExhibits()
|
||||||
|
} catch (error) {
|
||||||
|
return this.fallbackOrThrow(error, '讲解目录展项加载失败', async () => (
|
||||||
|
this.fallbackProvider?.listExplainExhibits() || []
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listExhibits() {
|
listExhibits() {
|
||||||
return this.listExplainExhibits()
|
return this.listExplainExhibits()
|
||||||
}
|
}
|
||||||
|
|
||||||
getExhibitById(id: string) {
|
async getExhibitById(id: string) {
|
||||||
return this.requestDetail(id)
|
const normalizedId = id.trim()
|
||||||
|
if (!normalizedId) return null
|
||||||
|
|
||||||
|
const cached = this.detailCache.get(normalizedId)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const catalogExhibits = await this.listExplainExhibits().catch(() => [])
|
||||||
|
const matched = catalogExhibits.find((exhibit) => (
|
||||||
|
exhibit.id === normalizedId
|
||||||
|
|| exhibit.playTargetId === normalizedId
|
||||||
|
|| exhibit.resolvedStopId === normalizedId
|
||||||
|
|| exhibit.linkedExhibits?.some((linked) => linked.id === normalizedId)
|
||||||
|
))
|
||||||
|
if (matched) {
|
||||||
|
this.detailCache.set(normalizedId, matched)
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isStaticFallbackEnabled()) return null
|
||||||
|
|
||||||
|
return this.fallbackProvider?.getExhibitById?.(normalizedId)
|
||||||
|
|| this.fallbackProvider?.listExhibits()
|
||||||
|
.then((items) => items.find((item) => item.id === normalizedId) || null)
|
||||||
|
.catch(() => null)
|
||||||
|
|| null
|
||||||
}
|
}
|
||||||
|
|
||||||
async listHalls() {
|
async listHalls() {
|
||||||
try {
|
try {
|
||||||
return await this.requestHallList()
|
return await this.requestCatalogHalls()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('后端展厅列表加载失败,将使用静态展厅兜底:', error)
|
return this.fallbackOrThrow(error, '讲解展厅目录加载失败', async () => (
|
||||||
return this.fallbackProvider.listHalls()
|
this.fallbackProvider?.listHalls() || []
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listTracks() {
|
async listTracks() {
|
||||||
const exhibits = await this.listExplainExhibits()
|
const exhibits = await this.listExplainExhibits()
|
||||||
return exhibits.map(toBackendExplainTrack)
|
return exhibits.map((exhibit) => ({
|
||||||
|
id: `track-${exhibit.playTargetId || exhibit.id}`,
|
||||||
|
exhibitId: exhibit.id,
|
||||||
|
hallId: exhibit.hallId,
|
||||||
|
title: exhibit.guideTitle || `${exhibit.name}讲解`,
|
||||||
|
summary: exhibit.guideText || exhibit.description,
|
||||||
|
coverImage: exhibit.image,
|
||||||
|
poiId: exhibit.poiId,
|
||||||
|
floorId: exhibit.floorId,
|
||||||
|
available: exhibit.audioStatus === 'READY',
|
||||||
|
playTargetType: exhibit.playTargetType,
|
||||||
|
playTargetId: exhibit.playTargetId
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMediaById(id: string) {
|
async getMediaById() {
|
||||||
const normalizedId = id.replace(/^media-/, '')
|
return null
|
||||||
const cachedDetail = Array.from(this.detailCache.values()).find((exhibit) => (
|
|
||||||
exhibit.id === normalizedId || exhibit.guideContentId === normalizedId
|
|
||||||
))
|
|
||||||
if (cachedDetail) return toBackendMediaAsset(cachedDetail)
|
|
||||||
|
|
||||||
const detail = await this.requestDetail(normalizedId).catch(() => null)
|
|
||||||
return detail ? toBackendMediaAsset(detail) : null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMediaForExplainTrack(trackId: string) {
|
async getMediaForExplainTrack() {
|
||||||
return this.getMediaById(`media-${trackId.replace(/^track-/, '')}`)
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
searchExplainExhibits(keyword = '') {
|
searchExplainExhibits(keyword = '') {
|
||||||
const normalizedKeyword = normalizeKeyword(keyword)
|
const normalizedKeyword = normalizeKeyword(keyword)
|
||||||
if (!normalizedKeyword) return this.requestStaticExplainExhibits()
|
if (!normalizedKeyword) return this.listExplainExhibits()
|
||||||
|
|
||||||
return this.requestSearch(normalizedKeyword).catch((error) => {
|
return this.searchCatalogExhibits(normalizedKeyword).catch((error) => {
|
||||||
console.warn('后端讲解搜索失败,将使用静态讲解兜底:', error)
|
return this.fallbackOrThrow(error, '讲解目录搜索失败', async () => (
|
||||||
return this.searchStaticExplainExhibits(normalizedKeyword)
|
this.fallbackProvider?.searchExplainExhibits?.(normalizedKeyword)
|
||||||
|
|| this.fallbackProvider?.listExplainExhibits()
|
||||||
|
.then((items) => items.filter((exhibit) => [
|
||||||
|
exhibit.name,
|
||||||
|
exhibit.hallName,
|
||||||
|
exhibit.floorLabel,
|
||||||
|
exhibit.description,
|
||||||
|
...(exhibit.tags || [])
|
||||||
|
].filter(Boolean).join(' ').toLowerCase().includes(normalizedKeyword.toLowerCase())))
|
||||||
|
|| []
|
||||||
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,9 @@ export const createMuseumContentProvider = (): ExplainContentProvider => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isGuideContentRemoteMode()) {
|
if (isGuideContentRemoteMode()) {
|
||||||
return new BackendExplainContentProvider(new StaticGuideContentProvider())
|
return new BackendExplainContentProvider(
|
||||||
|
dataSourceConfig.allowExplainStaticFallback ? new StaticGuideContentProvider() : undefined
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGuideContentMockMode()) {
|
if (isGuideContentMockMode()) {
|
||||||
@@ -278,4 +280,5 @@ export const createMuseumContentProvider = (): ExplainContentProvider => {
|
|||||||
return new StaticGuideContentProvider()
|
return new StaticGuideContentProvider()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const staticMuseumContentProvider = createMuseumContentProvider()
|
export const explainContentProvider = createMuseumContentProvider()
|
||||||
|
export const staticMuseumContentProvider = explainContentProvider
|
||||||
|
|||||||
@@ -129,6 +129,13 @@ export interface MuseumHall {
|
|||||||
description?: string
|
description?: string
|
||||||
image?: string
|
image?: string
|
||||||
exhibitCount?: number
|
exhibitCount?: number
|
||||||
|
outlineCount?: number
|
||||||
|
stopCount?: number
|
||||||
|
linkedExhibitCount?: number
|
||||||
|
audioReadyStopCount?: number
|
||||||
|
hasAudio?: boolean
|
||||||
|
audioStatus?: string
|
||||||
|
supportedLanguages?: string[]
|
||||||
area?: string
|
area?: string
|
||||||
poiId?: string
|
poiId?: string
|
||||||
location?: GuideLocationResolution
|
location?: GuideLocationResolution
|
||||||
@@ -170,6 +177,8 @@ export interface MuseumExhibit {
|
|||||||
imageStatus?: string
|
imageStatus?: string
|
||||||
imageSource?: string
|
imageSource?: string
|
||||||
galleryUrls?: string[]
|
galleryUrls?: string[]
|
||||||
|
linkedExhibitCount?: number
|
||||||
|
isSharedStop?: boolean
|
||||||
linkedExhibits?: Array<{
|
linkedExhibits?: Array<{
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -195,18 +204,44 @@ export interface ExplainGuideStop {
|
|||||||
coverImageUrl?: string
|
coverImageUrl?: string
|
||||||
description?: string
|
description?: string
|
||||||
hasAudio?: boolean
|
hasAudio?: boolean
|
||||||
|
audioStatus?: string
|
||||||
|
supportedLanguages?: string[]
|
||||||
|
hasTextRecord?: boolean
|
||||||
poiId?: string
|
poiId?: string
|
||||||
outlineId?: string
|
outlineId?: string
|
||||||
outlineName?: string
|
outlineName?: string
|
||||||
sort?: number
|
sort?: number
|
||||||
|
guideLevel?: string
|
||||||
|
imageStatus?: string
|
||||||
|
linkedExhibitCount?: number
|
||||||
|
isSharedStop?: boolean
|
||||||
|
linkedExhibits?: Array<{
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
nameEn?: string
|
||||||
|
exhibitCode?: string
|
||||||
|
coverImageUrl?: string
|
||||||
|
}>
|
||||||
|
playTargetType?: AudioPlayTargetType
|
||||||
|
playTargetId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExplainBusinessUnit {
|
export interface ExplainBusinessUnit {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
hallId: string
|
hallId: string
|
||||||
|
parentId?: string
|
||||||
|
code?: string
|
||||||
|
description?: string
|
||||||
|
sort?: number
|
||||||
|
level?: number
|
||||||
previewImageUrl?: string
|
previewImageUrl?: string
|
||||||
guideStopCount: number
|
guideStopCount: number
|
||||||
|
linkedExhibitCount?: number
|
||||||
|
audioReadyStopCount?: number
|
||||||
|
hasAudio?: boolean
|
||||||
|
audioStatus?: string
|
||||||
|
supportedLanguages?: string[]
|
||||||
stops: ExplainGuideStop[]
|
stops: ExplainGuideStop[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,14 @@
|
|||||||
<text class="hero-placeholder-text">讲解</text>
|
<text class="hero-placeholder-text">讲解</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="detail-body">
|
<view class="detail-body">
|
||||||
<text class="detail-title">{{ exhibit.title }}</text>
|
<text class="detail-title">{{ exhibit.title }}</text>
|
||||||
<text v-if="detailMeta" class="detail-meta">{{ detailMeta }}</text>
|
<text v-if="detailMeta" class="detail-meta">{{ detailMeta }}</text>
|
||||||
|
<view v-if="exhibit.linkedExhibitStatusText" class="detail-chip-row">
|
||||||
|
<view class="detail-chip" :class="{ shared: exhibit.isSharedStop }">
|
||||||
|
<text class="detail-chip-text">{{ exhibit.linkedExhibitStatusText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
v-if="exhibit.audio.status === 'playable'"
|
v-if="exhibit.audio.status === 'playable'"
|
||||||
@@ -328,6 +333,8 @@ const handleAudioError = async (_audio: AudioItem | null, message: string) => {
|
|||||||
audioLanguage: exhibit.value.audio.language,
|
audioLanguage: exhibit.value.audio.language,
|
||||||
playTargetType: exhibit.value.audio.playTargetType,
|
playTargetType: exhibit.value.audio.playTargetType,
|
||||||
playTargetId: exhibit.value.audio.playTargetId
|
playTargetId: exhibit.value.audio.playTargetId
|
||||||
|
}, {
|
||||||
|
refreshPlayInfo: true
|
||||||
})
|
})
|
||||||
|
|
||||||
if (selection.playable && selection.media?.url) {
|
if (selection.playable && selection.media?.url) {
|
||||||
@@ -491,6 +498,46 @@ const handleBack = () => {
|
|||||||
color: #555c51;
|
color: #555c51;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-chip-row {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-chip {
|
||||||
|
max-width: 100%;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #f4f6ef;
|
||||||
|
border: 1px solid #dde2d3;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-chip.shared {
|
||||||
|
background: #151713;
|
||||||
|
border-color: #151713;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-chip-text {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #555c51;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-chip.shared .detail-chip-text {
|
||||||
|
color: #e0df00;
|
||||||
|
}
|
||||||
|
|
||||||
.audio-panel {
|
.audio-panel {
|
||||||
height: 76px;
|
height: 76px;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ const explainGuideStopItems = computed<ExplainGuideStopSelectItem[]>(() => (
|
|||||||
floorId: stop.floorId,
|
floorId: stop.floorId,
|
||||||
coverImageUrl: stop.coverImageUrl,
|
coverImageUrl: stop.coverImageUrl,
|
||||||
description: stop.description,
|
description: stop.description,
|
||||||
hasAudio: stop.hasAudio
|
hasAudio: stop.hasAudio,
|
||||||
|
audioStatus: stop.audioStatus,
|
||||||
|
guideLevel: stop.guideLevel
|
||||||
})) || []
|
})) || []
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@@ -708,7 +708,9 @@ const explainGuideStopItems = computed<ExplainGuideStopSelectItem[]>(() => (
|
|||||||
floorId: stop.floorId,
|
floorId: stop.floorId,
|
||||||
coverImageUrl: stop.coverImageUrl,
|
coverImageUrl: stop.coverImageUrl,
|
||||||
description: stop.description,
|
description: stop.description,
|
||||||
hasAudio: stop.hasAudio
|
hasAudio: stop.hasAudio,
|
||||||
|
audioStatus: stop.audioStatus,
|
||||||
|
guideLevel: stop.guideLevel
|
||||||
})) || []
|
})) || []
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -995,19 +997,26 @@ const resolveSelectedPoiHall = async (): Promise<MuseumHall | null> => {
|
|||||||
if (!poi) return null
|
if (!poi) return null
|
||||||
|
|
||||||
const halls = await explainUseCase.listHalls()
|
const halls = await explainUseCase.listHalls()
|
||||||
const hallId = poi.hallId
|
|
||||||
if (hallId) {
|
|
||||||
const matchedById = halls.find((hall) => hall.id === hallId || hall.poiId === hallId)
|
|
||||||
if (matchedById) return matchedById
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchedByPoiId = halls.find((hall) => (
|
const matchedByPoiId = halls.find((hall) => (
|
||||||
hall.poiId === poi.id
|
hall.poiId === poi.id
|
||||||
|| hall.location?.poiId === poi.id
|
|| hall.location?.poiId === poi.id
|
||||||
|| (poi.entrances || []).some((entrance) => hall.poiId === entrance.sourcePlaceId)
|
|
||||||
))
|
))
|
||||||
if (matchedByPoiId) return matchedByPoiId
|
if (matchedByPoiId) return matchedByPoiId
|
||||||
|
|
||||||
|
const hallId = poi.hallId
|
||||||
|
if (hallId) {
|
||||||
|
const matchedById = halls.find((hall) => hall.id === hallId)
|
||||||
|
if (matchedById) return matchedById
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedByEntrance = halls.find((hall) => (
|
||||||
|
(poi.entrances || []).some((entrance) => (
|
||||||
|
hall.poiId === entrance.sourcePlaceId
|
||||||
|
|| hall.poiId === entrance.id
|
||||||
|
))
|
||||||
|
))
|
||||||
|
if (matchedByEntrance) return matchedByEntrance
|
||||||
|
|
||||||
const poiHallName = normalizeHallMatchText(poi.hallName || poi.name)
|
const poiHallName = normalizeHallMatchText(poi.hallName || poi.name)
|
||||||
return halls.find((hall) => {
|
return halls.find((hall) => {
|
||||||
const hallName = normalizeHallMatchText(hall.name)
|
const hallName = normalizeHallMatchText(hall.name)
|
||||||
@@ -1932,17 +1941,30 @@ const handleExplainBack = () => {
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
right: auto;
|
right: auto;
|
||||||
width: calc(var(--museum-h5-page-width) - 24px);
|
width: calc(var(--museum-h5-page-width) - 24px);
|
||||||
bottom: calc(env(safe-area-inset-bottom) + 16px);
|
bottom: calc(env(safe-area-inset-bottom) + 14px);
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: rgba(255, 255, 255, 0.96);
|
overflow: hidden;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.96);
|
background:
|
||||||
border-radius: 16px;
|
linear-gradient(180deg, rgba(224, 225, 0, 0.22) 0, rgba(224, 225, 0, 0) 58px),
|
||||||
box-shadow: 0 10px 28px rgba(36, 49, 42, 0.16);
|
#060704;
|
||||||
|
border: 1px solid rgba(224, 225, 0, 0.34);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.34);
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: 1003;
|
z-index: 1003;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guide-home-dock::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
right: 10px;
|
||||||
|
top: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--museum-accent);
|
||||||
|
}
|
||||||
|
|
||||||
.guide-home-dock.expanded {
|
.guide-home-dock.expanded {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -1964,10 +1986,14 @@ const handleExplainBack = () => {
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
touch-action: pan-y;
|
touch-action: pan-x pan-y;
|
||||||
z-index: 1100;
|
z-index: 1100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guide-home-dock.expanded::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* #ifdef H5 */
|
/* #ifdef H5 */
|
||||||
.guide-home-dock.expanded {
|
.guide-home-dock.expanded {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -1988,18 +2014,18 @@ const handleExplainBack = () => {
|
|||||||
|
|
||||||
.guide-quick-actions {
|
.guide-quick-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: clamp(96px, 14vh, 118px);
|
top: clamp(88px, 13vh, 112px);
|
||||||
right: 18px;
|
right: 14px;
|
||||||
width: 48px;
|
width: 52px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 7px;
|
||||||
z-index: 1002;
|
z-index: 1002;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-quick-action {
|
.guide-quick-action {
|
||||||
width: 48px;
|
width: 52px;
|
||||||
min-height: 56px;
|
min-height: 58px;
|
||||||
padding: 7px 4px 6px;
|
padding: 7px 4px 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -2007,15 +2033,16 @@ const handleExplainBack = () => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: rgba(255, 255, 255, 0.96);
|
background: rgba(245, 245, 237, 0.94);
|
||||||
border: 1px solid rgba(230, 233, 224, 0.96);
|
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 8px 22px rgba(36, 49, 42, 0.14);
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-quick-action.primary {
|
.guide-quick-action.primary {
|
||||||
background: #151713;
|
background: var(--museum-accent);
|
||||||
border-color: #151713;
|
border-color: var(--museum-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-quick-action:active {
|
.guide-quick-action:active {
|
||||||
@@ -2031,7 +2058,7 @@ const handleExplainBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.guide-quick-action.primary .guide-quick-icon {
|
.guide-quick-action.primary .guide-quick-icon {
|
||||||
color: var(--museum-accent);
|
color: #0a0b08;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-quick-icon-floor {
|
.guide-quick-icon-floor {
|
||||||
@@ -2113,7 +2140,7 @@ const handleExplainBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.guide-quick-action-text.primary {
|
.guide-quick-action-text.primary {
|
||||||
color: var(--museum-accent);
|
color: #0a0b08;
|
||||||
}
|
}
|
||||||
|
|
||||||
.route-sim-top-pill {
|
.route-sim-top-pill {
|
||||||
|
|||||||
@@ -80,16 +80,15 @@ onUnmounted(() => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.poi-search-page {
|
.poi-search-page {
|
||||||
--museum-h5-page-width: min(100vw, 430px);
|
--museum-h5-page-width: min(100vw, 430px);
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
padding: calc(env(safe-area-inset-top) + 28px) 14px calc(env(safe-area-inset-bottom) + 16px);
|
padding: calc(env(safe-area-inset-top) + 18px) 0 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background:
|
background: #f5f5ed;
|
||||||
linear-gradient(180deg, rgba(224, 225, 0, 0.22) 0, rgba(245, 245, 237, 0) 146px),
|
|
||||||
#f5f5ed;
|
|
||||||
color: #262421;
|
color: #262421;
|
||||||
font-family: '鸿蒙黑体', 'HarmonyOS Sans SC', 'HarmonyOS Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: '鸿蒙黑体', 'HarmonyOS Sans SC', 'HarmonyOS Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -97,6 +96,19 @@ onUnmounted(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poi-search-page::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 210px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0 64px, rgba(0, 0, 0, 0.18) 65px, rgba(0, 0, 0, 0.5) 100%),
|
||||||
|
url('/static/guide/app-launch-loading.webp') center top / cover no-repeat;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* #ifdef H5 */
|
/* #ifdef H5 */
|
||||||
.poi-search-page {
|
.poi-search-page {
|
||||||
height: var(--poi-search-page-height, 100dvh);
|
height: var(--poi-search-page-height, 100dvh);
|
||||||
@@ -108,20 +120,29 @@ onUnmounted(() => {
|
|||||||
/* #endif */
|
/* #endif */
|
||||||
|
|
||||||
.search-header {
|
.search-header {
|
||||||
min-height: 46px;
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
min-height: 48px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin: 0 2px 16px;
|
margin: 0 18px 118px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-panel-shell {
|
.search-panel-shell {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding: 18px 14px calc(env(safe-area-inset-bottom) + 14px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #fffef0;
|
||||||
|
border-radius: 22px 22px 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-shadow: 0 -10px 28px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-header-copy {
|
.search-header-copy {
|
||||||
@@ -132,31 +153,31 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 26px;
|
font-size: 24px;
|
||||||
line-height: 31px;
|
line-height: 30px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
color: #000000;
|
color: #050704;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-subtitle {
|
.page-subtitle {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 15px;
|
line-height: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #5d5b50;
|
color: #4f5149;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-link {
|
.map-link {
|
||||||
min-width: 74px;
|
min-width: 76px;
|
||||||
height: 32px;
|
height: 34px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: #000000;
|
background: #050704;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.88);
|
border: 1px solid #050704;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,13 +198,16 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
@media (max-width: 360px) {
|
@media (max-width: 360px) {
|
||||||
.poi-search-page {
|
.poi-search-page {
|
||||||
padding-top: calc(env(safe-area-inset-top) + 22px);
|
padding-top: calc(env(safe-area-inset-top) + 14px);
|
||||||
padding-left: 12px;
|
|
||||||
padding-right: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-header {
|
.search-header {
|
||||||
margin-bottom: 12px;
|
margin: 0 14px 108px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-panel-shell {
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import {
|
|||||||
dataSourceConfig
|
dataSourceConfig
|
||||||
} from '@/config/dataSource'
|
} from '@/config/dataSource'
|
||||||
import {
|
import {
|
||||||
normalizeSameOriginPublicUrl
|
type AudioPlayTargetType
|
||||||
} from '@/utils/publicUrl'
|
|
||||||
import type {
|
|
||||||
AudioPlayTargetType
|
|
||||||
} from '@/domain/museum'
|
} from '@/domain/museum'
|
||||||
import {
|
import {
|
||||||
|
toGuideAudioPlayInfo,
|
||||||
|
toGuideAudioTextInfo,
|
||||||
toGuideStopInfo,
|
toGuideStopInfo,
|
||||||
|
type BackendAudioPlayInfo,
|
||||||
|
type BackendAudioTextInfo,
|
||||||
type BackendGuideStopInfo,
|
type BackendGuideStopInfo,
|
||||||
|
type GuideAudioPlayInfo,
|
||||||
|
type GuideAudioTextInfo,
|
||||||
type GuideStopInfo
|
type GuideStopInfo
|
||||||
} from '@/data/adapters/guideStopInfoAdapter'
|
} from '@/data/adapters/guideStopInfoAdapter'
|
||||||
|
|
||||||
@@ -19,6 +22,7 @@ export interface AudioPlayInfoRequest {
|
|||||||
targetType: AudioPlayTargetType
|
targetType: AudioPlayTargetType
|
||||||
targetId: string
|
targetId: string
|
||||||
lang?: AudioLanguage
|
lang?: AudioLanguage
|
||||||
|
refresh?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GuideStopInfoRequest {
|
export interface GuideStopInfoRequest {
|
||||||
@@ -33,29 +37,12 @@ interface GuideStopInfoResponse {
|
|||||||
data?: BackendGuideStopInfo
|
data?: BackendGuideStopInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AudioPlayInfo {
|
export interface AudioPlayInfo extends GuideAudioPlayInfo {}
|
||||||
playable: boolean
|
|
||||||
targetType: AudioPlayTargetType
|
|
||||||
targetId: string | number
|
|
||||||
lang: AudioLanguage
|
|
||||||
narrationTier?: 'STANDARD' | 'EXTENDED'
|
|
||||||
audioId?: string | number | null
|
|
||||||
title?: string | null
|
|
||||||
duration?: number | null
|
|
||||||
format?: string | null
|
|
||||||
playUrl?: string | null
|
|
||||||
expiresAt?: string | null
|
|
||||||
subtitleUrl?: string | null
|
|
||||||
hasText?: boolean
|
|
||||||
fallback?: boolean
|
|
||||||
fallbackReason?: string | null
|
|
||||||
reason?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AudioPlayInfoResponse {
|
interface AudioPlayInfoResponse {
|
||||||
code: number
|
code: number
|
||||||
msg?: string
|
msg?: string
|
||||||
data?: AudioPlayInfo
|
data?: BackendAudioPlayInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AudioTextInfoRequest {
|
export interface AudioTextInfoRequest {
|
||||||
@@ -64,23 +51,12 @@ export interface AudioTextInfoRequest {
|
|||||||
lang?: AudioLanguage
|
lang?: AudioLanguage
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AudioTextInfo {
|
export interface AudioTextInfo extends GuideAudioTextInfo {}
|
||||||
available: boolean
|
|
||||||
targetType: AudioPlayTargetType
|
|
||||||
targetId: string | number
|
|
||||||
lang: AudioLanguage
|
|
||||||
narrationTier?: 'STANDARD' | 'EXTENDED'
|
|
||||||
title?: string | null
|
|
||||||
text?: string | null
|
|
||||||
textLength?: number | null
|
|
||||||
textHash?: string | null
|
|
||||||
reason?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AudioTextInfoResponse {
|
interface AudioTextInfoResponse {
|
||||||
code: number
|
code: number
|
||||||
msg?: string
|
msg?: string
|
||||||
data?: AudioTextInfo
|
data?: BackendAudioTextInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AudioPlayInfoRepository {
|
export interface AudioPlayInfoRepository {
|
||||||
@@ -143,19 +119,23 @@ const normalizeBaseUrl = (baseUrl: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolveAudioApiBaseUrl = () => {
|
const resolveAudioApiBaseUrl = () => {
|
||||||
const baseUrl = normalizeBaseUrl(dataSourceConfig.audioApiBaseUrl)
|
const baseUrl = normalizeBaseUrl(dataSourceConfig.apiBaseUrl)
|
||||||
return baseUrl.endsWith('/app-api') ? baseUrl : `${baseUrl}/app-api`
|
return baseUrl.endsWith('/app-api') ? baseUrl : `${baseUrl}/app-api`
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioKey = ({ targetType, targetId, lang }: Required<AudioPlayInfoRequest>) => (
|
type RequiredAudioPlayInfoRequest = Required<Omit<AudioPlayInfoRequest, 'refresh'>>
|
||||||
|
type RequiredGuideStopInfoRequest = Required<GuideStopInfoRequest>
|
||||||
|
type RequiredAudioTextInfoRequest = Required<AudioTextInfoRequest>
|
||||||
|
|
||||||
|
const audioKey = ({ targetType, targetId, lang }: RequiredAudioPlayInfoRequest) => (
|
||||||
`${targetType}:${targetId}:${lang}`
|
`${targetType}:${targetId}:${lang}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const stopInfoKey = ({ targetType, targetId, lang }: Required<GuideStopInfoRequest>) => (
|
const stopInfoKey = ({ targetType, targetId, lang }: RequiredGuideStopInfoRequest) => (
|
||||||
`${targetType}:${targetId}:${lang}`
|
`${targetType}:${targetId}:${lang}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const audioTextKey = ({ targetType, targetId, lang }: Required<AudioTextInfoRequest>) => (
|
const audioTextKey = ({ targetType, targetId, lang }: RequiredAudioTextInfoRequest) => (
|
||||||
`${targetType}:${targetId}:${lang}`
|
`${targetType}:${targetId}:${lang}`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -190,7 +170,7 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
|||||||
throw new Error('讲解展示信息接口暂不可用')
|
throw new Error('讲解展示信息接口暂不可用')
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedRequest: Required<GuideStopInfoRequest> = {
|
const normalizedRequest: RequiredGuideStopInfoRequest = {
|
||||||
targetType: request.targetType,
|
targetType: request.targetType,
|
||||||
targetId: request.targetId,
|
targetId: request.targetId,
|
||||||
lang: request.lang || (dataSourceConfig.audioLanguage as AudioLanguage)
|
lang: request.lang || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||||
@@ -221,7 +201,7 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPlayInfo(request: AudioPlayInfoRequest): Promise<AudioPlayInfo> {
|
async getPlayInfo(request: AudioPlayInfoRequest): Promise<AudioPlayInfo> {
|
||||||
const normalizedRequest: Required<AudioPlayInfoRequest> = {
|
const normalizedRequest: RequiredAudioPlayInfoRequest = {
|
||||||
targetType: request.targetType,
|
targetType: request.targetType,
|
||||||
targetId: request.targetId,
|
targetId: request.targetId,
|
||||||
lang: request.lang || (dataSourceConfig.audioLanguage as AudioLanguage)
|
lang: request.lang || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||||
@@ -233,13 +213,15 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
|||||||
targetType: normalizedRequest.targetType,
|
targetType: normalizedRequest.targetType,
|
||||||
targetId: normalizedRequest.targetId,
|
targetId: normalizedRequest.targetId,
|
||||||
lang: normalizedRequest.lang,
|
lang: normalizedRequest.lang,
|
||||||
|
hasText: false,
|
||||||
|
fallback: false,
|
||||||
reason: 'SERVICE_ERROR'
|
reason: 'SERVICE_ERROR'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cached = this.cache.get(key)
|
const cached = this.cache.get(key)
|
||||||
|
|
||||||
if (cached && !isExpired(cached.expiresAt)) {
|
if (!request.refresh && cached && !isExpired(cached.expiresAt)) {
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,17 +238,16 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
|||||||
throw new Error(response.msg || '语音播放解析失败')
|
throw new Error(response.msg || '语音播放解析失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
response.data.playUrl = normalizeSameOriginPublicUrl(response.data.playUrl)
|
const playInfo = toGuideAudioPlayInfo(response.data, normalizedRequest)
|
||||||
response.data.subtitleUrl = normalizeSameOriginPublicUrl(response.data.subtitleUrl)
|
|
||||||
|
|
||||||
if (response.data.playable || response.data.reason !== 'TARGET_NOT_FOUND') {
|
if (playInfo.playable || playInfo.reason !== 'TARGET_NOT_FOUND') {
|
||||||
this.cache.set(key, response.data)
|
this.cache.set(key, playInfo)
|
||||||
}
|
}
|
||||||
return response.data
|
return playInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTextInfo(request: AudioTextInfoRequest): Promise<AudioTextInfo> {
|
async getTextInfo(request: AudioTextInfoRequest): Promise<AudioTextInfo> {
|
||||||
const normalizedRequest: Required<AudioTextInfoRequest> = {
|
const normalizedRequest: RequiredAudioTextInfoRequest = {
|
||||||
targetType: request.targetType,
|
targetType: request.targetType,
|
||||||
targetId: request.targetId,
|
targetId: request.targetId,
|
||||||
lang: request.lang || (dataSourceConfig.audioLanguage as AudioLanguage)
|
lang: request.lang || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||||
@@ -301,11 +282,13 @@ export class DefaultAudioPlayInfoRepository implements AudioPlayInfoRepository {
|
|||||||
throw new Error(response.msg || '讲解词正文解析失败')
|
throw new Error(response.msg || '讲解词正文解析失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.data.available) {
|
const textInfo = toGuideAudioTextInfo(response.data, normalizedRequest)
|
||||||
this.textCache.set(key, response.data)
|
|
||||||
|
if (textInfo.available) {
|
||||||
|
this.textCache.set(key, textInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data
|
return textInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCache(lang?: AudioLanguage) {
|
clearCache(lang?: AudioLanguage) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
type MuseumContentRepository
|
type MuseumContentRepository
|
||||||
} from '@/repositories/MuseumContentRepository'
|
} from '@/repositories/MuseumContentRepository'
|
||||||
import {
|
import {
|
||||||
staticMuseumContentProvider,
|
explainContentProvider,
|
||||||
type ExplainContentProvider
|
type ExplainContentProvider
|
||||||
} from '@/data/providers/staticMuseumContentProvider'
|
} from '@/data/providers/staticMuseumContentProvider'
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ export class DefaultExplainRepository implements ExplainRepository {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly content: MuseumContentRepository = museumContentRepository,
|
private readonly content: MuseumContentRepository = museumContentRepository,
|
||||||
private readonly media: MediaRepository = mediaRepository,
|
private readonly media: MediaRepository = mediaRepository,
|
||||||
private readonly explainContent: ExplainContentProvider = staticMuseumContentProvider
|
private readonly explainContent: ExplainContentProvider = explainContentProvider
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
listExplainExhibits() {
|
listExplainExhibits() {
|
||||||
@@ -54,11 +54,12 @@ export class DefaultExplainRepository implements ExplainRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listHalls() {
|
listHalls() {
|
||||||
return this.content.listHalls()
|
return this.explainContent.listHalls()
|
||||||
}
|
}
|
||||||
|
|
||||||
getHallById(id: string) {
|
async getHallById(id: string) {
|
||||||
return this.content.getHallById(id)
|
const halls = await this.explainContent.listHalls()
|
||||||
|
return halls.find((hall) => hall.id === id || hall.poiId === id) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
async listGuideStopsByHall(hallId: string) {
|
async listGuideStopsByHall(hallId: string) {
|
||||||
|
|||||||
@@ -1,39 +1,8 @@
|
|||||||
import {
|
|
||||||
dataSourceConfig
|
|
||||||
} from '@/config/dataSource'
|
|
||||||
import type {
|
|
||||||
AudioPlayTargetType
|
|
||||||
} from '@/domain/museum'
|
|
||||||
import type {
|
import type {
|
||||||
AudioLanguage,
|
AudioLanguage,
|
||||||
AudioPlayInfoRequest
|
AudioPlayInfoRequest
|
||||||
} from '@/repositories/AudioPlayInfoRepository'
|
} from '@/repositories/AudioPlayInfoRepository'
|
||||||
|
|
||||||
interface CommonResult<T> {
|
|
||||||
code: number
|
|
||||||
msg?: string
|
|
||||||
data?: T
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PublishedGuideContent {
|
|
||||||
id?: string | number | null
|
|
||||||
title?: string | null
|
|
||||||
targetType?: string | null
|
|
||||||
targetId?: string | number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PublishedExhibitAudioSummary {
|
|
||||||
id?: string | number | null
|
|
||||||
name?: string | null
|
|
||||||
hasAudio?: boolean
|
|
||||||
supportedLanguages?: string[]
|
|
||||||
audioStatus?: string | null
|
|
||||||
playTargetType?: string | null
|
|
||||||
playTargetId?: string | number | null
|
|
||||||
audioDuration?: number | null
|
|
||||||
guideContents?: PublishedGuideContent[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PublishedExhibitAudioResolution {
|
export interface PublishedExhibitAudioResolution {
|
||||||
targets: AudioPlayInfoRequest[]
|
targets: AudioPlayInfoRequest[]
|
||||||
}
|
}
|
||||||
@@ -42,120 +11,10 @@ export interface PublishedExhibitAudioRepository {
|
|||||||
resolveByExhibitName(exhibitName: string, lang?: AudioLanguage): Promise<PublishedExhibitAudioResolution | null>
|
resolveByExhibitName(exhibitName: string, lang?: AudioLanguage): Promise<PublishedExhibitAudioResolution | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringifyId = (value: string | number | null | undefined) => (
|
export class DisabledPublishedExhibitAudioRepository implements PublishedExhibitAudioRepository {
|
||||||
value === null || typeof value === 'undefined' ? '' : String(value)
|
async resolveByExhibitName() {
|
||||||
)
|
return null
|
||||||
|
|
||||||
const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '')
|
|
||||||
|
|
||||||
const resolveAppApiBaseUrl = () => {
|
|
||||||
const baseUrl = normalizeBaseUrl(dataSourceConfig.audioApiBaseUrl || dataSourceConfig.apiBaseUrl)
|
|
||||||
return baseUrl.endsWith('/app-api') ? baseUrl : `${baseUrl}/app-api`
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseJsonPayload = <T>(payload: unknown): T => {
|
|
||||||
if (typeof payload === 'string') {
|
|
||||||
return JSON.parse(payload) as T
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload as T
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestJson = <T>(url: string): Promise<T> => new Promise((resolve, reject) => {
|
|
||||||
uni.request({
|
|
||||||
url,
|
|
||||||
method: 'GET',
|
|
||||||
timeout: 5000,
|
|
||||||
success: (response) => {
|
|
||||||
const statusCode = Number(response.statusCode || 0)
|
|
||||||
if (statusCode < 200 || statusCode >= 300) {
|
|
||||||
reject(new Error(`展品音频发布接口请求失败: ${statusCode}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
resolve(parseJsonPayload<T>(response.data))
|
|
||||||
} catch (error) {
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: reject
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const normalizeAudioTargetType = (targetType: string | null | undefined): AudioPlayTargetType | null => {
|
|
||||||
const normalizedType = targetType?.toUpperCase()
|
|
||||||
return normalizedType === 'STOP' || normalizedType === 'ITEM'
|
|
||||||
? normalizedType
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
|
|
||||||
const addTarget = (
|
|
||||||
targets: AudioPlayInfoRequest[],
|
|
||||||
targetType: string | null | undefined,
|
|
||||||
targetId: string | number | null | undefined,
|
|
||||||
lang: AudioLanguage
|
|
||||||
) => {
|
|
||||||
const normalizedType = normalizeAudioTargetType(targetType)
|
|
||||||
const normalizedId = stringifyId(targetId)
|
|
||||||
if (!normalizedType || !normalizedId) return
|
|
||||||
|
|
||||||
const exists = targets.some((target) => (
|
|
||||||
target.targetType === normalizedType && target.targetId === normalizedId && target.lang === lang
|
|
||||||
))
|
|
||||||
if (exists) return
|
|
||||||
|
|
||||||
targets.push({
|
|
||||||
targetType: normalizedType,
|
|
||||||
targetId: normalizedId,
|
|
||||||
lang
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildResolution = (
|
|
||||||
exhibit: PublishedExhibitAudioSummary,
|
|
||||||
lang: AudioLanguage
|
|
||||||
): PublishedExhibitAudioResolution => {
|
|
||||||
const targets: AudioPlayInfoRequest[] = []
|
|
||||||
|
|
||||||
addTarget(targets, exhibit.playTargetType, exhibit.playTargetId, lang)
|
|
||||||
exhibit.guideContents?.forEach((guide) => {
|
|
||||||
addTarget(targets, guide.targetType, guide.targetId, lang)
|
|
||||||
})
|
|
||||||
addTarget(targets, 'ITEM', exhibit.id, lang)
|
|
||||||
|
|
||||||
return { targets }
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DefaultPublishedExhibitAudioRepository implements PublishedExhibitAudioRepository {
|
|
||||||
async resolveByExhibitName(exhibitName: string, lang: AudioLanguage = dataSourceConfig.audioLanguage as AudioLanguage) {
|
|
||||||
const keyword = exhibitName.trim()
|
|
||||||
if (!keyword) return null
|
|
||||||
|
|
||||||
const searchUrl = `${resolveAppApiBaseUrl()}/gis/exhibit/search?keyword=${encodeURIComponent(keyword)}`
|
|
||||||
const searchResponse = await requestJson<CommonResult<PublishedExhibitAudioSummary[]>>(searchUrl)
|
|
||||||
if (searchResponse.code !== 0 || !searchResponse.data?.length) return null
|
|
||||||
|
|
||||||
const exactMatches = searchResponse.data.filter((item) => item.name === keyword)
|
|
||||||
const matched = (
|
|
||||||
exactMatches.find((item) => item.hasAudio && item.supportedLanguages?.includes(lang))
|
|
||||||
|| exactMatches.find((item) => item.hasAudio)
|
|
||||||
|| exactMatches[0]
|
|
||||||
|| searchResponse.data[0]
|
|
||||||
)
|
|
||||||
const exhibitId = stringifyId(matched.id)
|
|
||||||
let detail = matched
|
|
||||||
|
|
||||||
if (exhibitId) {
|
|
||||||
const detailUrl = `${resolveAppApiBaseUrl()}/gis/exhibit/get?id=${encodeURIComponent(exhibitId)}`
|
|
||||||
const detailResponse = await requestJson<CommonResult<PublishedExhibitAudioSummary>>(detailUrl)
|
|
||||||
if (detailResponse.code === 0 && detailResponse.data) {
|
|
||||||
detail = detailResponse.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildResolution(detail, lang)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publishedExhibitAudioRepository = new DefaultPublishedExhibitAudioRepository()
|
export const publishedExhibitAudioRepository = new DisabledPublishedExhibitAudioRepository()
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ import {
|
|||||||
explainRepository,
|
explainRepository,
|
||||||
type ExplainRepository
|
type ExplainRepository
|
||||||
} from '@/repositories/ExplainRepository'
|
} from '@/repositories/ExplainRepository'
|
||||||
import {
|
|
||||||
mediaRepository,
|
|
||||||
type MediaRepository
|
|
||||||
} from '@/repositories/MediaRepository'
|
|
||||||
import {
|
import {
|
||||||
audioPlayInfoRepository,
|
audioPlayInfoRepository,
|
||||||
audioReasonToText,
|
audioReasonToText,
|
||||||
@@ -28,10 +24,6 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
GuideStopInfo
|
GuideStopInfo
|
||||||
} from '@/data/adapters/guideStopInfoAdapter'
|
} from '@/data/adapters/guideStopInfoAdapter'
|
||||||
import {
|
|
||||||
publishedExhibitAudioRepository,
|
|
||||||
type PublishedExhibitAudioRepository
|
|
||||||
} from '@/repositories/PublishedExhibitAudioRepository'
|
|
||||||
import {
|
import {
|
||||||
dataSourceConfig
|
dataSourceConfig
|
||||||
} from '@/config/dataSource'
|
} from '@/config/dataSource'
|
||||||
@@ -75,9 +67,7 @@ export interface ExplainHallSummary {
|
|||||||
export class ExplainUseCase {
|
export class ExplainUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly explain: ExplainRepository = explainRepository,
|
private readonly explain: ExplainRepository = explainRepository,
|
||||||
private readonly media: MediaRepository = mediaRepository,
|
private readonly audioPlayInfo: AudioPlayInfoRepository = audioPlayInfoRepository
|
||||||
private readonly audioPlayInfo: AudioPlayInfoRepository = audioPlayInfoRepository,
|
|
||||||
private readonly publishedAudio: PublishedExhibitAudioRepository = publishedExhibitAudioRepository
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private applyTrackAudioState(exhibit: MuseumExhibit, track?: ExplainTrack | null): MuseumExhibit {
|
private applyTrackAudioState(exhibit: MuseumExhibit, track?: ExplainTrack | null): MuseumExhibit {
|
||||||
@@ -134,7 +124,7 @@ export class ExplainUseCase {
|
|||||||
fallback?: MuseumExhibit | null
|
fallback?: MuseumExhibit | null
|
||||||
): MuseumExhibit {
|
): MuseumExhibit {
|
||||||
const linkedPrimary = stopInfo.linkedExhibits[0]
|
const linkedPrimary = stopInfo.linkedExhibits[0]
|
||||||
const coverImage = stopInfo.coverImageUrl || fallback?.image
|
const coverImage = stopInfo.coverImageUrl || (stopInfo.imageStatus === 'READY' ? fallback?.image : undefined)
|
||||||
const description = stopInfo.description || fallback?.description || '该讲解暂无简介。'
|
const description = stopInfo.description || fallback?.description || '该讲解暂无简介。'
|
||||||
const audioTarget = this.resolveStopInfoAudioTarget(stopInfo)
|
const audioTarget = this.resolveStopInfoAudioTarget(stopInfo)
|
||||||
const audioAvailable = stopInfo.audioStatus === 'READY'
|
const audioAvailable = stopInfo.audioStatus === 'READY'
|
||||||
@@ -175,6 +165,8 @@ export class ExplainUseCase {
|
|||||||
imageStatus: stopInfo.imageStatus,
|
imageStatus: stopInfo.imageStatus,
|
||||||
imageSource: stopInfo.imageSource,
|
imageSource: stopInfo.imageSource,
|
||||||
galleryUrls: stopInfo.galleryUrls,
|
galleryUrls: stopInfo.galleryUrls,
|
||||||
|
linkedExhibitCount: stopInfo.linkedExhibitCount,
|
||||||
|
isSharedStop: stopInfo.isSharedStop,
|
||||||
linkedExhibits: stopInfo.linkedExhibits,
|
linkedExhibits: stopInfo.linkedExhibits,
|
||||||
stopInfoAvailable: stopInfo.available,
|
stopInfoAvailable: stopInfo.available,
|
||||||
stopInfoReason: stopInfo.reason,
|
stopInfoReason: stopInfo.reason,
|
||||||
@@ -211,15 +203,10 @@ export class ExplainUseCase {
|
|||||||
|
|
||||||
private applyPlayInfo(
|
private applyPlayInfo(
|
||||||
exhibit: MuseumExhibit,
|
exhibit: MuseumExhibit,
|
||||||
playInfo: AudioPlayInfo,
|
playInfo: AudioPlayInfo
|
||||||
options: { preserveStaticFallback?: boolean } = {}
|
|
||||||
): MuseumExhibit {
|
): MuseumExhibit {
|
||||||
const apiPlayable = playInfo.playable === true && Boolean(playInfo.playUrl)
|
const apiPlayable = playInfo.playable === true && Boolean(playInfo.playUrl)
|
||||||
const canUseStaticAudioFallback = !playInfo.reason || playInfo.reason === 'TARGET_NOT_FOUND'
|
const nextAudioUrl = apiPlayable ? playInfo.playUrl || undefined : undefined
|
||||||
const fallbackAudioUrl = options.preserveStaticFallback && canUseStaticAudioFallback && exhibit.audioUrl
|
|
||||||
? exhibit.audioUrl
|
|
||||||
: undefined
|
|
||||||
const nextAudioUrl = apiPlayable ? playInfo.playUrl || undefined : fallbackAudioUrl
|
|
||||||
const nextAudioDuration = typeof playInfo.duration === 'number'
|
const nextAudioDuration = typeof playInfo.duration === 'number'
|
||||||
? playInfo.duration
|
? playInfo.duration
|
||||||
: exhibit.audioDuration
|
: exhibit.audioDuration
|
||||||
@@ -234,7 +221,8 @@ export class ExplainUseCase {
|
|||||||
audioHasText: playInfo.hasText === true || exhibit.audioHasText,
|
audioHasText: playInfo.hasText === true || exhibit.audioHasText,
|
||||||
audioNarrationTier: playInfo.narrationTier || exhibit.audioNarrationTier,
|
audioNarrationTier: playInfo.narrationTier || exhibit.audioNarrationTier,
|
||||||
audioUnavailableReason: apiPlayable ? undefined : audioReasonToText(playInfo.reason),
|
audioUnavailableReason: apiPlayable ? undefined : audioReasonToText(playInfo.reason),
|
||||||
audioAvailable: apiPlayable || Boolean(fallbackAudioUrl)
|
audioAvailable: apiPlayable,
|
||||||
|
audioStatus: apiPlayable ? 'READY' : 'MISSING'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +246,6 @@ export class ExplainUseCase {
|
|||||||
exhibit: MuseumExhibit,
|
exhibit: MuseumExhibit,
|
||||||
options: {
|
options: {
|
||||||
includeText?: boolean
|
includeText?: boolean
|
||||||
preserveStaticFallback?: boolean
|
|
||||||
track?: ExplainTrack | null
|
track?: ExplainTrack | null
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<MuseumExhibit> {
|
): Promise<MuseumExhibit> {
|
||||||
@@ -268,9 +255,7 @@ export class ExplainUseCase {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const playInfo = await this.audioPlayInfo.getPlayInfo({ targetType, targetId })
|
const playInfo = await this.audioPlayInfo.getPlayInfo({ targetType, targetId })
|
||||||
let nextExhibit = this.applyPlayInfo(baseExhibit, playInfo, {
|
let nextExhibit = this.applyPlayInfo(baseExhibit, playInfo)
|
||||||
preserveStaticFallback: options.preserveStaticFallback
|
|
||||||
})
|
|
||||||
|
|
||||||
if (options.includeText && playInfo.hasText) {
|
if (options.includeText && playInfo.hasText) {
|
||||||
try {
|
try {
|
||||||
@@ -346,7 +331,6 @@ export class ExplainUseCase {
|
|||||||
|
|
||||||
return this.enrichExhibitAudio(exhibit, {
|
return this.enrichExhibitAudio(exhibit, {
|
||||||
includeText: options.includeText,
|
includeText: options.includeText,
|
||||||
preserveStaticFallback: true,
|
|
||||||
track
|
track
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -433,7 +417,18 @@ export class ExplainUseCase {
|
|||||||
|
|
||||||
async loadExplainHallSummaries(hallIds: string[]): Promise<Record<string, ExplainHallSummary>> {
|
async loadExplainHallSummaries(hallIds: string[]): Promise<Record<string, ExplainHallSummary>> {
|
||||||
const uniqueHallIds = Array.from(new Set(hallIds.map((id) => id.trim()).filter(Boolean)))
|
const uniqueHallIds = Array.from(new Set(hallIds.map((id) => id.trim()).filter(Boolean)))
|
||||||
|
const halls = await this.listHalls().catch(() => [])
|
||||||
|
const summariesFromHallList = new Map(halls.map((hall) => [hall.id, hall]))
|
||||||
const entries = await Promise.all(uniqueHallIds.map(async (hallId) => {
|
const entries = await Promise.all(uniqueHallIds.map(async (hallId) => {
|
||||||
|
const hall = summariesFromHallList.get(hallId)
|
||||||
|
if (hall && (typeof hall.outlineCount === 'number' || typeof hall.stopCount === 'number')) {
|
||||||
|
return [hallId, {
|
||||||
|
hallId,
|
||||||
|
businessUnitCount: hall.outlineCount || 0,
|
||||||
|
guideStopCount: hall.stopCount || 0
|
||||||
|
}] as const
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const units = await this.loadTemporaryBusinessUnitsByHall(hallId)
|
const units = await this.loadTemporaryBusinessUnitsByHall(hallId)
|
||||||
const guideStopCount = units.reduce((total, unit) => total + unit.guideStopCount, 0)
|
const guideStopCount = units.reduce((total, unit) => total + unit.guideStopCount, 0)
|
||||||
@@ -508,15 +503,6 @@ export class ExplainUseCase {
|
|||||||
const exhibit = detailExhibit || summaryExhibit
|
const exhibit = detailExhibit || summaryExhibit
|
||||||
if (!exhibit) return null
|
if (!exhibit) return null
|
||||||
|
|
||||||
const fallbackMedia = await (track?.mediaId
|
|
||||||
? this.media.getMediaById(track.mediaId)
|
|
||||||
: exhibit.guideContentId
|
|
||||||
? this.media.getMediaById(`media-${exhibit.guideContentId}`)
|
|
||||||
: Promise.resolve(null)
|
|
||||||
).catch((error) => {
|
|
||||||
console.warn('静态讲解媒体读取失败,将继续尝试播放接口:', error)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
const targetType: AudioPlayTargetType = track?.playTargetType
|
const targetType: AudioPlayTargetType = track?.playTargetType
|
||||||
|| exhibit.playTargetType
|
|| exhibit.playTargetType
|
||||||
|| 'ITEM'
|
|| 'ITEM'
|
||||||
@@ -533,7 +519,7 @@ export class ExplainUseCase {
|
|||||||
id: `play-${playInfo.audioId || `${requestTargetType}-${requestTargetId}`}`,
|
id: `play-${playInfo.audioId || `${requestTargetType}-${requestTargetId}`}`,
|
||||||
type: 'audio',
|
type: 'audio',
|
||||||
url: playInfo.playUrl || undefined,
|
url: playInfo.playUrl || undefined,
|
||||||
duration: typeof playInfo.duration === 'number' ? playInfo.duration : fallbackMedia?.duration,
|
duration: typeof playInfo.duration === 'number' ? playInfo.duration : undefined,
|
||||||
language: playInfo.lang,
|
language: playInfo.lang,
|
||||||
available: true
|
available: true
|
||||||
}
|
}
|
||||||
@@ -547,49 +533,6 @@ export class ExplainUseCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toDirectMediaSelection = (playInfo: AudioPlayInfo): ExplainAudioSelection | null => {
|
|
||||||
const directMedia = fallbackMedia?.available && fallbackMedia.url
|
|
||||||
? fallbackMedia
|
|
||||||
: exhibit.audioUrl
|
|
||||||
? {
|
|
||||||
id: `media-${exhibit.guideContentId || exhibit.id}`,
|
|
||||||
type: 'audio' as const,
|
|
||||||
url: exhibit.audioUrl,
|
|
||||||
duration: exhibit.audioDuration,
|
|
||||||
language: playInfo.lang,
|
|
||||||
available: true
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
|
|
||||||
if (!directMedia?.url) return null
|
|
||||||
|
|
||||||
return {
|
|
||||||
exhibit,
|
|
||||||
track,
|
|
||||||
media: directMedia,
|
|
||||||
playable: true,
|
|
||||||
playInfo: {
|
|
||||||
...playInfo,
|
|
||||||
playable: true,
|
|
||||||
audioId: directMedia.id,
|
|
||||||
title: track?.title || exhibit.guideTitle || exhibit.name,
|
|
||||||
duration: directMedia.duration || playInfo.duration,
|
|
||||||
playUrl: directMedia.url,
|
|
||||||
fallback: true,
|
|
||||||
fallbackReason: playInfo.reason || 'DIRECT_MEDIA_FALLBACK'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const directMediaSelection = toDirectMediaSelection({
|
|
||||||
playable: false,
|
|
||||||
targetType,
|
|
||||||
targetId,
|
|
||||||
lang: dataSourceConfig.audioLanguage as AudioPlayInfo['lang'],
|
|
||||||
reason: 'DIRECT_MEDIA_FALLBACK'
|
|
||||||
})
|
|
||||||
if (directMediaSelection) return directMediaSelection
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const initialPlayInfo = await this.audioPlayInfo.getPlayInfo({ targetType, targetId })
|
const initialPlayInfo = await this.audioPlayInfo.getPlayInfo({ targetType, targetId })
|
||||||
|
|
||||||
@@ -597,20 +540,6 @@ export class ExplainUseCase {
|
|||||||
return toPlayableSelection(initialPlayInfo, targetType, targetId)
|
return toPlayableSelection(initialPlayInfo, targetType, targetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initialPlayInfo.reason === 'TARGET_NOT_FOUND') {
|
|
||||||
const publishedResolution = await this.publishedAudio.resolveByExhibitName(exhibit.name, initialPlayInfo.lang)
|
|
||||||
if (publishedResolution?.targets.length) {
|
|
||||||
for (const candidate of publishedResolution.targets) {
|
|
||||||
if (candidate.targetType === targetType && candidate.targetId === targetId) continue
|
|
||||||
|
|
||||||
const playInfo = await this.audioPlayInfo.getPlayInfo(candidate)
|
|
||||||
if (playInfo.playable && playInfo.playUrl) {
|
|
||||||
return toPlayableSelection(playInfo, candidate.targetType, candidate.targetId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
exhibit,
|
exhibit,
|
||||||
track,
|
track,
|
||||||
@@ -626,11 +555,10 @@ export class ExplainUseCase {
|
|||||||
targetType,
|
targetType,
|
||||||
targetId,
|
targetId,
|
||||||
lang: dataSourceConfig.audioLanguage as AudioPlayInfo['lang'],
|
lang: dataSourceConfig.audioLanguage as AudioPlayInfo['lang'],
|
||||||
|
hasText: false,
|
||||||
|
fallback: false,
|
||||||
reason: 'SERVICE_ERROR'
|
reason: 'SERVICE_ERROR'
|
||||||
}
|
}
|
||||||
const directMediaSelection = toDirectMediaSelection(serviceErrorPlayInfo)
|
|
||||||
if (directMediaSelection) return directMediaSelection
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
exhibit,
|
exhibit,
|
||||||
track,
|
track,
|
||||||
@@ -642,7 +570,10 @@ export class ExplainUseCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectAudioForExplainDetail(exhibit: MuseumExhibit): Promise<ExplainAudioSelection> {
|
async selectAudioForExplainDetail(
|
||||||
|
exhibit: MuseumExhibit,
|
||||||
|
options: { refreshPlayInfo?: boolean } = {}
|
||||||
|
): Promise<ExplainAudioSelection> {
|
||||||
const targetType = exhibit.playTargetType || 'ITEM'
|
const targetType = exhibit.playTargetType || 'ITEM'
|
||||||
const targetId = exhibit.playTargetId || exhibit.id
|
const targetId = exhibit.playTargetId || exhibit.id
|
||||||
|
|
||||||
@@ -660,7 +591,8 @@ export class ExplainUseCase {
|
|||||||
const playInfo = await this.audioPlayInfo.getPlayInfo({
|
const playInfo = await this.audioPlayInfo.getPlayInfo({
|
||||||
targetType,
|
targetType,
|
||||||
targetId,
|
targetId,
|
||||||
lang: (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage)
|
lang: (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage),
|
||||||
|
refresh: options.refreshPlayInfo === true
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!playInfo.playable || !playInfo.playUrl) {
|
if (!playInfo.playable || !playInfo.playUrl) {
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ export interface ExplainDetailPageViewModel {
|
|||||||
statusText?: string
|
statusText?: string
|
||||||
}
|
}
|
||||||
imageStatus?: string
|
imageStatus?: string
|
||||||
|
linkedExhibitCount?: number
|
||||||
|
isSharedStop?: boolean
|
||||||
|
linkedExhibitStatusText?: string
|
||||||
linkedExhibits?: Array<{
|
linkedExhibits?: Array<{
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -128,9 +131,9 @@ const languageLabelFor = (language?: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAudioReady = (exhibit: MuseumExhibit) => (
|
const isAudioReady = (exhibit: MuseumExhibit) => (
|
||||||
exhibit.audioStatus === 'READY'
|
exhibit.audioStatus
|
||||||
|| exhibit.audioAvailable === true
|
? exhibit.audioStatus === 'READY'
|
||||||
|| Boolean(exhibit.audioUrl?.trim())
|
: exhibit.audioAvailable === true || Boolean(exhibit.audioUrl?.trim())
|
||||||
)
|
)
|
||||||
|
|
||||||
export const toExplainCardViewModel = (exhibit: MuseumExhibit): ExplainCardViewModel => {
|
export const toExplainCardViewModel = (exhibit: MuseumExhibit): ExplainCardViewModel => {
|
||||||
@@ -153,7 +156,7 @@ export const toExplainCardViewModel = (exhibit: MuseumExhibit): ExplainCardViewM
|
|||||||
audioDuration: exhibit.audioDuration,
|
audioDuration: exhibit.audioDuration,
|
||||||
languageLabel: languageLabelFor(exhibit.audioLanguage),
|
languageLabel: languageLabelFor(exhibit.audioLanguage),
|
||||||
audioStatus,
|
audioStatus,
|
||||||
audioStatusText: hasPlayableAudio ? '可播放' : '图文讲解',
|
audioStatusText: hasPlayableAudio ? '可播放' : exhibit.audioUnavailableReason || '当前语言暂无语音讲解',
|
||||||
playTargetType: exhibit.playTargetType,
|
playTargetType: exhibit.playTargetType,
|
||||||
playTargetId: exhibit.playTargetId,
|
playTargetId: exhibit.playTargetId,
|
||||||
badges: buildBadges(exhibit),
|
badges: buildBadges(exhibit),
|
||||||
@@ -168,6 +171,14 @@ export const toExplainExhibitViewModel = (exhibit: MuseumExhibit): ExplainExhibi
|
|||||||
|
|
||||||
export const toExplainDetailPageViewModel = (exhibit: MuseumExhibit): ExplainDetailPageViewModel => {
|
export const toExplainDetailPageViewModel = (exhibit: MuseumExhibit): ExplainDetailPageViewModel => {
|
||||||
const hasPlayableAudio = isAudioReady(exhibit)
|
const hasPlayableAudio = isAudioReady(exhibit)
|
||||||
|
const linkedExhibitCount = typeof exhibit.linkedExhibitCount === 'number'
|
||||||
|
? exhibit.linkedExhibitCount
|
||||||
|
: exhibit.linkedExhibits?.length
|
||||||
|
const linkedExhibitStatusText = linkedExhibitCount
|
||||||
|
? exhibit.isSharedStop
|
||||||
|
? `${linkedExhibitCount} 件展品共用此讲解点`
|
||||||
|
: `关联 ${linkedExhibitCount} 件展品`
|
||||||
|
: undefined
|
||||||
const metadataLines = [
|
const metadataLines = [
|
||||||
exhibit.size ? `展品编号:${exhibit.size}` : '',
|
exhibit.size ? `展品编号:${exhibit.size}` : '',
|
||||||
exhibit.year ? `年代:${exhibit.year}` : '',
|
exhibit.year ? `年代:${exhibit.year}` : '',
|
||||||
@@ -204,9 +215,12 @@ export const toExplainDetailPageViewModel = (exhibit: MuseumExhibit): ExplainDet
|
|||||||
playTargetType: exhibit.playTargetType,
|
playTargetType: exhibit.playTargetType,
|
||||||
playTargetId: exhibit.playTargetId,
|
playTargetId: exhibit.playTargetId,
|
||||||
hasText: exhibit.audioHasText,
|
hasText: exhibit.audioHasText,
|
||||||
statusText: hasPlayableAudio ? '可播放' : '图文讲解'
|
statusText: hasPlayableAudio ? '可播放' : '当前语言暂无语音讲解'
|
||||||
},
|
},
|
||||||
imageStatus: exhibit.imageStatus,
|
imageStatus: exhibit.imageStatus,
|
||||||
|
linkedExhibitCount,
|
||||||
|
isSharedStop: exhibit.isSharedStop,
|
||||||
|
linkedExhibitStatusText,
|
||||||
linkedExhibits: exhibit.linkedExhibits,
|
linkedExhibits: exhibit.linkedExhibits,
|
||||||
chapters: metadataLines.length
|
chapters: metadataLines.length
|
||||||
? metadataLines.map((line, index) => ({
|
? metadataLines.map((line, index) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user