Files
frontend-miniapp/src/pages/exhibit/detail.vue
2026-06-12 09:25:22 +08:00

315 lines
7.3 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<GuidePageFrame
:active-tab="activeTopTab"
variant="static"
@tab-change="handleTopTabChange"
>
<view class="detail-page">
<scroll-view class="content" scroll-y>
<!-- 展品图片 -->
<view class="exhibit-hero">
<image class="hero-image" :src="exhibit.image" mode="aspectFill" />
<view class="audio-control" @tap="handlePlayAudio">
<text class="audio-icon">{{ isPlaying ? '⏸' : '▶' }}</text>
</view>
</view>
<!-- 展品信息 -->
<view class="exhibit-info">
<text class="exhibit-title">{{ exhibit.name }}</text>
<text v-if="exhibit.artist" class="exhibit-artist">{{ exhibit.artist }}</text>
<view class="info-grid">
<view v-if="exhibit.year" class="info-item">
<text class="info-label">创作年代</text>
<text class="info-value">{{ exhibit.year }}</text>
</view>
<view v-if="exhibit.material" class="info-item">
<text class="info-label">材质</text>
<text class="info-value">{{ exhibit.material }}</text>
</view>
<view v-if="exhibit.size" class="info-item">
<text class="info-label">尺寸</text>
<text class="info-value">{{ exhibit.size }}</text>
</view>
<view v-if="exhibit.hall" class="info-item">
<text class="info-label">展厅位置</text>
<text class="info-value">{{ exhibit.hall }}</text>
</view>
</view>
<view class="description-section">
<text class="section-title">作品介绍</text>
<text class="description-text">{{ exhibit.description }}</text>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="action-bar">
<view class="action-btn-group">
<view class="action-btn" @tap="handleNavigate">
<text class="btn-icon">🗺</text>
<text class="btn-text">查看位置</text>
</view>
<view class="action-btn" @tap="handleCollect">
<text class="btn-icon">{{ isCollected ? '❤️' : '🤍' }}</text>
<text class="btn-text">收藏</text>
</view>
<view class="action-btn" @tap="handleShare">
<text class="btn-icon">📤</text>
<text class="btn-text">分享</text>
</view>
</view>
</view>
</view>
</GuidePageFrame>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
import {
explainUseCase
} from '@/usecases/explainUseCase'
import {
toExplainDetailViewModel,
type ExplainDetailViewModel
} from '@/view-models/explainViewModels'
import {
isGuideTopTab,
navigateToGuideTopTab,
type GuideTopTab
} from '@/utils/guideTopTabs'
const exhibit = ref<ExplainDetailViewModel>({
id: '',
name: '展项内容',
hall: '展厅位置待补充',
image: '/static/exhibit-placeholder.jpg',
description: '该展项介绍待正式内容库补充。',
hasAudio: false,
audioUnavailableReason: '正式讲解音频尚未接入媒体仓储'
})
const isPlaying = ref(false)
const isCollected = ref(false)
const activeTopTab = ref<GuideTopTab>('guide')
onLoad(async (options: any) => {
if (options.id) {
const exhibitData = await explainUseCase.getExhibitById(options.id)
if (exhibitData) {
exhibit.value = toExplainDetailViewModel(exhibitData)
}
}
const tab = Array.isArray(options.tab) ? options.tab[0] : options.tab
if (isGuideTopTab(tab)) {
activeTopTab.value = tab
}
})
const handlePlayAudio = async () => {
const selection = exhibit.value.id
? await explainUseCase.selectAudioForExhibit(exhibit.value.id)
: null
uni.showToast({
title: selection?.unavailableMessage || exhibit.value.audioUnavailableReason || '该展项暂无讲解音频',
icon: 'none'
})
}
const handleNavigate = () => {
if (!exhibit.value.poiId) {
uni.showToast({
title: '该展项暂无三维位置数据',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/route/detail?facilityId=${encodeURIComponent(exhibit.value.poiId)}&target=${encodeURIComponent(exhibit.value.name)}&state=preview`
})
}
const handleCollect = () => {
isCollected.value = !isCollected.value
uni.showToast({
title: isCollected.value ? '已收藏' : '已取消收藏',
icon: 'none'
})
}
const handleShare = () => {
uni.showShareMenu()
}
const handleTopTabChange = (tab: GuideTopTab) => {
navigateToGuideTopTab(tab)
}
</script>
<style scoped lang="scss">
.detail-page {
width: 100%;
height: 100%;
background-color: var(--museum-bg-light);
display: flex;
flex-direction: column;
}
.content {
flex: 1;
overflow-y: auto;
}
.exhibit-hero {
position: relative;
width: 100%;
height: 450px;
background-color: var(--museum-bg-map);
}
.hero-image {
width: 100%;
height: 100%;
}
.audio-control {
position: absolute;
bottom: 20px;
right: 20px;
width: 64px;
height: 64px;
background-color: var(--museum-accent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
cursor: pointer;
transition: all 0.3s;
}
.audio-control:active {
transform: scale(0.95);
}
.audio-icon {
font-size: 28px;
color: var(--museum-text-primary);
}
.exhibit-info {
padding: 20px 16px 32px;
background-color: var(--museum-bg-surface);
}
.exhibit-title {
font-size: 26px;
font-weight: 700;
color: var(--museum-text-primary);
margin-bottom: 6px;
display: block;
line-height: 1.3;
}
.exhibit-artist {
font-size: 15px;
color: var(--museum-text-secondary);
margin-bottom: 20px;
display: block;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 16px;
margin-bottom: 20px;
padding: 16px;
background-color: var(--museum-bg-light);
border-radius: var(--radius-card);
}
.info-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.info-label {
font-size: 12px;
color: var(--museum-text-disabled);
font-weight: 500;
}
.info-value {
font-size: 14px;
color: var(--museum-text-primary);
font-weight: 400;
}
.description-section {
margin-top: 20px;
}
.section-title {
font-size: 17px;
font-weight: 600;
color: var(--museum-text-primary);
margin-bottom: 10px;
display: block;
}
.description-text {
font-size: 14px;
line-height: 1.7;
color: var(--museum-text-secondary);
}
.action-bar {
background-color: var(--museum-bg-surface);
border-top: 1px solid var(--museum-border-light);
padding: 8px 16px;
padding-bottom: calc(8px + env(safe-area-inset-bottom));
}
.action-btn-group {
display: flex;
gap: 8px;
}
.action-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 10px 8px;
background-color: var(--museum-bg-light);
border-radius: var(--radius-button);
cursor: pointer;
transition: all 0.2s;
}
.action-btn:active {
background-color: var(--museum-border-light);
transform: scale(0.98);
}
.btn-icon {
font-size: 22px;
}
.btn-text {
font-size: 11px;
color: var(--museum-text-secondary);
font-weight: 500;
}
</style>