refactor(pages): 使用 flex 布局替代动态计算滚动高度

main
cr 2026-04-26 11:01:55 +08:00
parent 203fd7a1a6
commit b3fc83af1b
3 changed files with 130 additions and 228 deletions

View File

@ -11,15 +11,15 @@
<!-- Tab 切换栏 --> <!-- Tab 切换栏 -->
<view class="tab-bar"> <view class="tab-bar">
<view <view
class="tab-item" class="tab-item"
:class="{ active: currentTab === 'basic' }" :class="{ active: currentTab === 'basic' }"
@tap="currentTab = 'basic'" @tap="currentTab = 'basic'"
> >
<text class="tab-text">基本信息</text> <text class="tab-text">基本信息</text>
</view> </view>
<view <view
class="tab-item" class="tab-item"
:class="{ active: currentTab === 'intro' }" :class="{ active: currentTab === 'intro' }"
@tap="currentTab = 'intro'" @tap="currentTab = 'intro'"
> >
@ -27,8 +27,8 @@
</view> </view>
</view> </view>
<!-- 内容区域 - 独立滚动 --> <!-- 内容区域 - 滚动 -->
<scroll-view class="detail-scroll" scroll-y :style="{ height: scrollViewHeight + 'px' }"> <scroll-view class="detail-scroll" scroll-y>
<view class="detail-content"> <view class="detail-content">
<!-- 基本信息 --> <!-- 基本信息 -->
<template v-if="currentTab === 'basic'"> <template v-if="currentTab === 'basic'">
@ -46,9 +46,9 @@
<view class="info-row tag-row"> <view class="info-row tag-row">
<text class="label-text">服务类别</text> <text class="label-text">服务类别</text>
<view class="tag-list"> <view class="tag-list">
<text <text
class="tag-item" class="tag-item"
v-for="(tag, index) in activityInfo.serviceTypes" v-for="(tag, index) in activityInfo.serviceTypes"
:key="index" :key="index"
>{{ tag }}</text> >{{ tag }}</text>
</view> </view>
@ -58,9 +58,9 @@
<view class="info-row tag-row"> <view class="info-row tag-row">
<text class="label-text">服务对象</text> <text class="label-text">服务对象</text>
<view class="tag-list"> <view class="tag-list">
<text <text
class="tag-item target-tag" class="tag-item target-tag"
v-for="(tag, index) in activityInfo.targetAudience" v-for="(tag, index) in activityInfo.targetAudience"
:key="index" :key="index"
>{{ tag }}</text> >{{ tag }}</text>
</view> </view>
@ -119,15 +119,12 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue'; import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app'; import { onLoad } from '@dcloudio/uni-app';
// Tab // Tab
const currentTab = ref('basic'); const currentTab = ref('basic');
// px
const scrollViewHeight = ref(0);
// //
const activityInfo = ref({ const activityInfo = ref({
id: null, id: null,
@ -152,24 +149,6 @@ onLoad((options) => {
} }
}); });
// scroll-view
onMounted(() => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
uni.createSelectorQuery()
.in(instance)
.select('.tab-bar')
.boundingClientRect((rect) => {
if (rect) {
// scroll-view = - tab - tab - (120rpx)
scrollViewHeight.value = sysInfo.windowHeight - rect.height - rect.top - 60;
}
})
.exec();
});
});
// //
async function loadActivityDetail(id) { async function loadActivityDetail(id) {
// TODO: API // TODO: API
@ -224,16 +203,20 @@ function goBack() {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 页面容器 */ /* 页面容器:纵向 flex 布局,减去导航栏高度 */
.activity-detail-page { .activity-detail-page {
position: relative; position: relative;
min-height: 100vh; display: flex;
flex-direction: column;
height: calc(100vh - 176rpx);
overflow: hidden;
} }
/* 顶部Banner轮播 */ /* 顶部Banner轮播 */
.banner-swiper { .banner-swiper {
width: 100%; width: 100%;
height: 400rpx; height: 400rpx;
flex-shrink: 0;
} }
.banner-img { .banner-img {
@ -249,6 +232,7 @@ function goBack() {
background-color: #FFFFFF; background-color: #FFFFFF;
padding: 0 32rpx; padding: 0 32rpx;
border-bottom: 1rpx solid #F0F0F0; border-bottom: 1rpx solid #F0F0F0;
flex-shrink: 0;
.tab-item { .tab-item {
flex: 1; flex: 1;
@ -284,9 +268,12 @@ function goBack() {
} }
} }
/* 内容滚动区域 */ /* 内容滚动区域flex:1 占满剩余空间height:0 是小程序 scroll-view 配合 flex 的关键 */
.detail-scroll { .detail-scroll {
/* 高度由JS动态绑定 */ position: relative;
z-index: 5;
flex: 1;
height: 0;
} }
/* 详情内容 */ /* 详情内容 */
@ -387,7 +374,7 @@ function goBack() {
/* 联系人行 */ /* 联系人行 */
.contact-row { .contact-row {
align-items: center; align-items: center;
&:active { &:active {
opacity: 0.7; opacity: 0.7;
} }
@ -410,9 +397,9 @@ function goBack() {
padding: 32rpx; padding: 32rpx;
} }
/* 底部占位 */ /* 底部占位:基础间距 + 安全区域 */
.bottom-placeholder { .bottom-placeholder {
height: 140rpx; height: calc(140rpx + env(safe-area-inset-bottom));
} }
/* 底部固定按钮栏 */ /* 底部固定按钮栏 */

View File

@ -5,41 +5,41 @@
<!-- 渐变背景 --> <!-- 渐变背景 -->
<view class="gradient-bg"></view> <view class="gradient-bg"></view>
<!-- 固定头部区域标题+发布信息 --> <!-- 头部区域标题+发布信息 -->
<view class="detail-header"> <view class="detail-header">
<view class="header-inner"> <view class="header-inner">
<!-- 标题 --> <!-- 标题 -->
<view class="detail-title"> <view class="detail-title">
<text class="title-text">{{ detailInfo.title }}</text> <text class="title-text">{{ detailInfo.title }}</text>
</view> </view>
<!-- 发布信息 --> <!-- 发布信息 -->
<view class="publish-info"> <view class="publish-info">
<text class="community-name">{{ detailInfo.viewCount || 0 }}次浏览</text> <text class="community-name">{{ detailInfo.viewCount || 0 }}次浏览</text>
<text class="publish-date">{{ detailInfo.publishDate ? sheep.$helper.timeFormat(detailInfo.publishDate, 'yyyy年mm月dd日 hh:MM') : '' }}</text> <text class="publish-date">{{ detailInfo.publishDate ? sheep.$helper.timeFormat(detailInfo.publishDate, 'yyyy年mm月dd日 hh:MM') : '' }}</text>
</view> </view>
<!-- 分割线 --> <!-- 分割线 -->
<view class="divider"></view> <view class="divider"></view>
</view>
</view>
<!-- 富文本内容 - 固定滚动区域 -->
<scroll-view class="content-scroll" scroll-y :style="{ height: scrollHeight + 'px', top: scrollTop + 'px' }">
<view class="content-card">
<view class="content-inner">
<mp-html :content="detailInfo.content"></mp-html>
</view> </view>
</view> </view>
<!-- 底部占位 -->
<view class="bottom-placeholder"></view> <!-- 富文本内容 - 滚动区域 -->
</scroll-view> <scroll-view class="content-scroll" scroll-y>
<view class="content-card">
<view class="content-inner">
<mp-html :content="detailInfo.content"></mp-html>
</view>
</view>
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
</scroll-view>
</view> </view>
</s-layout> </s-layout>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue'; import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app'; import { onLoad } from '@dcloudio/uni-app';
import KnowledgeApi from '@/sheep/api/community/knowledge'; import KnowledgeApi from '@/sheep/api/community/knowledge';
import sheep from '@/sheep'; import sheep from '@/sheep';
@ -52,11 +52,6 @@ const detailInfo = ref({
content: '', content: '',
}); });
//
const scrollHeight = ref(0);
//
const scrollTop = ref(0);
// //
onLoad((options) => { onLoad((options) => {
if (options.id) { if (options.id) {
@ -64,30 +59,6 @@ onLoad((options) => {
} }
}); });
// scroll-view
const calcScrollHeight = () => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
const query = uni.createSelectorQuery().in(instance);
//
query.select('.detail-header').boundingClientRect();
query.exec((res) => {
const headerRect = res[0];
if (headerRect) {
const safeBottom = sysInfo.safeAreaInsets?.bottom || 0;
scrollTop.value = headerRect.height + headerRect.top;
scrollHeight.value = sysInfo.windowHeight - scrollTop.value - safeBottom;
}
});
});
};
onMounted(() => {
calcScrollHeight();
});
// //
async function loadDetail(id) { async function loadDetail(id) {
const { code, data } = await KnowledgeApi.getDetail(id); const { code, data } = await KnowledgeApi.getDetail(id);
@ -98,17 +69,18 @@ async function loadDetail(id) {
publishDate: data.createTime || '', publishDate: data.createTime || '',
content: data.content || '', content: data.content || '',
}; };
setTimeout(calcScrollHeight, 100);
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 页面容器 */ /* 页面容器:纵向 flex 布局,减去导航栏高度 */
.detail-page { .detail-page {
position: relative; position: relative;
z-index: 1; display: flex;
flex-direction: column;
height: calc(100vh - 176rpx);
overflow: hidden;
} }
/* 渐变背景 */ /* 渐变背景 */
@ -123,13 +95,11 @@ async function loadDetail(id) {
pointer-events: none; pointer-events: none;
} }
/* 固定头部区域 */ /* 头部区域:正常文档流 */
.detail-header { .detail-header {
position: fixed; position: relative;
left: 0;
width: 100%;
z-index: 10; z-index: 10;
background: transparent; flex-shrink: 0;
} }
.header-inner { .header-inner {
@ -177,12 +147,12 @@ async function loadDetail(id) {
margin-bottom: 32rpx; margin-bottom: 32rpx;
} }
/* 内容滚动区域 - 固定定位 */ /* 内容滚动区域flex:1 占满剩余空间height:0 是小程序 scroll-view 配合 flex 的关键 */
.content-scroll { .content-scroll {
position: fixed; position: relative;
left: 0;
width: 100%;
z-index: 5; z-index: 5;
flex: 1;
height: 0;
} }
/* 内容卡片 */ /* 内容卡片 */
@ -209,8 +179,8 @@ async function loadDetail(id) {
} }
} }
/* 底部占位 */ /* 底部占位:基础间距 + 安全区域 */
.bottom-placeholder { .bottom-placeholder {
height: 40rpx; height: calc(40rpx + env(safe-area-inset-bottom));
} }
</style> </style>

View File

@ -4,72 +4,67 @@
<view class="detail-page"> <view class="detail-page">
<!-- 渐变背景 --> <!-- 渐变背景 -->
<view class="gradient-bg"></view> <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="detail-header">
<view class="publisher"> <view class="header-inner">
<image class="publisher-avatar" src="/static/img/person.png" mode="aspectFill" /> <!-- 文章标题 -->
<text class="publisher-name">{{ noticeInfo.publisher }}</text> <view class="article-title">
<text class="title-text">{{ noticeInfo.title }}</text>
</view> </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 class="publish-info">
</view> <view class="publisher">
</view> <image class="publisher-avatar" src="/static/img/person.png" mode="aspectFill" />
<text class="publisher-name">{{ noticeInfo.publisher }}</text>
<!-- 富文本内容 - 独立滚动区域 --> </view>
<scroll-view class="article-scroll" scroll-y :style="{ height: contentScrollHeight + 'px', top: contentScrollTop + 'px' }"> <text class="publish-date">{{ noticeInfo.publishDate ? sheep.$helper.timeFormat(noticeInfo.publishDate, 'yyyy/mm/dd hh:MM:ss') : '' }}</text>
<view class="article-content"> </view>
<view class="article-inner">
<mp-html :content="noticeInfo.content"></mp-html> <!-- 分割线 -->
</view> <view class="divider"></view>
</view> </view>
<!-- 占位防止内容被底部附件遮挡 --> </view>
<view class="bottom-placeholder" v-if="noticeInfo.attachmentList && noticeInfo.attachmentList.length > 0"></view>
</scroll-view> <!-- 富文本内容 - 滚动区域 -->
<scroll-view class="article-scroll" scroll-y>
<!-- 附件列表 - 动态高度随内容撑开 --> <view class="article-content">
<view class="attachment-section" v-if="noticeInfo.attachmentList && noticeInfo.attachmentList.length > 0"> <view class="article-inner">
<view class="attachment-inner"> <mp-html :content="noticeInfo.content"></mp-html>
<view </view>
class="attachment-item" </view>
v-for="(item, index) in noticeInfo.attachmentList" <!-- 占位防止内容被底部附件遮挡 -->
:key="index" <view class="bottom-placeholder" v-if="noticeInfo.attachmentList && noticeInfo.attachmentList.length > 0"></view>
@tap="downloadAttachment(item)" </scroll-view>
>
<view class="attachment-left"> <!-- 附件列表 -->
<image class="attachment-icon" :src="getAttachmentIcon(item.type)" mode="aspectFit" /> <view class="attachment-section" v-if="noticeInfo.attachmentList && noticeInfo.attachmentList.length > 0">
<text class="attachment-name">{{ item.name }}</text> <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>
<image class="download-icon" src="/static/img/right-icon.png" mode="aspectFit" />
</view> </view>
</view> </view>
</view>
</view> </view>
</s-layout> </s-layout>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick, getCurrentInstance, watch } from 'vue'; import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app'; import { onLoad } from '@dcloudio/uni-app';
import NoticeApi from '@/sheep/api/community/notice'; import NoticeApi from '@/sheep/api/community/notice';
import sheep from '@/sheep'; import sheep from '@/sheep';
// px
const contentScrollHeight = ref(0);
// px
const contentScrollTop = ref(0);
// //
const noticeInfo = ref({ const noticeInfo = ref({
title: '', title: '',
@ -86,48 +81,6 @@ onLoad((options) => {
} }
}); });
// 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) { async function loadNoticeDetail(id) {
const { code, data } = await NoticeApi.getDetail(id); const { code, data } = await NoticeApi.getDetail(id);
@ -143,8 +96,6 @@ async function loadNoticeDetail(id) {
type: getFileType(item.name), type: getFileType(item.name),
})), })),
}; };
// scroll-view
setTimeout(calcScrollHeight, 100);
} }
} }
@ -248,10 +199,13 @@ function downloadAttachment(item) {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 页面容器 */ /* 页面容器:纵向 flex 布局,减去导航栏高度 */
.detail-page { .detail-page {
position: relative; position: relative;
z-index: 1; display: flex;
flex-direction: column;
height: calc(100vh - 176rpx);
overflow: hidden;
} }
/* 渐变背景 */ /* 渐变背景 */
@ -266,22 +220,20 @@ function downloadAttachment(item) {
pointer-events: none; pointer-events: none;
} }
/* 固定头部区域(标题+发布信息) */ /* 头部区域:正常文档流 */
.detail-header { .detail-header {
position: fixed; position: relative;
left: 0;
width: 750rpx;
z-index: 10; z-index: 10;
background: transparent; flex-shrink: 0;
} }
.header-inner { .header-inner {
padding: 0 32rpx; padding: 0 32rpx;
} }
/* 底部占位 - 根据附件数量动态调整 */ /* 底部占位 - 基础间距 + 安全区域 */
.bottom-placeholder { .bottom-placeholder {
height: 40rpx; height: calc(40rpx + env(safe-area-inset-bottom));
} }
/* 文章标题 */ /* 文章标题 */
@ -310,7 +262,6 @@ function downloadAttachment(item) {
.publisher-avatar { .publisher-avatar {
width: 48rpx; width: 48rpx;
height: 48rpx; height: 48rpx;
// border-radius: 50%;
margin-right: 16rpx; margin-right: 16rpx;
} }
@ -333,25 +284,22 @@ function downloadAttachment(item) {
margin-bottom: 32rpx; margin-bottom: 32rpx;
} }
/* 富文本滚动区域 - 高度由JS动态绑定 */ /* 富文本滚动区域flex:1 占满剩余空间height:0 是小程序 scroll-view 配合 flex 的关键 */
.article-scroll { .article-scroll {
position: fixed; position: relative;
left: 0;
width: 100%;
z-index: 5; z-index: 5;
flex: 1;
height: 0;
} }
/* 富文本内容 - 圆角背景卡片 */ /* 富文本内容 - 圆角背景卡片 */
.article-content { .article-content {
margin: 0 32rpx; margin: 0 32rpx;
border-radius: 24rpx; border-radius: 24rpx;
// background-color: #FFFFFF;
} }
/* 富文本内容内层 - 控制padding */ /* 富文本内容内层 */
.article-inner { .article-inner {
// padding: 32rpx;
:deep(p) { :deep(p) {
font-size: 28rpx; font-size: 28rpx;
color: #333333; color: #333333;
@ -366,12 +314,9 @@ function downloadAttachment(item) {
} }
} }
/* 附件区域 - 动态高度最多显示3个附件 */ /* 附件区域 */
.attachment-section { .attachment-section {
position: fixed; flex-shrink: 0;
bottom: 0;
left: 0;
width: 100%;
max-height: 360rpx; max-height: 360rpx;
background: #FFFFFF; background: #FFFFFF;
box-shadow: 0rpx -8rpx 64rpx 0rpx rgba(0, 0, 0, 0.16); box-shadow: 0rpx -8rpx 64rpx 0rpx rgba(0, 0, 0, 0.16);