评论框-组件
<template>
<!-- @tap=";" @mousemove=";" :style="'bottom:'+KeyboardHeight+'px;'" -->
<view>
<view class="comment_mask" :style="maskStyle" @tap.stop="maskCancel"></view>
<view :class="{'input':isMaskContentStyle==true,'onInput':isMaskContentStyle==false}"
class="comment_mask_content" :style="inputStyle" @tap.stop=";" @click.stop=";">
<view class="comment_input">
<u-icon name="edit-pen" v-if="isIcon" color="#999999" size="20"></u-icon>
<textarea v-if="!isMaskContentStyle" ref="textarea" class="textarea" @focus="focusTextarea"
:value="innerValue" :adjust-position="false" placeholder-style="color: #999999;" confirm-type="send"
:placeholder="placeholderValue" auto-height :focus="true" v-model="innerValue" @confirm="confirm"
@keyboardheightchange="keyboardheightchange"></textarea>
<!-- <u--input v-if="isMaskContentStyle" @focus="inputFocus" placeholder="说点什么..." :adjustPosition="false"
border="none" shape="circle"></u--input> -->
<view v-if="isMaskContentStyle" style="flex: 1;" @tap="inputFocus">
<text
style="font-size: 26rpx;font-family: PingFang SC;font-weight: 400;color: #999999;padding-top: 10rpx;">说点什么...</text>
</view>
</view>
<view v-if="!isMaskContentStyle" class="comment_send" @tap.stop="saveComment">
<image class="comment_send_img" :src="require('@/static/icons/send-out.png')"></image>
</view>
</view>
</view>
</template>
<script>
import {
mapGetters
} from "vuex"
export default {
name: "CommentTextarea",
props: {
placeholderText: {
type: String,
default: "说点什么..."
},
value: {
type: String,
default: ""
},
newsId: {
type: String,
default: ""
},
isOnInput: {
type: Boolean,
default: true
},
replayName: {
type: Object,
default: {}
}
},
data() {
return {
KeyboardHeight: 0,
location: "fixed;",
innerValue: "",
maskStyle: '',
isMaskContentStyle: true,
isIcon: true,
placeholderValue: "",
postsId: "",
replayData: {},
isReply: false, //是否处于回复的状态
tempData: {},
tempId: "",
inputStyle: "position: fixed;left: 0;right: 0;bottom: 0;"
};
},
watch: {
replayName: {
immediate: true,
handler(newVal, oldVal) {
if (Object.keys(newVal).length === 0) {
return
}
console.log("得到需要回复的消息", newVal);
// this.isMaskContentStyle = false;
this.isReply = true;
this.replayData = newVal;
this.placeholderValue = `回复:@${this.replayData.replayName}`;
this.innerValue = this.tempData[this.replayData.replyUserID] || "";
this.maskStyle = "background-color: rgba(125, 125, 125, 0.3);position: fixed;";
},
},
isOnInput: {
immediate: true,
handler(newVal, oldVal) {
this.isMaskContentStyle = newVal;
},
},
newsId: {
immediate: true,
handler(newVal, oldVal) {
console.log("帖子ID", newVal)
this.postsId = newVal;
},
},
value: {
immediate: true,
handler(newVal, oldVal) {
this.innerValue = newVal;
},
},
placeholderText: {
immediate: true,
handler(newVal, oldVal) {
this.placeholderValue = newVal;
},
}
},
methods: {
focusTextarea() {
},
/**
* 取消输入
* */
maskCancel() {
let that = this;
setTimeout(() => {
that.tempData[that.isReply?that.replayData.replyUserID:that.tempId] = that.innerValue;
that.isMaskContentStyle = true;
that.inputStyle = `position: fixed;left: 0;right: 0;bottom: 0;`
that.maskStyle = "background-color: rgba(125, 125, 125, 0);position: static;";
uni.hideKeyboard();
that.innerValue = "";
that.isIcon = true;
that.isReply = false;
that.replayData = {};
console.log("回复状态---取消", that.isReply)
}, 100);
},
/**
* 取消输入
* */
confirm(event) {
console.log("完成", event);
this.saveComment();
},
inputFocus() {
if (!this.userID) {
//这里判断用户是否登录
return;
}
let that = this;
that.isReply = false;
that.innerValue = that.tempData[that.tempId] || "";
that.placeholderValue = `说点什么...`;
that.maskStyle = "background-color: rgba(125, 125, 125, 0.3);position: fixed;";
that.isMaskContentStyle = false;
that.isIcon = false;
that.inputStyle = `position: fixed;left: 0;right: 0;bottom: 0;`
},
/**
* 保存输入
* */
saveComment() {
let that = this;
if (this.innerValue.trim().length <= 0) {
return;
}
if (Object.keys(that.replayData).length === 0 && !this.isReply) {
const param = {
"userID": this.userID,
"newsID": this.postsId,
"content": this.innerValue
}
//保存方法
} else {
const param = {
"userID": this.userID,
"replyUserID": this.replayData.replyUserID,
"newsID": this.postsId,
"criticismReplyID": this.replayData.criticismID,
"content": this.innerValue
}
//回复评论保存方法
}
},
keyboardheightchange(e) {
this.inputStyle = `position: fixed;left: 0;right: 0;bottom: ${e.detail.height};`
},
getUUID() {
return "xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
},
computed: {
...mapGetters(['columnID', 'userID']),
i18n() {
return i18n.t('columnAutoRefresh')
}
},
mounted() {
let that = this;
this.maskStyle = "background-color: rgba(125, 125, 125, 0);position: static;";
this.tempId = that.getUUID();
},
beforeDestroy() {}
}
</script>
<style lang="scss" scoped>
.comment_mask {
//
position: fixed;
background-color: rgba(125, 125, 125, 0);
bottom: 0;
left: 0;
right: 0;
top: 0;
}
.comment_mask_content {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background-color: #FFFFFF;
border-top: 1rpx #EEEEEE solid;
display: flex;
flex-direction: row;
align-items: center;
z-index: 99999999999;
.comment_input {
margin: 30rpx 30rpx 30rpx 30rpx;
border-radius: 30rpx;
background: #EEEEEE;
border: #EEEEEE 1rpx solid;
padding: 10rpx;
display: flex;
flex-direction: row;
flex: 12;
}
.textarea {
font-size: 36rpx;
font-family: PingFang SC;
color: #2E2D2D;
}
}
.onInput {
border-top-left-radius: 30rpx;
border-top-right-radius: 30rpx;
}
.input {
border-top-left-radius: 0rpx;
border-top-right-radius: 0rpx;
}
.comment_send {
margin-right: 30rpx;
flex: 1;
&:active {
background-color: rgba(195, 193, 193, 0.1);
}
}
.comment_send_img {
width: 50rpx;
height: 50rpx;
}
//"
</style>
评论展示列表组件
<template>
<view class="comments-card" v-if="commentContents.length>0">
<view class="header">
<u-sticky>
<u-tabs-icon :list="tabList" class="tabs"
:activeStyle="{color: '#2E66B1',fontWeight:'bold',fontSize:'32rpx'}"
:inactiveStyle="{color: '#333',fontWeight:'bold',fontSize:'32rpx'}" :current="tabsCurrent"
itemStyle="padding-left: 0rpx;height: 85rpx;padding:0rpx;" @change="tabsChangeHandler">
</u-tabs-icon>
</u-sticky>
</view>
<view class="list-wrapper">
<view class="comment-content">
<view class="total-comments">
<text class="total-comments-text">{{commentContents.length}}评论</text>
</view>
<view class="comments">
<list ref="list">
<cell class="comments-items" v-for="(item, index) in commentContents" :key="index">
<view class="comments-body">
<!-- 顶级评论 -->
<view class="top-comments" :data-index="index" :data-type="0"
:data-criticismID="item.criticismID"
@tap.stop="replay(item.criticismID,item.userName,item.userID,index)"
@longpress.stop="longTapSelect" @longTap.stop="longTapSelect">
<image :src="item.face" v-if="item.userIcon" class="comments-face"></image>
<u-avatar v-else :text="truncateCharacter(item.userName)"></u-avatar>
<view class="top-comments-info">
<view class="comments-header">
<text class="comments-header-text">{{item.userName}}</text>
<text class="comments-header-text" v-if="item.isAuthor==0"
style="background-color: #316AB9;color: #FFFFFF;margin-left: 8rpx;padding: 2rpx 5rpx 2rpx 5rpx;border-radius: 3rpx;">作者</text>
</view>
<view class="comments-content">
<text class="comments-content-text">{{item.content}}</text>
</view>
<view class="comments-info">
<text
class="grey">{{dateFormat(item.createTime,item.isWaterArmy==0)}}</text>
<!-- <text class="comments-replay-btn"
@tap.stop="replay(item.criticismID,item.userName,item.userID,index)">回复</text> -->
<text class="comments-replay-btn">回复</text>
</view>
</view>
</view>
<!-- 回复 -->
<view class="comments-reply">
<view class="comments-items top-comments"
v-for="(item2, index2) in replyList[index]" :key="index2" :data-index="index2"
:data-criticismID="item2.criticismID" :data-type="1" :data-parentIndex="index"
:data-parentCriticismId="item.criticismID"
@tap.stop="replay(item.criticismID,item.userName,item.userID,index)"
@longpress.stop="longTapSelect" @longTap.stop="longTapSelect">
<image :src="item2.userIcon" v-if="item2.userIcon" class="comments-face">
</image>
<u-avatar v-else :text="truncateCharacter(item2.userName)" size="30"></u-avatar>
<view class="comments-body">
<view class="comments-header">
<text class="comments-header-text">{{item2.userName}}</text>
<text class="comments-header-text" v-if="item2.isAuthor==0"
style="background-color: #316AB9;color: #FFFFFF;margin-left: 8rpx;padding: 2rpx 5rpx 2rpx 5rpx;border-radius: 3rpx;">作者</text>
<template v-if="item2.replyUserName">
<image class="comments-header-image"
:src="require('@/static/icons/right-arrow.png')"
mode="widthFix" />
<text class="comments-header-text">{{item2.replyUserName}}</text>
</template>
</view>
<view class="comments-content">
<text
class="comments-content-text flex flex-direction">{{item2.content}}</text>
</view>
<view class="comments-info">
<text
class="grey">{{dateFormat(item2.createTime,item2.isWaterArmy==0)}}</text>
<text class="comments-replay-btn">回复</text>
</view>
</view>
</view>
</view>
<view class="comments-more-container " v-if="item.childrenCriticismList.length>0"
:name="`loadingState${index}`" :ref="`loadingState${index}`">
<view class="comments-more">
<view class="comments-more-tips" @tap="showMore(index)"
v-if="item.loadingState=='loadmore'">
<text class="comments-more-text">—</text>
<text
class="comments-more-text">展开{{item.childrenCriticismList.length}}条回复</text>
<image class="comments-more-image" mode="aspectFill"
:src="require('@/static/icons/down-arrow.png')">
</image>
</view>
<view class="comments-more-tips" v-if="item.loadingState=='expandMore'">
<view class="comments-more-tips" @tap="showExpandMore(index)">
<text class="comments-more-text">—</text>
<text class="comments-more-text">{{"展开更多"}}</text>
<image class="comments-more-image" mode="aspectFill"
:src="require('@/static/icons/down-arrow.png')">
</image>
</view>
<view class="comments-more-tips" @tap="noMore(index)"
style="margin-left: 30rpx;">
<text class="comments-more-text">—</text>
<text class="comments-more-text">{{"收起"}}</text>
<image class="comments-more-image" mode="widthFix"
:src="require('@/static/icons/up-arrow.png')">
</image>
</view>
</view>
<view v-if="item.loadingState=='nomore'" class="comments-more-tips"
@tap="noMore(index)">
<text class="comments-more-text">{{"收起"}}</text>
<image class="comments-more-image" mode="widthFix"
:src="require('@/static/icons/up-arrow.png')"></image>
</view>
</view>
</view>
</view>
</cell>
</list>
</view>
</view>
<!-- <swiper class="list-wrapper" :current="tabsCurrent" @change="swiperChange">
<swiper-item>
</swiper-item>
<swiper-item>
<view class="comment-content">追踪</view>
</swiper-item>
</swiper> -->
</view>
<view class="comments-card-popup" v-if="showShade" @tap="hidePop">
<view class="pop" :style="popStyle" :class="{'show':showPop}">
<text class="pop-text" @tap.stop="delComment">删除</text>
</view>
</view>
</view>
</template>
<script>
import {
mapGetters
} from "vuex"
import i18n from "@/lang";
import {
forEach
} from "lodash";
export default {
name: "CommentCard",
data() {
return {
postsId: "",
tabsCurrent: 0,
replyList: {},
loadingState: 'loadmore', //加载前值为loadmore,没有数据为nomore
commentContents: [],
moreData: {},
showShade: false,
popStyle: "",
winSize: "",
delCommentData: {},
pageIndex: 1,
pageSize: 10
};
},
props: {
newsId: {
type: String,
default: ""
},
replyRefresh: {
type: String,
default: "",
}
},
computed: {
tabList() {
return [{
name: "评论"
}]
},
...mapGetters(['columnID', 'userID']),
i18n() {
return i18n.t('columnAutoRefresh')
}
},
watch: {
newsId: {
immediate: true,
handler(newVal, oldVal) {
this.postsId = newVal;
this.GetCriticism();
},
},
replyRefresh: {
immediate: true,
handler(newVal, oldVal) {
if (newVal == "") {
return
}
this.GetCriticism();
},
}
},
methods: {
swiperChange(e) {
this.tabsCurrent = e.detail.current;
},
tabsChangeHandler(item) {
this.tabsCurrent = item.index
},
replay(criticismID, replayName, replyUserID, index) {
this.$emit("replay", {
criticismID,
replayName,
replyUserID,
index
})
},
showMore(index) {
// debugger;
// this.$refs[`loadingState${index}`]
this.handlingComment(index);
},
/**收起评论**/
noMore(index) {
this.$set(this.replyList, index, {});
Reflect.deleteProperty(this.replyList, index);
this.commentContents[index].loadingState = "loadmore"; //展开回复
return;
},
/*展开更多评论*/
showExpandMore(index) {
this.handlingComment(index);
},
handlingComment(index) {
if (!this.replyList[index]) {
let temp = this.tempChunk(this.commentContents[index].childrenCriticismList, 3, 0);
this.$set(this.replyList, index, temp[0]);
if (temp.length === 1) {
this.commentContents[index].loadingState = "nomore"; //收起
return;
}
this.commentContents[index].loadingState = "expandMore"; //展开更多回复
} else {
let temp1 = this.tempChunk(this.commentContents[index].childrenCriticismList, 10, this.replyList[index]
.length);
this.replyList[index] = this.replyList[index].concat(temp1[0]);
if (temp1.length === 1) {
this.commentContents[index].loadingState = "nomore"; //收起
return;
}
}
},
GetCriticism() {
let that = this;
if (this.postsId == "") {
return
}
//加载评论回数据
},
GetCriticismReply(criticismID, index) {
let that = this;
//二级评论时触发刷新评论数据
},
dateFormat(time, type) {
let date = new Date(time);
let year = date.getFullYear();
// 在日期格式中,月份是从0开始的,因此要加0,使用三元表达式在小于10的前面加0,以达到格式统一 如 09:11:05
let month = date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1;
let day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate();
let hours = date.getHours() < 10 ? "0" + date.getHours() : date.getHours();
let minutes = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
let seconds = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
// 拼接
if (type) {
return hours + ":" + minutes;
}
return seconds == '00' ? hours + ":" + minutes : hours + ":" + minutes + ":" + seconds;
},
truncateCharacter(item) {
if (item) {
return item.slice(0, 1)
}
return "";
},
/*
arr 数组集合 num 一个数组多少条数据 index 下标
**/
tempChunk(arr, num, index) {
let j = index,
o = j;
let newArray = [];
while (j < arr.length) {
j += num;
newArray.push(arr.slice(o, j));
o = j;
}
return newArray;
},
longTapSelect(e) {
let that = this;
let [touches, style] = [e.touches[0], ""];
/* 因 非H5端不兼容 style 属性绑定 Object ,所以拼接字符 */
if (touches.screenY > (this.winSize.height / 2)) {
style = `bottom:${this.winSize.height-touches.screenY}px;`;
} else {
style = `top:${touches.screenY}px;`;
}
if (touches.screenX > (this.winSize.witdh / 2)) {
style += `right:${this.winSize.witdh-touches.screenX}px;`;
} else {
style += `left:${touches.screenX}px;`;
}
this.popStyle = style;
that.showShade = true;
that.delCommentData = e.target.dataset;
console.log("长按删除", JSON.stringify(e.target.dataset))
},
hidePop() {
setTimeout(() => {
this.showShade = false;
this.delCommentData = {};
}, 250);
},
/* 获取窗口尺寸 */
getWindowSize() {
uni.getSystemInfo({
success: (res) => {
this.winSize = {
"witdh": res.windowWidth,
"height": res.windowHeight
}
}
})
},
delComment() {
let that = this;
//删除评论
}
},
mounted() {
this.getWindowSize();
},
}
</script>
<style lang="scss" scoped>
// .comments-card {
// margin-bottom: 100rpx;
// }
.comments-card-popup {
position: fixed;
z-index: 100;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(125, 125, 125, 0.3);
-webkit-touch-callout: none;
.pop {
position: fixed;
z-index: 101;
width: 180rpx;
box-sizing: border-box;
font-size: 28rpx;
text-align: left;
color: #333;
background-color: #FFFFFF;
.pop-text {
font-size: 30rpx;
color: #333333;
padding: 20rpx 20rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
-webkit-touch-callout: none;
&:active {
background-color: rgba(195, 193, 193, 0.1);
}
}
}
}
.header {
overflow: hidden;
padding-bottom: 20rpx;
.tabs-wrapper {
background-color: transparent;
// padding-bottom: 0rpx;
// margin-bottom: 0rpx;
}
border-bottom: 2rpx solid #EEEEEE;
}
.list-wrapper {
display: flex;
// background-color: red;
padding: 0;
margin: 0;
}
.comment-content {
display: flex;
// padding-top: 20rpx;
.total-comments {
display: flex;
align-items: center;
align-content: center;
justify-content: center;
overflow: hidden;
margin-top: 30rpx;
&-text {
font-size: 36rpx;
font-family: PingFang SC;
font-weight: 400;
color: #2E2D2D;
line-height: 37rpx;
}
}
}
.comments {
display: flex;
.comments-items {
display: flex;
flex-direction: row;
margin-bottom: 32rpx;
.comments-face {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
.comments-body {
flex: 1;
padding-left: 16rpx;
.top-comments {
display: flex;
flex-direction: row;
.top-comments-info {
margin-left: 20rpx;
}
&:active {
background-color: rgba(195, 193, 193, 0.1);
}
}
.comments-header {
margin-top: 8rpx;
margin-bottom: 8rpx;
display: flex;
flex-direction: row;
&-text {
font-size: 24rpx;
font-family: PingFang SC;
font-weight: bold;
color: #BDBDBD;
}
}
.comments-content {
&-text {
font-size: 28rpx;
font-family: PingFang SC;
font-weight: 400;
color: #2E2D2D;
line-height: 37rpx;
}
}
.comments-info {
margin-top: 8rpx;
display: flex;
flex-direction: row;
.comments-replay-btn {
font-size: 24rpx;
font-family: PingFang SC;
font-weight: 400;
color: #6D6D6D;
padding-left: 20rpx;
}
}
.comments-more-container {
display: flex;
flex-direction: column;
margin-right: 10rpx;
// background-color: #2E2D2D;
height: 80rpx;
margin-left: 80rpx;
.comments-more {
display: flex;
flex-direction: row;
color: #999999;
align-items: center;
align-content: center;
justify-content: flex-start;
padding-top: 5rpx;
.comments-more-text {
font-size: 28rpx;
font-family: PingFang SC;
font-weight: 400;
color: #B4B4B4;
}
.comments-more-image {
width: 25rpx;
height: 25rpx;
margin-left: 8rpx;
}
.comments-more-icon {
margin-left: 8rpx;
}
}
}
}
}
}
.comments-reply {
display: flex;
margin-top: 8rpx;
margin-left: 80rpx;
.comments-items {
margin-bottom: 0rpx;
flex: 1;
flex-direction: row;
.comments-face {
width: 70rpx;
height: 70rpx;
}
.comments-body {
.comments-header {
font-size: 26rpx;
display: flex;
flex-direction: row;
align-items: center;
align-content: center;
justify-content: flex-start;
.comments-header-image {
width: 15rpx;
height: 15rpx;
}
}
.comments-content {
display: flex;
flex: 1;
flex-wrap: wrap;
width: 457rpx;
.comments-content-text {
flex: 1;
font-size: 28rpx;
font-family: PingFang SC;
font-weight: 400;
color: #2E2D2D;
white-space: pre-wrap;
word-wrap: break-word;
}
}
}
}
}
.comments-more-tips {
display: flex;
flex-direction: row;
align-items: center;
align-content: center;
justify-content: flex-start;
}
.grey {
font-size: 22rpx;
font-family: PingFang SC;
font-weight: 400;
color: #B4B4B4;
}
</style>
使用方法 ——在父组件中分别引入
<view class="bottom-content">
<ColumnCard :logo="columnLogo" :counter="cardCounter" :columnName="column.name" :intro="column.intro"
:isSubscribe="personal.isSubscribeColumn" @follow="follow" @toColumnDetail="toColumnDetail"
:id="column.id" />
</view>
<u-gap height="5" bgColor="#EEEEEE"></u-gap>
<view class="bottom-content" style="padding-top: 5rpx;">
<CommentCard :newsId="newsID" v-on:replay="replay" ref="commentListRef" />
</view>
父组件加入下面几个方法
replay(data) {
this.replayData = data;
this.$refs.commentTextareaRef.isMaskContentStyle = false;
},
/*
* 刷新回复评论列表
*/
replyRefreshList(data) {
this.$refs.commentTextareaRef.isMaskContentStyle = true;
this.$refs.commentListRef.GetCriticismReply(data.criticismID, data.index);
},
/*
* 刷新评论列表
*/
commentRefreshList() {
this.$refs.commentTextareaRef.isMaskContentStyle = true;
this.$refs.commentListRef.GetCriticism();
},
pullUpLoading() {
console.log("上拉加载");
this.$refs.commentListRef.pageIndex = this.$refs.commentListRef.pageIndex + 1;
this.$refs.commentListRef.GetCriticism();
}
效果图:
评论区