实现下图所示,手动向上滑,窗口滑出:

javascript
// ---data定义
// 面板当前位置 (px)
panelPosition: 620,
// 面板完全展开的位置
panelUpPosition: 100,
// 面板收起的位置
panelDownPosition: 400,
// 触摸起始点
startY: 0,
// 记录滑动过程中的位移
moveY: 0,
// 控制遮罩显示
isPanelVisible: false,
// 控制scroll-view的滚动位置
scrollTop: 0,
// 标记面板当前状态
panelState: 'down',
videoContext: null,
javascript
// -----布局
<view
class="detail-mask"
:class="{ 'mask-show': isPanelVisible }"
@tap="hideDetailPanel"
></view>
<view
class="detail-panel"
:style="{ transform: `translateY(${panelPosition}px)` }"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<view class="panel-handle">
<view class="handle-bar"></view>
</view>
<scroll-view
class="panel-content"
scroll-y="true"
:scroll-top="scrollTop"
style="height: 2000rpx"
>
<view class="content-section">
<view class="top-box">
<view class="section-title">查看数据</view>
<view class="iconfont icon-zhaoxiangji1" @click="toPhoto"></view>
</view>
<view class="realtime-section" v-if="cameraData.length > 0">
<image
class="content-image"
:src="snapshotImg"
mode=""
v-if="snapshotImg"
></image>
<view class="sub-title" @click="toViewVideo">
<view class="iconfont icon-shipinchakan"></view>
</view>
</view>
</view>
<view class="next-box">
<view class="content-section">
<view class="section-box" v-if="cameraData.length > 0">
<view class="section-title set-top">长势图</view>
<view class="section-history" @click="viewHistory">历史数据</view>
</view>
<view class="comparison-box" v-if="snapshotImgArr.length > 0">
<view class="item-mid">
<view class="two-step">
<view class="two-line">
<view class="step-right">
<view class="line-content">
<view class="right-a">
<view class="r-name set-color">{{ getTime() }}</view>
</view>
<view class="grouth-box">
<view
class="img-box"
v-for="item in snapshotImgArr"
:key="item"
>
<image
style="width: 290rpx; height: 200rpx"
:src="item.imagePath"
mode=""
></image>
<view class="img-text">{{ item.imageDate }}</view>
</view>
</view>
<view class="content">
<view class="section">
<view class="markdown-container">
<uMarkdown
:source="textInfo.comparisonResult"
></uMarkdown>
</view>
</view>
<view class="section">
<view class="markdown-container">
<uMarkdown
:source="textInfo.recommendations"
></uMarkdown>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
javascript
// ------函数方法
// 隐藏详情面板
hideDetailPanel () {
this.panelPosition = 620
this.panelState = 'down'
// 动画结束后隐藏遮罩
setTimeout(() => {
this.isPanelVisible = false
this.scrollTop = 0
}, 300)
},
handlePhoneTouchStart (e) {
uni.makePhoneCall({
phoneNumber: '13122222222'
})
e.stopPropagation()
},
// 触摸开始
onTouchStart (e) {
if (
e.target.dataset.type === 'phoneBtn' ||
e.target.dataset.type === 'mointorBtn'
) {
return
}
// e.stopPropagation()
this.startY = e.touches[0].clientY
this.isPanelVisible = true
this.$nextTick(() => {
setTimeout(() => {
this.panelPosition = this.panelUpPosition
this.panelState = 'up'
}, 50)
})
},
// 触摸移动
onTouchMove (e) {
if (!this.isPanelVisible) return
let currentY = e.touches[0].clientY
this.moveY = currentY - this.startY
// 阻止向下滚动时页面整体滚动
if (this.moveY > 0 && this.scrollTop <= 0) {
e.preventDefault()
}
// 计算新的面板位置
let newPosition = this.panelUpPosition + this.moveY
// 限制拖动范围:不能向上推得比完全展开更高,向下不能过低
newPosition = Math.max(this.panelUpPosition - 50, newPosition) // 允许稍微拉过一点
newPosition = Math.min(newPosition, this.panelDownPosition + 50)
this.panelPosition = newPosition
},
// 触摸结束
onTouchEnd (e) {
console.log('onTouchEnd')
if (!this.isPanelVisible) return
// e.stopPropagation()
const moveDistance = this.moveY
const panelHeight = this.panelDownPosition - this.panelUpPosition
// 根据滑动距离和速度决定最终状态
if (moveDistance > panelHeight * 0.3 || moveDistance > 100) {
// 下滑超过阈值,收起
this.hideDetailPanel()
} else {
// 否则,恢复展开状态
this.panelPosition = this.panelUpPosition
this.panelState = 'up'
}
// 重置移动距离
this.moveY = 0
},
javascript
css样式
.detail-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 998;
}
.mask-show {
opacity: 1;
visibility: visible;
}
.detail-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 95vh;
background: #fff;
border-top-left-radius: 30rpx;
border-top-right-radius: 30rpx;
z-index: 999;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* 平滑的动画曲线 */
box-shadow: 0 -5rpx 20rpx rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
}
.handle-bar {
width: 100rpx;
height: 8rpx;
background-color: #ddd;
border-radius: 4rpx;
}
.panel-content {
flex: 1;
padding: 0 30rpx;
// width: 700rpx;
box-sizing: border-box;
}
.realtime-section {
position: relative;
}
.sub-title {
position: absolute;
bottom: 12rpx;
left: 0;
width: 100%;
height: 68rpx;
background: rgba(0, 0, 0, 0.5);
color: #fff;
border-radius: 10rpx;
text-align: right;
line-height: 68rpx;
.text-bold {
font-size: 30rpx;
margin-left: 16rpx;
margin-top: 16rpx;
}
.icon-shipinchakan {
margin-right: 40rpx;
}
}
.icon-fangda04 {
position: absolute;
top: 32rpx;
right: 40rpx;
color: #fff;
}
.section-box {
display: flex;
align-items: center;
justify-content: space-between;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
}
.section-history {
font-size: 28rpx;
color: #767676;
// width: 160rpx;
height: 40rpx;
}
.content-image {
width: 100%;
border-radius: 15rpx;
margin-top: 15rpx;
height: 385rpx;
}
/* 全屏视频样式 */
.fullscreen-video {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
background-color: #000;
}
.video-close-btn {
position: absolute;
top: 60rpx;
right: 30rpx;
z-index: 10000;
width: 60rpx;
height: 60rpx;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 40rpx;
}