fjrcloud-community-app/pages/sub/notice/detail.vue

418 lines
9.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!-- 通知公告详情页 -->
<template>
<s-layout title="通知公告" navbar="inner" color="#333333">
<!-- 渐变背景 -->
<view class="gradient-bg"></view>
<!-- 固定头部区域 -->
<view class="detail-header">
<view class="header-inner">
<!-- 文章标题 -->
<view class="article-title">
<text class="title-text">{{ noticeInfo.title }}</text>
</view>
<!-- 发布信息 -->
<view class="publish-info">
<view class="publisher">
<image class="publisher-avatar" src="/static/img/person.png" mode="aspectFill" />
<text class="publisher-name">{{ noticeInfo.publisher }}</text>
</view>
<text class="publish-date">{{ noticeInfo.publishDate ? sheep.$helper.timeFormat(noticeInfo.publishDate, 'yyyy/mm/dd hh:MM:ss') : '' }}</text>
</view>
<!-- 分割线 -->
<view class="divider"></view>
</view>
</view>
<!-- 富文本内容 - 独立滚动区域 -->
<scroll-view class="article-scroll" scroll-y :style="{ height: contentScrollHeight + 'px', top: contentScrollTop + 'px' }">
<view class="article-content">
<view class="article-inner">
<mp-html :content="noticeInfo.content"></mp-html>
</view>
</view>
<!-- 占位防止内容被底部附件遮挡 -->
<view class="bottom-placeholder" v-if="noticeInfo.attachmentList && noticeInfo.attachmentList.length > 0"></view>
</scroll-view>
<!-- 附件列表 - 动态高度,随内容撑开 -->
<view class="attachment-section" v-if="noticeInfo.attachmentList && noticeInfo.attachmentList.length > 0">
<view class="attachment-inner">
<view
class="attachment-item"
v-for="(item, index) in noticeInfo.attachmentList"
:key="index"
@tap="downloadAttachment(item)"
>
<view class="attachment-left">
<image class="attachment-icon" :src="getAttachmentIcon(item.type)" mode="aspectFit" />
<text class="attachment-name">{{ item.name }}</text>
</view>
<image class="download-icon" src="/static/img/right-icon.png" mode="aspectFit" />
</view>
</view>
</view>
</s-layout>
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance, watch } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import NoticeApi from '@/sheep/api/community/notice';
import sheep from '@/sheep';
// 富文本滚动区域高度单位px
const contentScrollHeight = ref(0);
// 富文本滚动区域距离顶部的距离单位px
const contentScrollTop = ref(0);
// 公告信息
const noticeInfo = ref({
title: '',
publisher: '',
publishDate: '',
content: '',
attachmentList: []
});
// 页面加载
onLoad((options) => {
if (options.id) {
loadNoticeDetail(options.id);
}
});
// 计算scroll-view高度和位置
const calcScrollHeight = () => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
const query = uni.createSelectorQuery().in(instance);
// 获取头部高度和位置
query.select('.detail-header').boundingClientRect();
// 获取附件区域高度(如果存在)
query.select('.attachment-section').boundingClientRect();
query.exec((res) => {
const headerRect = res[0];
const attachmentRect = res[1];
let attachmentHeight = 0;
if (attachmentRect && attachmentRect.height > 0) {
attachmentHeight = attachmentRect.height;
}
if (headerRect) {
// scroll-view距离顶部 = 头部高度 + 头部距离顶部距离(已包含状态栏和导航栏高度)
contentScrollTop.value = headerRect.height + headerRect.top;
// scroll-view高度 = 屏幕可用高度 - scroll-view距离顶部 - 附件高度 - 底部安全区
const safeBottom = sysInfo.safeAreaInsets?.bottom || 0;
contentScrollHeight.value = sysInfo.windowHeight - contentScrollTop.value - attachmentHeight - safeBottom;
}
});
});
};
// 渲染完成后计算
onMounted(() => {
calcScrollHeight();
});
// 数据加载完成后重新计算(附件异步加载)
watch(() => noticeInfo.value.attachmentList, () => {
setTimeout(calcScrollHeight, 100);
}, { deep: true });
// 加载公告详情
async function loadNoticeDetail(id) {
const { code, data } = await NoticeApi.getDetail(id);
if (code === 0 && data) {
noticeInfo.value = {
title: data.title || '',
publisher: data.publisher || '',
publishDate: data.publishTime || '',
content: data.content || '',
attachmentList: (data.attachmentList || []).map((item) => ({
name: item.name,
url: item.url,
type: getFileType(item.name),
})),
};
// 数据加载后重新计算scroll-view高度
setTimeout(calcScrollHeight, 100);
}
}
// 根据文件名判断文件类型
function getFileType(fileName) {
if (!fileName) return 'file';
const ext = fileName.split('.').pop().toLowerCase();
const typeMap = {
png: 'image',
jpg: 'image',
jpeg: 'image',
gif: 'image',
xls: 'excel',
xlsx: 'excel',
pdf: 'pdf',
doc: 'word',
docx: 'word',
};
return typeMap[ext] || 'file';
}
// 获取附件图标
function getAttachmentIcon(type) {
const iconMap = {
image: '/static/img/eli-icon1.png',
excel: '/static/img/eli-icon2.png',
pdf: '/static/img/eli-icon2.png',
word: '/static/img/eli-icon2.png'
};
return iconMap[type] || '/static/img/eli-icon2.png';
}
// 下载附件
function downloadAttachment(item) {
if (!item.url) {
uni.showToast({
title: '附件链接不存在',
icon: 'none'
});
return;
}
uni.showLoading({
title: '下载中...'
});
uni.downloadFile({
url: item.url,
success: (res) => {
if (res.statusCode === 200) {
// 保存到本地
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
uni.hideLoading();
uni.showToast({
title: '下载成功',
icon: 'success'
});
// 打开文件
uni.openDocument({
filePath: saveRes.savedFilePath,
showMenu: true,
success: () => {
console.log('打开文档成功');
},
fail: () => {
uni.showToast({
title: '无法打开该文件',
icon: 'none'
});
}
});
},
fail: () => {
uni.hideLoading();
uni.showToast({
title: '保存失败',
icon: 'none'
});
}
});
} else {
uni.hideLoading();
uni.showToast({
title: '下载失败',
icon: 'none'
});
}
},
fail: () => {
uni.hideLoading();
uni.showToast({
title: '下载失败',
icon: 'none'
});
}
});
}
</script>
<style lang="scss" scoped>
/* 渐变背景 */
.gradient-bg {
position: fixed;
top: 0;
left: 0;
width: 750rpx;
height: 660rpx;
background: linear-gradient(180deg, #F8EDE8 0%, #F5F5F5 50%);
z-index: 0;
pointer-events: none;
}
/* 页面容器 */
.notice-detail-page {
position: relative;
z-index: 1;
}
/* 固定头部区域(标题+发布信息) */
.detail-header {
position: fixed;
left: 0;
width: 750rpx;
z-index: 10;
background: transparent;
}
.header-inner {
padding: 0 32rpx;
}
/* 底部占位 - 根据附件数量动态调整 */
.bottom-placeholder {
height: 40rpx;
}
/* 文章标题 */
.article-title {
padding: 32rpx 0 24rpx;
.title-text {
font-size: 36rpx;
font-weight: 600;
color: #333333;
line-height: 1.5;
}
}
/* 发布信息 */
.publish-info {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 24rpx;
.publisher {
display: flex;
align-items: center;
.publisher-avatar {
width: 48rpx;
height: 48rpx;
// border-radius: 50%;
margin-right: 16rpx;
}
.publisher-name {
font-size: 28rpx;
color: #666666;
}
}
.publish-date {
font-size: 26rpx;
color: #999999;
}
}
/* 分割线 */
.divider {
height: 1rpx;
background-color: #E5E5E5;
margin-bottom: 32rpx;
}
/* 富文本滚动区域 - 高度由JS动态绑定 */
.article-scroll {
position: fixed;
left: 0;
width: 100%;
z-index: 5;
}
/* 富文本内容 - 圆角背景卡片 */
.article-content {
margin: 0 32rpx;
border-radius: 24rpx;
// background-color: #FFFFFF;
}
/* 富文本内容内层 - 控制padding */
.article-inner {
// padding: 32rpx;
:deep(p) {
font-size: 28rpx;
color: #333333;
line-height: 1.8;
margin-bottom: 16rpx;
}
:deep(img) {
width: 100%;
border-radius: 16rpx;
margin: 24rpx 0;
}
}
/* 附件区域 - 动态高度最多显示3个附件 */
.attachment-section {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
max-height: 360rpx;
background: #FFFFFF;
box-shadow: 0rpx -8rpx 64rpx 0rpx rgba(0, 0, 0, 0.16);
border-radius: 24rpx 24rpx 0 0;
overflow-y: auto;
z-index: 9999;
}
.attachment-inner {
padding: 24rpx 32rpx;
}
.attachment-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
&:not(:last-child) {
border-bottom: 1rpx solid #F0F0F0;
}
.attachment-left {
display: flex;
align-items: center;
flex: 1;
.attachment-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.attachment-name {
font-size: 28rpx;
color: #333333;
}
}
.download-icon {
width: 32rpx;
height: 32rpx;
}
}
</style>