最近使用uniapp做了个h5的项目,嵌入App使用。中间有个评论组件,把人整个够呛,前后改了好几遍。第一次开发,照着文档找组件,好不容易写了个差不错,结果发现在手机App里表现非常不理想,表现为textarea无法自动focus,input组件也是如此。周六加班终于搞了个差不错。
问题1、textarea自动focus???
本来的方案是,在底部做个假的输入框,click时,弹出textarea所在popup,结果popup弹出来了,textarea没自动focus
分析:textarea自带的focus不好用
解决:舍弃假的输入框,直接使用textarea所在的view,开始在底部定位,textarea的 focus事件里,将view的位置进行修改。
问题2、评论取消后,textarea又无法聚焦了,即使focus使用了一个变量来控制
分析:或许focus绑定的值需要变化,才能引起textarea是否能focus
解决:取消评论后,及时将变量的值改为false,下次需要focus时,将变量值改为true
问题3、评论框内需显示@xxx:,给后端发送请求时还不能带着
分析: 评论时带着@xxx:比较好实现,发送时截取使用string.replace
解决:string.replace(/@[0-9a-zA-Z\u4e00-\u9fa5]+:/, '')
/@[0-9a-zA-Z\u4e00-\u9fa5]+:/,这个正则就是包括了 英文字母a-zA-Z数字0-9以及中文
最坑的地方是 string.replace方法不在原来的string修改,而是返回了一个新的字符串,需要接收!!!!
问题4、嵌入的App也没有做统一的处理,结果安卓手机键盘会盖在页面最上面,苹果手机则会将页面进行上推,我上面定义的view位置是bottom:50%,苹果手机给我推没了,5555
解决:根据/iphone|ipad|ipod|ios/.test(window.navigator.userAgent.toLowerCase()),判断出是苹果手机后,定位bottom:0。
附上代码,方便以后查阅。
vue3版的uniapp
<template>
<view class="comment-wrap">
<view class="title">评论</view>
<view class="comment-list" v-if="list.length">
<view v-for="item in list" :key="`comment-${item.id}`">
<Item :item="item" @reply="onReply" />
<view class="second-list" v-if="item.comment.length>0">
<Item avatar-size="22" v-for="jtem in item.comment.slice(0, 2)" :item="jtem"
:key="`reply-${jtem.id}`" @reply="onReply" />
<view class="check-more" v-if="item.comment.length>2" @click="openMoreComment(item)">
查看全部{{item.comment.length}}条精彩评论 ></view>
</view>
</view>
<view class="more-comment-tip" @click="getMoreComment">{{more?'查看更多':'已经到底了'}}</view>
</view>
<view class="nodata" v-show="showCommentResult&&list.length===0">
<image src="../../static/noContent.png" mode=""></image>
<text>暂无评论</text>
</view>
</view>
<!-- 显示一级评论的全部回复 -->
<u-popup :show="showMoreCommentPop" :closeable="true" mode="bottom" round="32" @close="closeMoreCommentPop"
class="comment-input-popup">
<view class="popup-title-custom">显示全部({{moreComment?.comment?.length}})</view>
<view class="main-comment">
<Item :item="moreComment" @reply="e=>onReply(e, 'first')" />
</view>
<view class="popup-subtitle">全部回复</view>
<view class="comment-comment" v-show="moreComment?.comment?.length">
<Item :item="jtem" v-for="jtem in moreComment.comment" @reply="onReply" />
<view class="more-comment-tip">已经到底了</view>
</view>
</u-popup>
<view v-show="showInputPop" class="c-mask" @click="closeInputPop" @touchmove.stop.prevent="disableScroll"></view>
<view class="iinput-view" :style="{bottom: showInputPop?isIphone?'0%':'50%':'0%'}" @touchmove.stop.prevent="disableScroll">
<view class="c-popup-content">
<view class="comment-input-wrap">
<textarea v-model="comment" class="comment-textarea" :maxlength="250" :focus="replyFocus"
placeholder="欢迎发表你的观点" confirm-type="send" auto-height @confirm="commentCommit"
@focus="bindFocus" />
<uni-icons v-if="hasComment" type="paperplane" size="30" color="#286CFB"
@click="commentCommit"></uni-icons>
<view class="cancel-text" v-if="showInputPop&&!hasComment" @click="closeInputPop">取消
</view>
</view>
<!-- <view class="reply-block" v-if="replyContent">{{replyContent}}</view> -->
</view>
</view>
</template>
<script setup>
import {
ref,
getCurrentInstance,
computed
} from 'vue'
import Item from './Item.vue'
import {
getCommentList,
commentCreate
} from '../../api/index.js'
// 当前实例
const {
proxy
} = getCurrentInstance()
const props = defineProps({
articleId: {
type: [Number, String],
}
})
// 新建评论
const comment = ref('')
// 评论列表
const list = ref([])
// 新建评论弹窗
const showInputPop = ref(false)
// @回复内容显示
const replyContent = ref('')
// 回复目标ID
const targetId = ref('')
// 更多评论弹窗
const showMoreCommentPop = ref(false)
// 一级评论展开的内容
const moreComment = ref({})
// 是否显示评论获取的列表结果
const showCommentResult = ref(false)
// 点击回复控制textarea的聚焦
const replyFocus = ref(false)
// 获取当前设备,如果是Safari,弹框将留在底部
const isIphone = ref('')
isIphone.value = /iphone|ipad|ipod|ios/.test(window.navigator.userAgent.toLowerCase())
// 请求列表需要的page和pagesize
let page = 1
let pagesize = 10
// 是否还有更多评论
const more = ref(true)
const hasComment = computed(()=>{
return comment.value.replace(/@[0-9a-zA-Z\u4e00-\u9fa5]+:/, '').trim().length>0
})
const requestList = async () => {
const params = {
article_id: props.articleId,
target: 0,
page,
pagesize
}
const {
data
} = await getCommentList(params)
if (data) {
// 判断是否需要显示还有更多
more.value = Math.ceil(data.total / pagesize) > page
data.list.forEach(item => {
item.comment = itFn([], item.comment, '')
item.comment.length && item.comment.sort((f, s) => (+new Date(s.created_at) -
+new Date(f.created_at)))
})
if (params.page === 1) {
// 显示有无结果
showCommentResult.value = true
list.value = []
}
list.value = list.value.concat(data.list)
if (showMoreCommentPop.value) {
updateMoreCommentData()
}
}
}
// 一级评论更多里面回复后刷新数据
const updateMoreCommentData = () => {
moreComment.value = list.value.find(item => item.id === moreComment.value.id)
}
// 递归得到所有1级评论的回复
function itFn(target, comment, text) {
if (comment.length > 0) {
for (let i = 0; i < comment.length; i++) {
let item = comment[i]
if (text) {
item.replyContent = text
}
target.push(item)
if (item?.comment?.length > 0) {
let str = `@${item.username}:${item.content}`
itFn(target, item.comment, str)
}
}
}
return target
}
requestList()
// 提交评论
const commentCommit = async () => {
let commentStr = comment.value
commentStr = commentStr.replace(/@[_-0-9a-zA-Z\u4e00-\u9fa5]+:/, '')
commentStr = commentStr.trim()
if (commentStr) {
const {
data
} = await commentCreate({
aid: props.articleId,
content: commentStr,
target: targetId.value ? targetId.value : 0
})
if (data) {
uni.showToast({
title: '评论成功',
icon: 'none'
})
comment.value = ''
closeInputPop()
requestList()
}
} else {
uni.showToast({
title: '内容不能为空哦',
icon: 'none'
})
}
}
const openInputPop = () => {
showInputPop.value = true
}
// 组件内回复, type==='first'时是一级直接评论,不用增加@内容
const onReply = (e, type) => {
// 组件回复
if (e) {
comment.value = type === 'first' ? '' : e.prefixContent
targetId.value = e.targetId
}
openInputPop()
replyFocus.value = true
}
const openMoreComment = (data) => {
showMoreCommentPop.value = true
moreComment.value = data
}
const closeMoreCommentPop = () => {
showMoreCommentPop.value = false
moreComment.value = {}
}
const closeInputPop = () => {
showInputPop.value = false
replyFocus.value = false
comment.value = ''
targetId.value = ''
}
const getMoreComment = () => {
if (more.value) {
page++
requestList()
}
}
const disableScroll = () => {
return
}
const bindFocus = () => {
showInputPop.value = true
// 1级弹框底部直接触发的
if (showMoreCommentPop.value) {
targetId.value = targetId.value ? targetId.value : moreComment.value.id
}
}
</script>
<style lang="scss" scoped>
.comment-wrap {
padding: 40rpx 32rpx;
width: 100%;
height: auto;
padding-bottom: calc(50rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
.comment-list{
padding-bottom: 10vh;
}
}
.title {
margin-bottom: 40rpx;
width: 100%;
height: 48rpx;
font-size: 34rpx;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 600;
color: #1F2227;
line-height: 48rpx;
}
.second-list {
padding-left: 80rpx;
}
.more-comment-tip {
height: 104rpx;
font-size: 24rpx;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #8F959F;
line-height: 94rpx;
text-align: center;
}
.comment-input-popup ::v-deep .u-popup__content,
.c-popup-content {
padding: 38rpx 28rpx;
width: 100%;
background-color: #fff;
padding-bottom: calc(50rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
}
.comment-input-wrap {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.cancel-text {
margin-left: 8rpx;
width: 60rpx;
font-size: 28rpx;
color: #286CFB;
flex-shrink: 0;
}
.comment-textarea {
flex-grow: 1;
}
}
.reply-block {
padding: 8rpx 16rpx;
margin: 16rpx 0;
font-size: 24rpx;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #8F959F;
line-height: 34rpx;
background: #F5F6FB;
border-radius: 8rpx;
}
.check-more {
margin-left: 56rpx;
margin-bottom: 32rpx;
height: 34rpx;
font-size: 24rpx;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #999999;
line-height: 34rpx;
}
.popup-title-custom {
margin-bottom: 28rpx;
height: 48rpx;
font-size: 34rpx;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #1F2227;
line-height: 48rpx;
text-align: center;
}
.popup-subtitle {
margin: 32rpx 0;
height: 40rpx;
font-size: 28rpx;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #1F2227;
line-height: 40rpx;
}
.comment-comment {
max-height: 50vh;
overflow: scroll;
padding-bottom: 10vh;
}
.nodata {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 28rpx;
line-height: 40rpx;
color: #A5A9AF;
image {
width: 256rpx;
height: 256rpx;
}
}
.comment-textarea {
padding: 5px 15px;
border: 1px solid #aaa;
border-radius: 16px;
}
.c-mask {
position: fixed;
left: 0;
top: 0;
z-index: 10075;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, .5);
}
.iinput-view {
position: fixed;
left: 0;
bottom: 0;
z-index: 10077;
width: 100vw;
min-height: 10vh;
background-color: #fff;
box-shadow: 5px 0 0 5px #f5f5f5;
}
</style>