优化首页搜索与讲解详情语言切换
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-09 15:01:33 +08:00
parent 68336fb443
commit eac4b74bdc
2 changed files with 82 additions and 43 deletions

View File

@@ -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 {

View File

@@ -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()}`
} }