| | |
| | | <view class="container"> |
| | | <!-- 状态栏占位 --> |
| | | <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view> |
| | | |
| | | |
| | | <!-- 导航栏 --> |
| | | <view class="nav-bar"> |
| | | <image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image> |
| | |
| | | |
| | | <!-- 视频播放器区域 --> |
| | | <view v-if="isVideoType" class="video-container"> |
| | | <video |
| | | class="video-player" |
| | | :src="detail.videoUrl" |
| | | controls |
| | | :autoplay="false" |
| | | ></video> |
| | | <video class="video-player" :src="detail.videoUrl" controls :autoplay="false"></video> |
| | | </view> |
| | | |
| | | <!-- 图文封面区域 --> |
| | |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { onLoad } from '@dcloudio/uni-app' |
| | | import { getHealthPopularizationDetail } from './service' |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { onLoad } from '@dcloudio/uni-app' |
| | | import { getHealthPopularizationDetail } from './service' |
| | | |
| | | interface PopularizationDetail { |
| | | id?: number | null |
| | | title: string |
| | | coverImage: string |
| | | contentType?: number | null |
| | | contentTypeText: string |
| | | content: string |
| | | videoUrl: string |
| | | videoFileName: string |
| | | } |
| | | |
| | | const statusBarHeight = ref(0) |
| | | const loading = ref(false) |
| | | const popularizationId = ref('') |
| | | const detail = ref<PopularizationDetail>(createDefaultDetail()) |
| | | |
| | | /** |
| | | * 创建健康科普详情页默认数据,避免接口返回前页面闪空。 |
| | | */ |
| | | function createDefaultDetail(): PopularizationDetail { |
| | | return { |
| | | id: null, |
| | | title: '', |
| | | coverImage: '', |
| | | contentType: null, |
| | | contentTypeText: '', |
| | | content: '', |
| | | videoUrl: '', |
| | | videoFileName: '', |
| | | interface PopularizationDetail { |
| | | id ?: number | null |
| | | title : string |
| | | coverImage : string |
| | | contentType ?: number | null |
| | | contentTypeText : string |
| | | content : string |
| | | videoUrl : string |
| | | videoFileName : string |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据内容类型判断当前详情是否属于视频详情页。 |
| | | */ |
| | | const isVideoType = computed(() => Number(detail.value.contentType) === 2) |
| | | const statusBarHeight = ref(0) |
| | | const loading = ref(false) |
| | | const popularizationId = ref('') |
| | | const detail = ref<PopularizationDetail>(createDefaultDetail()) |
| | | |
| | | /** |
| | | * 将富文本正文转换为 `rich-text` 可直接消费的节点字符串。 |
| | | */ |
| | | const contentNodes = computed(() => buildRichTextNodes(detail.value.content, '暂无详情内容')) |
| | | |
| | | /** |
| | | * 初始化导航栏所需的状态栏高度。 |
| | | */ |
| | | onMounted(() => { |
| | | uni.getSystemInfo({ |
| | | success: (res) => { |
| | | statusBarHeight.value = res.statusBarHeight || 0 |
| | | /** |
| | | * 创建健康科普详情页默认数据,避免接口返回前页面闪空。 |
| | | */ |
| | | function createDefaultDetail() : PopularizationDetail { |
| | | return { |
| | | id: null, |
| | | title: '', |
| | | coverImage: '', |
| | | contentType: null, |
| | | contentTypeText: '', |
| | | content: '', |
| | | videoUrl: '', |
| | | videoFileName: '', |
| | | } |
| | | }) |
| | | }) |
| | | |
| | | /** |
| | | * 页面加载时读取科普主键并拉取详情接口。 |
| | | */ |
| | | onLoad((options) => { |
| | | popularizationId.value = typeof options?.id === 'string' ? options.id : '' |
| | | loadDetail() |
| | | }) |
| | | |
| | | /** |
| | | * 拉取健康科普详情接口并更新页面状态。 |
| | | */ |
| | | async function loadDetail() { |
| | | if (!popularizationId.value) { |
| | | detail.value = createDefaultDetail() |
| | | return |
| | | } |
| | | loading.value = true |
| | | try { |
| | | const response = await getHealthPopularizationDetail(popularizationId.value) |
| | | if (response?.code !== 0 || !response.data) { |
| | | showLoadFailedToast() |
| | | |
| | | /** |
| | | * 根据内容类型判断当前详情是否属于视频详情页。 |
| | | */ |
| | | const isVideoType = computed(() => Number(detail.value.contentType) === 2) |
| | | |
| | | /** |
| | | * 将富文本正文转换为 `rich-text` 可直接消费的节点字符串。 |
| | | */ |
| | | const contentNodes = computed(() => buildRichTextNodes(detail.value.content, '暂无详情内容')) |
| | | |
| | | /** |
| | | * 初始化导航栏所需的状态栏高度。 |
| | | */ |
| | | onMounted(() => { |
| | | uni.getSystemInfo({ |
| | | success: (res) => { |
| | | statusBarHeight.value = res.statusBarHeight || 0 |
| | | } |
| | | }) |
| | | }) |
| | | |
| | | /** |
| | | * 页面加载时读取科普主键并拉取详情接口。 |
| | | */ |
| | | onLoad((options) => { |
| | | popularizationId.value = typeof options?.id === 'string' ? options.id : '' |
| | | loadDetail() |
| | | }) |
| | | |
| | | /** |
| | | * 拉取健康科普详情接口并更新页面状态。 |
| | | */ |
| | | async function loadDetail() { |
| | | if (!popularizationId.value) { |
| | | detail.value = createDefaultDetail() |
| | | return |
| | | } |
| | | // 图文和视频共用一个详情接口,前端只按内容类型切换展示结构。 |
| | | detail.value = { |
| | | ...createDefaultDetail(), |
| | | ...response.data, |
| | | loading.value = true |
| | | try { |
| | | const response = await getHealthPopularizationDetail(popularizationId.value) |
| | | if (response?.code !== 0 || !response.data) { |
| | | showLoadFailedToast() |
| | | return |
| | | } |
| | | // 图文和视频共用一个详情接口,前端只按内容类型切换展示结构。 |
| | | detail.value = { |
| | | ...createDefaultDetail(), |
| | | ...response.data, |
| | | } |
| | | } catch { |
| | | showLoadFailedToast() |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } catch { |
| | | showLoadFailedToast() |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 详情加载失败时给出统一提示。 |
| | | */ |
| | | function showLoadFailedToast() { |
| | | uni.showToast({ |
| | | title: '健康科普详情加载失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | /** |
| | | * 详情加载失败时给出统一提示。 |
| | | */ |
| | | function showLoadFailedToast() { |
| | | uni.showToast({ |
| | | title: '健康科普详情加载失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | |
| | | /** |
| | | * 将富文本内容转换为 `rich-text` 可直接渲染的节点。 |
| | | */ |
| | | function buildRichTextNodes(content?: string, fallbackText?: string) { |
| | | if (typeof content === 'string' && content.trim()) { |
| | | return content |
| | | /** |
| | | * 将富文本内容转换为 `rich-text` 可直接渲染的节点。 |
| | | */ |
| | | function buildRichTextNodes(content ?: string, fallbackText ?: string) { |
| | | if (typeof content === 'string' && content.trim()) { |
| | | return content.replace(/<img/gi, |
| | | '<img style="max-width:100%;height:auto;margin:0 auto;display:block"') |
| | | } |
| | | if (typeof fallbackText === 'string' && fallbackText.trim()) { |
| | | return `<p>${fallbackText.trim()}</p>` |
| | | } |
| | | return '<p></p>' |
| | | } |
| | | if (typeof fallbackText === 'string' && fallbackText.trim()) { |
| | | return `<p>${fallbackText.trim()}</p>` |
| | | } |
| | | return '<p></p>' |
| | | } |
| | | |
| | | /** |
| | | * 返回上一页;若没有历史栈,则回到首页避免停留在空白上下文。 |
| | | */ |
| | | function goBack() { |
| | | const currentPages = getCurrentPages() |
| | | if (currentPages.length > 1) { |
| | | uni.navigateBack() |
| | | return |
| | | /** |
| | | * 返回上一页;若没有历史栈,则回到首页避免停留在空白上下文。 |
| | | */ |
| | | function goBack() { |
| | | const currentPages = getCurrentPages() |
| | | if (currentPages.length > 1) { |
| | | uni.navigateBack() |
| | | return |
| | | } |
| | | uni.switchTab({ |
| | | url: '/pages/home/home' |
| | | }) |
| | | } |
| | | uni.switchTab({ |
| | | url: '/pages/home/home' |
| | | }) |
| | | } |
| | | </script> |
| | | |
| | | <style> |
| | | page, html { |
| | | background-color: #F5F6F8; |
| | | } |
| | | page, |
| | | html { |
| | | background-color: #F5F6F8; |
| | | } |
| | | </style> |
| | | |
| | | <style lang="scss" scoped> |
| | | .container { |
| | | min-height: 100vh; |
| | | background-color: #F5F6F8; |
| | | } |
| | | .container { |
| | | min-height: 100vh; |
| | | background-color: #F5F6F8; |
| | | } |
| | | |
| | | .nav-bar { |
| | | height: 88rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 0 24rpx; |
| | | background-color: #fff; |
| | | position: relative; |
| | | } |
| | | .nav-bar { |
| | | height: 88rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 0 24rpx; |
| | | background-color: #fff; |
| | | position: relative; |
| | | } |
| | | |
| | | .back-icon { |
| | | width: 40rpx; |
| | | height: 40rpx; |
| | | position: absolute; |
| | | left: 24rpx; |
| | | z-index: 10; |
| | | } |
| | | .back-icon { |
| | | width: 40rpx; |
| | | height: 40rpx; |
| | | position: absolute; |
| | | left: 24rpx; |
| | | z-index: 10; |
| | | } |
| | | |
| | | .nav-title { |
| | | flex: 1; |
| | | text-align: center; |
| | | font-size: 36rpx; |
| | | font-weight: 500; |
| | | color: #333; |
| | | } |
| | | .nav-title { |
| | | flex: 1; |
| | | text-align: center; |
| | | font-size: 36rpx; |
| | | font-weight: 500; |
| | | color: #333; |
| | | } |
| | | |
| | | .video-container { |
| | | width: 100%; |
| | | height: 402rpx; |
| | | } |
| | | .video-container { |
| | | width: 100%; |
| | | height: 402rpx; |
| | | } |
| | | |
| | | .video-player { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | .video-player { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .cover-container { |
| | | width: 100%; |
| | | height: 402rpx; |
| | | } |
| | | .cover-container { |
| | | width: 100%; |
| | | height: 402rpx; |
| | | } |
| | | |
| | | .cover-image { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | .cover-image { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .content-area { |
| | | padding: 32rpx; |
| | | background-color: #F5F6F8; |
| | | } |
| | | .content-area { |
| | | padding: 32rpx; |
| | | background-color: #F5F6F8; |
| | | } |
| | | |
| | | .content-title { |
| | | font-size: 32rpx; |
| | | font-weight: 500; |
| | | color: #333; |
| | | line-height: 48rpx; |
| | | } |
| | | .content-title { |
| | | font-size: 32rpx; |
| | | font-weight: 500; |
| | | color: #333; |
| | | line-height: 48rpx; |
| | | } |
| | | |
| | | .rich-content { |
| | | margin-top: 24rpx; |
| | | width: 100%; |
| | | font-size: 28rpx; |
| | | line-height: 1.8; |
| | | color: #333; |
| | | } |
| | | .rich-content { |
| | | margin-top: 24rpx; |
| | | width: 100%; |
| | | font-size: 28rpx; |
| | | line-height: 1.8; |
| | | color: #333; |
| | | } |
| | | |
| | | .status-text { |
| | | margin-top: 24rpx; |
| | | font-size: 26rpx; |
| | | line-height: 40rpx; |
| | | color: #86909C; |
| | | } |
| | | </style> |
| | | .status-text { |
| | | margin-top: 24rpx; |
| | | font-size: 26rpx; |
| | | line-height: 40rpx; |
| | | color: #86909C; |
| | | } |
| | | </style> |