This commit is contained in:
@@ -796,11 +796,24 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-back-icon {
|
.home-back-icon {
|
||||||
font-size: 30px;
|
display: flex;
|
||||||
line-height: 30px;
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #151713;
|
color: #151713;
|
||||||
transform: translateX(-1px);
|
}
|
||||||
|
|
||||||
|
.home-back-icon::before {
|
||||||
|
content: '';
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-left: 2px solid currentColor;
|
||||||
|
border-bottom: 2px solid currentColor;
|
||||||
|
transform: translateX(2px) rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-fullscreen-title {
|
.home-fullscreen-title {
|
||||||
|
|||||||
@@ -13,8 +13,6 @@
|
|||||||
class="content"
|
class="content"
|
||||||
:class="{ 'with-fixed-player': isDetailAudioDockVisible || showAudioPlayer }"
|
:class="{ 'with-fixed-player': isDetailAudioDockVisible || showAudioPlayer }"
|
||||||
scroll-y
|
scroll-y
|
||||||
scroll-with-animation
|
|
||||||
:scroll-into-view="scrollTargetId"
|
|
||||||
:show-scrollbar="false"
|
:show-scrollbar="false"
|
||||||
>
|
>
|
||||||
<view class="immersive-hero" :class="{ 'has-real-image': heroImage }">
|
<view class="immersive-hero" :class="{ 'has-real-image': heroImage }">
|
||||||
@@ -44,8 +42,8 @@
|
|||||||
v-for="option in contentLanguageOptions"
|
v-for="option in contentLanguageOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
class="language-anchor-option"
|
class="language-anchor-option"
|
||||||
:class="{ active: activeContentLanguage === option.value }"
|
:class="{ active: selectedAudioLanguage === option.value }"
|
||||||
@tap="handleAnchorTap(option.value)"
|
@tap="handleLanguageChange(option.value)"
|
||||||
>
|
>
|
||||||
<text class="language-anchor-text">{{ option.label }}</text>
|
<text class="language-anchor-text">{{ option.label }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -54,18 +52,9 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="content-panel">
|
<view class="content-panel">
|
||||||
<view
|
<view class="content-section">
|
||||||
v-for="section in contentLanguageOptions"
|
<text class="section-text">{{ currentDetailText }}</text>
|
||||||
:id="section.anchorId"
|
<text v-if="currentDetailTextLoading" class="section-hint">正在加载讲解正文</text>
|
||||||
:key="section.value"
|
|
||||||
class="content-section"
|
|
||||||
>
|
|
||||||
<view class="section-heading">
|
|
||||||
<view class="section-marker" :class="{ english: section.value === 'en-US' }"></view>
|
|
||||||
<text class="section-title">{{ section.heading }}</text>
|
|
||||||
</view>
|
|
||||||
<text class="section-text">{{ detailTextFor(section.value) }}</text>
|
|
||||||
<text v-if="detailTextLoadingByLanguage[section.value]" class="section-hint">正在加载讲解正文</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="visibleLinkedExhibits.length" class="content-section">
|
<view v-if="visibleLinkedExhibits.length" class="content-section">
|
||||||
@@ -157,7 +146,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||||
import type { AudioItem } from '@/components/audio/AudioPlayer.vue'
|
import type { AudioItem } from '@/components/audio/AudioPlayer.vue'
|
||||||
@@ -213,8 +202,6 @@ const detailEntryRequest = ref<{
|
|||||||
targetType?: AudioPlayTargetType
|
targetType?: AudioPlayTargetType
|
||||||
targetId?: string
|
targetId?: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const scrollTargetId = ref('')
|
|
||||||
const activeContentLanguage = ref<AudioLanguage>('zh-CN')
|
|
||||||
const detailTextByLanguage = ref<Record<AudioLanguage, string>>({
|
const detailTextByLanguage = ref<Record<AudioLanguage, string>>({
|
||||||
'zh-CN': defaultDetail.body,
|
'zh-CN': defaultDetail.body,
|
||||||
'en-US': 'English narration text is not available yet.'
|
'en-US': 'English narration text is not available yet.'
|
||||||
@@ -223,26 +210,24 @@ const detailTextLoadingByLanguage = ref<Record<AudioLanguage, boolean>>({
|
|||||||
'zh-CN': false,
|
'zh-CN': false,
|
||||||
'en-US': false
|
'en-US': false
|
||||||
})
|
})
|
||||||
|
const detailTextLoadedByLanguage = ref<Record<AudioLanguage, boolean>>({
|
||||||
|
'zh-CN': false,
|
||||||
|
'en-US': false
|
||||||
|
})
|
||||||
// 讲解详情页位置入口暂未开放,避免误导用户进入位置预览流程。
|
// 讲解详情页位置入口暂未开放,避免误导用户进入位置预览流程。
|
||||||
const isLocationActionVisible = false
|
const isLocationActionVisible = false
|
||||||
|
|
||||||
const contentLanguageOptions: Array<{
|
const contentLanguageOptions: Array<{
|
||||||
value: AudioLanguage
|
value: AudioLanguage
|
||||||
label: string
|
label: string
|
||||||
heading: string
|
|
||||||
anchorId: string
|
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
value: 'zh-CN',
|
value: 'zh-CN',
|
||||||
label: '中文',
|
label: '中文'
|
||||||
heading: '中文讲解',
|
|
||||||
anchorId: 'zh-section'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'en-US',
|
value: 'en-US',
|
||||||
label: 'English',
|
label: 'English'
|
||||||
heading: 'English',
|
|
||||||
anchorId: 'en-section'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -315,7 +300,17 @@ const detailAudioLanguageOptions = computed(() => (
|
|||||||
audioLanguageOptions.filter((option) => supportedDetailAudioLanguages.value.includes(option.value))
|
audioLanguageOptions.filter((option) => supportedDetailAudioLanguages.value.includes(option.value))
|
||||||
))
|
))
|
||||||
const isAudioLanguageSwitchVisible = computed(() => detailAudioLanguageOptions.value.length > 1)
|
const isAudioLanguageSwitchVisible = computed(() => detailAudioLanguageOptions.value.length > 1)
|
||||||
|
const isCurrentDetailAudioTarget = computed(() => {
|
||||||
|
const source = globalAudioPlayer.currentSource.value
|
||||||
|
const targetType = exhibit.value.audio.playTargetType || 'ITEM'
|
||||||
|
const targetId = exhibit.value.audio.playTargetId || exhibit.value.id
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
globalAudioPlayer.currentAudio.value
|
||||||
|
&& source?.targetType === targetType
|
||||||
|
&& source.targetId === targetId
|
||||||
|
)
|
||||||
|
})
|
||||||
// 讲解详情页只定位到所属展厅,不再追踪具体展品点位。
|
// 讲解详情页只定位到所属展厅,不再追踪具体展品点位。
|
||||||
const resolveHallGuidePoi = async () => {
|
const resolveHallGuidePoi = async () => {
|
||||||
return guideUseCase.resolveContentLocationPreviewTarget({
|
return guideUseCase.resolveContentLocationPreviewTarget({
|
||||||
@@ -339,6 +334,10 @@ const applyDetailText = (lang: AudioLanguage, text?: string) => {
|
|||||||
...detailTextByLanguage.value,
|
...detailTextByLanguage.value,
|
||||||
[lang]: normalized || fallbackTextForLanguage(lang)
|
[lang]: normalized || fallbackTextForLanguage(lang)
|
||||||
}
|
}
|
||||||
|
detailTextLoadedByLanguage.value = {
|
||||||
|
...detailTextLoadedByLanguage.value,
|
||||||
|
[lang]: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildTextSourceFromViewModel = (viewModel: ExplainDetailPageViewModel) => ({
|
const buildTextSourceFromViewModel = (viewModel: ExplainDetailPageViewModel) => ({
|
||||||
@@ -404,6 +403,16 @@ const loadBilingualDetailText = async (
|
|||||||
void loadFullDetailTextForLanguage(request, nextLang)
|
void loadFullDetailTextForLanguage(request, nextLang)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ensureDetailTextForLanguage = async (lang: AudioLanguage) => {
|
||||||
|
if (detailTextLoadedByLanguage.value[lang] || detailTextLoadingByLanguage.value[lang]) return
|
||||||
|
if (!detailEntryRequest.value) {
|
||||||
|
applyDetailText(lang)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadFullDetailTextForLanguage(detailEntryRequest.value, lang)
|
||||||
|
}
|
||||||
|
|
||||||
const loadExplainDetail = async (
|
const loadExplainDetail = async (
|
||||||
request: NonNullable<typeof detailEntryRequest.value>,
|
request: NonNullable<typeof detailEntryRequest.value>,
|
||||||
lang: AudioLanguage
|
lang: AudioLanguage
|
||||||
@@ -461,22 +470,37 @@ onLoad(async (options: any = {}) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleAnchorTap = async (lang: AudioLanguage) => {
|
|
||||||
activeContentLanguage.value = lang
|
|
||||||
scrollTargetId.value = ''
|
|
||||||
await nextTick()
|
|
||||||
scrollTargetId.value = lang === 'en-US' ? 'en-section' : 'zh-section'
|
|
||||||
}
|
|
||||||
|
|
||||||
const detailTextFor = (lang: AudioLanguage) => {
|
const detailTextFor = (lang: AudioLanguage) => {
|
||||||
return detailTextByLanguage.value[lang] || fallbackTextForLanguage(lang)
|
return detailTextByLanguage.value[lang] || fallbackTextForLanguage(lang)
|
||||||
}
|
}
|
||||||
|
const currentDetailText = computed(() => detailTextFor(selectedAudioLanguage.value))
|
||||||
|
const currentDetailTextLoading = computed(() => detailTextLoadingByLanguage.value[selectedAudioLanguage.value])
|
||||||
|
|
||||||
|
const replaceDetailRouteLanguage = (lang: AudioLanguage) => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
|
const nextHash = `#${buildDetailRoute(lang)}`
|
||||||
|
if (window.location.hash === nextHash) return
|
||||||
|
|
||||||
|
window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}${nextHash}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLanguageChange = async (lang: AudioLanguage, options: { syncAudio?: boolean } = {}) => {
|
||||||
|
if (lang === selectedAudioLanguage.value) return
|
||||||
|
|
||||||
|
const shouldSyncAudio = options.syncAudio !== false && isCurrentDetailAudioTarget.value
|
||||||
|
selectedAudioLanguage.value = lang
|
||||||
|
replaceDetailRouteLanguage(lang)
|
||||||
|
void ensureDetailTextForLanguage(lang)
|
||||||
|
|
||||||
|
if (shouldSyncAudio && globalAudioPlayer.currentSource.value?.lang !== lang) {
|
||||||
|
await globalAudioPlayer.switchLanguage(lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDetailAudioLanguageChange = (lang: AudioLanguage) => {
|
const handleDetailAudioLanguageChange = (lang: AudioLanguage) => {
|
||||||
if (lang === selectedAudioLanguage.value) return
|
|
||||||
if (!supportedDetailAudioLanguages.value.includes(lang)) return
|
if (!supportedDetailAudioLanguages.value.includes(lang)) return
|
||||||
|
void handleLanguageChange(lang)
|
||||||
selectedAudioLanguage.value = lang
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -492,11 +516,13 @@ watch(
|
|||||||
&& source.targetId === targetId
|
&& source.targetId === targetId
|
||||||
) {
|
) {
|
||||||
selectedAudioLanguage.value = lang
|
selectedAudioLanguage.value = lang
|
||||||
|
replaceDetailRouteLanguage(lang)
|
||||||
|
void ensureDetailTextForLanguage(lang)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const buildDetailRoute = () => {
|
function buildDetailRoute(lang: AudioLanguage = selectedAudioLanguage.value) {
|
||||||
const request = detailEntryRequest.value
|
const request = detailEntryRequest.value
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
id: request?.exhibitId || exhibit.value.id,
|
id: request?.exhibitId || exhibit.value.id,
|
||||||
@@ -508,8 +534,8 @@ const buildDetailRoute = () => {
|
|||||||
if (currentAudioTarget.value.targetId) {
|
if (currentAudioTarget.value.targetId) {
|
||||||
params.set('targetId', currentAudioTarget.value.targetId)
|
params.set('targetId', currentAudioTarget.value.targetId)
|
||||||
}
|
}
|
||||||
if (selectedAudioLanguage.value) {
|
if (lang) {
|
||||||
params.set('lang', selectedAudioLanguage.value)
|
params.set('lang', lang)
|
||||||
}
|
}
|
||||||
return `/pages/exhibit/detail?${params.toString()}`
|
return `/pages/exhibit/detail?${params.toString()}`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user