文件
OmniCard/mpuniapp/pages/template/vertical-video-feed/comment-panel.uvue

401 行
11 KiB
Plaintext

<template>
<view class="popup-box" v-if="show" @click="onMaskClick">
<view class="popup-mask"></view>
<view ref="commentPanelRef" class="comment-panel" @click.stop
@touchstart="onPanelTouchStart" @touchmove="onPanelTouchMove" @touchend="onPanelTouchEnd">
<view class="comment-panel-header">
<text class="comment-panel-title">评论 {{commentList.length}}</text>
<view class="comment-panel-close" @click="handleClose">
<text class="close-text">✕</text>
</view>
</view>
<scroll-view ref="commentScrollRef" class="comment-scroll" :direction="scrollDirection" bounces="true">
<view class="comment-list">
<view class="comment-item" v-for="(comment, index) in commentList" :key="index">
<view class="comment-avatar">
<text class="avatar-text">{{comment.user.charAt(0)}}</text>
</view>
<view class="comment-content">
<text class="comment-user">{{comment.user}}</text>
<text class="comment-text">{{comment.content}}</text>
<text class="comment-time">{{comment.time}}</text>
</view>
</view>
</view>
</scroll-view>
<view class="comment-input-box" :style="'transform: translateY(-' + keyboardHeight + 'px);'">
<input
class="comment-input"
v-model="commentInput"
placeholder="说点什么..."
@confirm="submitComment"
:adjust-position="false"
@keyboardheightchange="onKeyboardHeightChange"
/>
<view class="comment-send-btn" :class="{ 'disabled': !canSend }" @click="submitComment">
<text class="send-text">发送</text>
</view>
</view>
<!-- 当前 uni-app x 框架在 iOS、Android 上报的键盘高度均已包含底部安全区,键盘弹出时隐藏额外占位 -->
<view v-if="keyboardHeight == 0" class="safe-area-inset-bottom"></view>
</view>
</view>
</template>
<script setup lang="uts">
/**
* @description: 作为竖滑视频页的评论面板,支持展示评论列表、输入评论并同步更新评论数量。
* @logic: 面板根据父页面传入的显示状态决定是否弹出,初始化预置评论列表;用户输入并发送评论后,把数量变化通过事件回传给父页面。
* @optimization: 评论面板只在父页面需要时显示,并通过事件把数量变化回传给父页面,避免父子页面维护重复状态。
*/
type CommentItem = { user : string, content : string, time : string }
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
close: []
countChange: [count: number]
}>()
const commentList = ref([] as CommentItem[])
const commentInput = ref("" as string)
const keyboardHeight = ref(0 as number)
// 滚动到顶再下拉关闭(与 half-screen 一致)
let panelMove = false
let panelStartY = 0
let panelOffset = 0
let panelHeight = 0
let lastTouchY = 0
let lastTouchY2 = 0
let bAnimating = false
const commentPanelRef = ref<UniElement | null>(null)
const commentScrollRef = ref<UniScrollViewElement | null>(null)
const scrollDirection = ref('vertical')
// 计算是否可以发送
const canSend = computed(() => {
return commentInput.value.trim().length > 0
})
// 初始化评论数据
function initComments() {
commentList.value = [
{ user: "用户A", content: "这个视频太棒了!", time: "2分钟前" },
{ user: "用户B", content: "学到了很多,感谢分享", time: "5分钟前" },
{ user: "用户C", content: "期待更多精彩内容", time: "10分钟前" },
{ user: "用户D", content: "点赞支持!", time: "15分钟前" },
{ user: "用户E", content: "很有用的教程", time: "20分钟前" },
{ user: "用户F", content: "很有用的教程", time: "20分钟前" },
{ user: "用户G", content: "很有用的教程", time: "20分钟前" },
{ user: "用户H", content: "很有用的教程", time: "20分钟前" },
{ user: "用户I", content: "很有用的教程", time: "20分钟前" },
{ user: "用户J", content: "很有用的教程", time: "20分钟前" }
]
emit('countChange', commentList.value.length)
}
function handleClose() {
keyboardHeight.value = 0
panelMove = false
panelStartY = 0
panelOffset = 0
// #ifdef APP
commentScrollRef.value?.setAttribute('direction', 'vertical')
// #endif
// #ifdef WEB || MP
scrollDirection.value = 'vertical'
// #endif
commentPanelRef.value?.style?.setProperty('transform', '')
commentPanelRef.value?.style?.setProperty('transition-property', 'none')
emit('close')
}
function measurePanelHeight() {
commentPanelRef.value?.getBoundingClientRectAsync()?.then((rect: DOMRect) => {
panelHeight = rect.height
})
}
function onPanelTouchStart(_event : TouchEvent) {
commentPanelRef.value?.style?.setProperty('transition-duration', '0')
}
function onPanelTouchMove(e : TouchEvent) {
if (bAnimating) return
let top : number = commentScrollRef.value?.scrollTop ?? 0
let p = e.touches[0]
lastTouchY2 = lastTouchY
lastTouchY = p.screenY
if (top <= 0.01 || panelMove) {
if (panelStartY == 0) {
panelStartY = p.screenY
}
let offset = p.screenY - panelStartY
if (offset > 0) {
panelMove = true
// #ifdef APP
commentScrollRef.value?.setAttribute('direction', 'none')
// #endif
// #ifdef WEB || MP
scrollDirection.value = 'none'
// #endif
commentPanelRef.value?.style?.setProperty('transform', 'translateY(' + offset.toFixed(2) + 'px)')
panelOffset = offset
} else if (panelOffset > 0) {
offset = panelStartY - p.screenY
if (offset > panelOffset) {
offset = 0
panelMove = false
}
// #ifdef APP
commentScrollRef.value?.setAttribute('direction', 'vertical')
// #endif
// #ifdef WEB || MP
scrollDirection.value = 'vertical'
// #endif
commentPanelRef.value?.style?.setProperty('transform', 'translateY(' + offset.toFixed(2) + 'px)')
panelOffset = offset
}
}
// #ifdef WEB
e.preventDefault()
// #endif
}
function resumePanel() {
const time = 300
commentPanelRef.value?.style?.setProperty('transition-duration', time.toString() + 'ms')
commentPanelRef.value?.style?.setProperty('transition-timing-function', 'ease-in-out')
commentPanelRef.value?.style?.setProperty('transition-property', 'transform')
commentPanelRef.value?.style?.setProperty('transform', 'translateY(0px)')
panelMove = false
// #ifdef APP
commentScrollRef.value?.setAttribute('direction', 'vertical')
// #endif
// #ifdef WEB || MP
scrollDirection.value = 'vertical'
// #endif
panelStartY = 0
panelOffset = 0
setTimeout(() => {
bAnimating = false
commentPanelRef.value?.style?.setProperty('transition-property', 'none')
}, time)
bAnimating = true
}
function onPanelTouchEnd(_event : TouchEvent) {
panelStartY = 0
if (bAnimating) return
let top : number = commentScrollRef.value?.scrollTop ?? 0
let bHide = panelHeight > 0 && (panelHeight - panelOffset) < panelHeight / 4
if (bHide) {
bHide = lastTouchY2 > 0 && lastTouchY2 <= lastTouchY
} else if (top <= 0.01) {
bHide = (lastTouchY - lastTouchY2) > 3
}
if (bHide) {
handleClose()
} else if (panelOffset > 0) {
resumePanel()
}
}
function onMaskClick() {
handleClose()
}
function onKeyboardHeightChange(event : UniInputKeyboardHeightChangeEvent) {
keyboardHeight.value = event.detail.height
}
function submitComment() {
if (commentInput.value.trim() == "") {
return
}
const timeStr = "刚刚"
const newComment : CommentItem = {
user: "我",
content: commentInput.value,
time: timeStr
}
commentList.value.unshift(newComment)
commentInput.value = ""
emit('countChange', commentList.value.length)
}
// 弹窗打开时测量面板高度(用于下拉关闭阈值)
watch((): boolean => props.show, (val: boolean) => {
if (val) {
nextTick(() => {
measurePanelHeight()
})
}
})
// 初始化
initComments()
</script>
<style>
/* 弹窗容器 */
.popup-box {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
}
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
}
.comment-panel {
position: absolute;
top: 200px;
bottom: 0;
left: 0;
right: 0;
width: 100%;
background-color: #1a1a1a;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
flex-direction: column;
}
.comment-panel-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom-width: 1px;
border-bottom-color: #333333;
}
.comment-panel-title {
font-size: 16px;
font-weight: bold;
color: #ffffff;
}
.comment-panel-close {
width: 30px;
height: 30px;
justify-content: center;
align-items: center;
}
.close-text {
font-size: 20px;
color: #999999;
}
.comment-scroll {
flex: 1;
}
.comment-list {
padding: 10px 20px;
}
.comment-item {
flex-direction: row;
margin-bottom: 15px;
}
.comment-avatar {
width: 36px;
height: 36px;
border-radius: 18px;
background-color: #007aff;
justify-content: center;
align-items: center;
margin-right: 10px;
}
.avatar-text {
color: #ffffff;
font-size: 14px;
font-weight: bold;
}
.comment-content {
flex: 1;
flex-direction: column;
}
.comment-user {
font-size: 14px;
font-weight: bold;
color: #ffffff;
margin-bottom: 5px;
}
.comment-text {
font-size: 14px;
color: #e0e0e0;
line-height: 20px;
margin-bottom: 5px;
}
.comment-time {
font-size: 12px;
color: #888888;
}
.comment-input-box {
flex-direction: row;
align-items: center;
padding: 10px 15px;
border-top-width: 1px;
border-top-color: #333333;
background-color: #1a1a1a;
transition-property: transform;
transition-duration: 0.1s;
transition-timing-function: ease-out;
}
.safe-area-inset-bottom {
height: env(safe-area-inset-bottom);
flex-shrink: 0;
background-color: #1a1a1a;
}
.comment-input {
flex: 1;
height: 36px;
padding: 0 10px;
background-color: #2c2c2c;
color: #ffffff;
border-radius: 18px;
font-size: 14px;
margin-right: 10px;
}
.comment-send-btn {
padding: 8px 20px;
background-color: #007aff;
border-radius: 18px;
}
.comment-send-btn.disabled {
background-color: #444444;
opacity: 0.6;
}
.send-text {
color: #ffffff;
font-size: 14px;
}
</style>