401 行
11 KiB
Plaintext
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>
|