cc-comment.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. <template>
  2. <view class="c_total">评论 {{ props.tableTotal }}</view>
  3. <template v-if="dataList && dataList.length">
  4. <view class="c_comment" v-for="(item1, index1) in dataList" :key="item1.id">
  5. <!-- 一级评论 -->
  6. <CommonComp
  7. :data="item1"
  8. @likeClick="() => likeClick({ item1, index1 })"
  9. @replyClick="() => replyClick({ item1, index1 })"
  10. @deleteClick="() => deleteClick({ item1, index1 })"
  11. />
  12. <view class="children_item" v-if="item1.children && item1.children.length">
  13. <!-- 二级评论 -->
  14. <CommonComp
  15. v-for="(item2, index2) in item1.childrenShow"
  16. :key="item2.id"
  17. :data="item2"
  18. :pData="item1"
  19. @likeClick="() => likeClick({ item1, index1, item2, index2 })"
  20. @replyClick="() => replyClick({ item1, index1, item2, index2 })"
  21. @deleteClick="() => deleteClick({ item1, index1, item2, index2 })"
  22. />
  23. <!-- 展开二级评论 -->
  24. <view
  25. class="expand_reply"
  26. v-if="expandTxtShow({ item1, index1 })"
  27. @tap="() => expandReplyFun({ item1, index1 })"
  28. >
  29. <span class="txt"> 展开{{ item1.children.length - item1.childrenShow.length }}条回复 </span>
  30. <uni-icons type="down" size="24" color="#007aff"></uni-icons>
  31. </view>
  32. <!-- 折叠二级评论 -->
  33. <view
  34. class="shrink_reply"
  35. v-if="shrinkTxtShow({ item1, index1 })"
  36. @tap="() => shrinkReplyFun({ item1, index1 })"
  37. >
  38. <span class="txt"> 收起回复内容 </span>
  39. <uni-icons type="up" size="24" color="#007aff"></uni-icons>
  40. </view>
  41. </view>
  42. </view>
  43. </template>
  44. <!-- 空盒子 -->
  45. <view class="empty_box" v-else>
  46. <uni-icons type="chatboxes" size="36" color="#c0c0c0"></uni-icons>
  47. <view>
  48. <span class="txt"> 这里是一片荒草地, </span>
  49. <span class="txt click" @click="() => newCommentFun()">说点什么...</span>
  50. </view>
  51. </view>
  52. <!-- 评论弹窗 -->
  53. <uni-popup ref="cPopupRef" type="bottom" @change="popChange">
  54. <view class="c_popup_box">
  55. <view class="reply_text">
  56. <template v-if="Object.keys(replyTemp).length">
  57. <span class="text_aid">回复给</span>
  58. <img class="user_avatar" :src="replyTemp.item2 ? replyTemp.item2.user_avatar : replyTemp.item1.user_avatar" />
  59. <span class="text_main">{{ replyTemp.item2 ? replyTemp.item2.user_name : replyTemp.item1.user_name }}</span>
  60. </template>
  61. <span v-else class="text_main">发表新评论</span>
  62. </view>
  63. <view class="content">
  64. <view class="text_area">
  65. <uni-easyinput
  66. class="text_area"
  67. type="textarea"
  68. v-model="commentValue"
  69. :placeholder="commentPlaceholder"
  70. :focus="focus"
  71. trim
  72. autoHeight
  73. maxlength="300"
  74. ></uni-easyinput>
  75. </view>
  76. <view class="send_btn" @tap="() => sendClick()">发送</view>
  77. </view>
  78. </view>
  79. </uni-popup>
  80. <!-- 删除弹窗 -->
  81. <uni-popup ref="delPopupRef" type="dialog">
  82. <uni-popup-dialog
  83. mode="base"
  84. title=""
  85. content="确定删除这条评论吗?"
  86. :before-close="true"
  87. @close="delCloseFun"
  88. @confirm="delConfirmFun"
  89. ></uni-popup-dialog>
  90. </uni-popup>
  91. </template>
  92. <script setup>
  93. import CommonComp from "./componets/common";
  94. import { reactive, ref, watch, computed, defineExpose } from "vue";
  95. const props = defineProps({
  96. /** 登陆用户信息
  97. * id: number // 登陆用户id
  98. * user_name: number // 登陆用户名
  99. * user_avatar: string // 登陆用户头像地址
  100. */
  101. myInfo: {
  102. type: Object,
  103. default: () => {},
  104. },
  105. /** 文章作者信息
  106. * id: number // 文章作者id
  107. * user_name: number // 文章作者名
  108. * user_avatar: string // 文章作者头像地址
  109. */
  110. userInfo: {
  111. type: Object,
  112. default: () => {},
  113. },
  114. /** 评论列表
  115. * id: number // 评论id
  116. * parent_id: number // 父级评论id
  117. * reply_id: number // 被回复人评论id
  118. * reply_name: string // 被回复人名称
  119. * user_name: string // 用户名
  120. * user_avatar: string // 评论者头像地址
  121. * user_content: string // 评论内容
  122. * is_like: boolean // 是否点赞
  123. * like_count: number // 点赞数统计
  124. * create_time: string // 创建时间
  125. */
  126. tableData: {
  127. type: Array,
  128. default: () => [],
  129. },
  130. // 评论总数
  131. tableTotal: {
  132. type: Number,
  133. default: 0,
  134. },
  135. // 评论删除模式
  136. // bind - 当被删除的一级评论存在回复评论, 那么该评论内容变更显示为[当前评论内容已被移除]
  137. // only - 仅删除当前评论(后端删除相关联的回复评论, 否则总数显示不对)
  138. // all - 删除所有评论包括回复评论
  139. deleteMode: {
  140. type: String,
  141. default: "all",
  142. },
  143. });
  144. const emit = defineEmits([
  145. "update:tableTotal",
  146. "likeFun", // 点赞事件
  147. "replyFun", // 回复事件
  148. "deleteFun", // 删除事件
  149. ]);
  150. // 渲染数据(前端的格式)
  151. let dataList = ref([]);
  152. watch(
  153. () => props.tableData,
  154. (newVal) => {
  155. if (newVal.length !== dataList.value.length) {
  156. let temp = props.tableData;
  157. dataList.value = treeTransForm(temp);
  158. console.log(dataList.value, '>>>>>>dataList.value');
  159. }
  160. },
  161. { deep: true, immediate: true }
  162. );
  163. // 数据转换
  164. function treeTransForm(data) {
  165. let newData = JSON.parse(JSON.stringify(data));
  166. let result = [];
  167. let map = {};
  168. newData.forEach((item, i) => {
  169. item.owner = item.user_id === props.myInfo.user_id; // 是否为当前登陆用户 可以对自己的评论进行删除 不能回复
  170. item.author = item.user_id === props.userInfo.user_id; // 是否为作者 显示标记
  171. map[item.id] = item;
  172. });
  173. newData.forEach((item) => {
  174. let parent = map[item.parent_id];
  175. if (parent) {
  176. (parent.children || (parent.children = [])).push(item); // 所有回复
  177. if (parent.children.length === 1) {
  178. (parent.childrenShow = []).push(item); // 显示的回复
  179. }
  180. } else {
  181. result.push(item);
  182. }
  183. });
  184. return result;
  185. }
  186. // 点赞
  187. let setLike = (item) => {
  188. item.is_like = !item.is_like;
  189. item.like_count = item.is_like ? item.like_count + 1 : item.like_count - 1;
  190. };
  191. function likeClick({ item1, index1, item2, index2 }) {
  192. let item = item2 || item1;
  193. setLike(item);
  194. emit("likeFun", { params: item }, (res) => {
  195. // 请求后端失败, 重置点赞
  196. setLike(item);
  197. });
  198. }
  199. // 回复
  200. let cPopupRef = ref(null); // 弹窗实例
  201. let replyTemp = reactive({}); // 临时数据
  202. function replyClick({ item1, index1, item2, index2 }) {
  203. replyTemp = JSON.parse(JSON.stringify({ item1, index1, item2, index2 }));
  204. cPopupRef.value.open();
  205. }
  206. // 发起新评论
  207. let isNewComment = ref(false); // 是否为新评论
  208. defineExpose({ newCommentFun });
  209. function newCommentFun() {
  210. isNewComment.value = true;
  211. cPopupRef.value.open();
  212. }
  213. // 评论弹窗
  214. let focus = ref(false);
  215. function popChange(e) {
  216. // 关闭弹窗
  217. if (!e.show) {
  218. commentValue.value = ""; // 清空输入框值
  219. replyTemp = {}; // 清空被回复人信息
  220. isNewComment.value = false; // 恢复是否为新评论默认值
  221. }
  222. focus.value = e.show;
  223. }
  224. let commentValue = ref(""); // 输入框值
  225. let commentPlaceholder = ref("说点什么..."); // 输入框占位符
  226. // 发送评论
  227. function sendClick({ item1, index1, item2, index2 } = replyTemp) {
  228. let item = item2 || item1;
  229. let params = {};
  230. // 新评论
  231. if (isNewComment.value) {
  232. params = {
  233. id: Math.random(), // 评论id
  234. parent_id: null, // 父级评论id
  235. reply_id: null, // 被回复评论id
  236. reply_name: null, // 被回复人名称
  237. };
  238. } else {
  239. // 回复评论
  240. params = {
  241. id: Math.random(), // 评论id
  242. parent_id: item?.parent_id ?? item.id, // 父级评论id
  243. reply_id: item.id, // 被回复评论id
  244. reply_name: item.user_name, // 被回复人名称
  245. };
  246. }
  247. params = {
  248. ...params,
  249. user_id: props.myInfo.user_id, // 用户id
  250. user_name: props.myInfo.user_name, // 用户名
  251. user_avatar: props.myInfo.user_avatar, // 用户头像地址
  252. user_content: commentValue.value, // 用户评论内容
  253. is_like: false, // 是否点赞
  254. like_count: 0, // 点赞数统计
  255. create_time: "刚刚", // 创建时间
  256. owner: true, // 是否为所有者 所有者可以进行删除 管理员默认true
  257. };
  258. uni.showLoading({
  259. title: "正在发送",
  260. mask: true,
  261. });
  262. emit("replyFun", { params }, (res) => {
  263. uni.hideLoading();
  264. // 拿到后端返回的id赋值, 因为删除要用到id
  265. params = { ...params, id: res.id };
  266. // 新评论
  267. if (isNewComment.value) {
  268. dataList.value.push(params);
  269. } else {
  270. // 回复
  271. let c_data = dataList.value[index1];
  272. (c_data.children || (c_data.children = [])).push(params);
  273. // 如果已展开所有回复, 那么此时插入children长度会大于childrenShow长度1, 所以就直接展开显示即可
  274. if (c_data.children.length === (c_data.childrenShow || (c_data.childrenShow = [])).length + 1) {
  275. c_data.childrenShow.push(params);
  276. }
  277. }
  278. emit("update:tableTotal", props.tableTotal + 1);
  279. cPopupRef.value.close();
  280. });
  281. }
  282. // 删除
  283. const delPopupRef = ref(null);
  284. let delTemp = reactive({}); // 临时数据
  285. function deleteClick({ item1, index1, item2, index2 }) {
  286. delTemp = JSON.parse(JSON.stringify({ item1, index1, item2, index2 }));
  287. delPopupRef.value.open();
  288. }
  289. // 关闭删除弹窗
  290. function delCloseFun() {
  291. delTemp = {};
  292. delPopupRef.value.close();
  293. }
  294. // 确定删除
  295. function delConfirmFun({ item1, index1, item2, index2 } = delTemp) {
  296. let c_data = dataList.value[index1];
  297. uni.showLoading({
  298. title: "正在删除",
  299. mask: true,
  300. });
  301. // 删除二级评论
  302. if (index2 >= 0) {
  303. emit("deleteFun", { params: [c_data.children[index2].id], mode: props.deleteMode }, (res) => {
  304. uni.hideLoading();
  305. emit("update:tableTotal", props.tableTotal - 1);
  306. c_data.children.splice(index2, 1);
  307. c_data.childrenShow.splice(index2, 1);
  308. });
  309. } else {
  310. // 删除一级评论
  311. if (c_data.children && c_data.children.length) {
  312. // 如果一级评论包含回复评论
  313. switch (props.deleteMode) {
  314. case "bind":
  315. // 一级评论内容展示修改为: 当前评论内容已被移除
  316. emit(
  317. "deleteFun",
  318. {
  319. params: [c_data.id],
  320. mode: props.deleteMode,
  321. },
  322. (res) => {
  323. uni.hideLoading();
  324. c_data?.user_content = "当前评论内容已被移除";
  325. }
  326. );
  327. break;
  328. case "only":
  329. // 后端自行根据删除的一级评论id, 查找关联的子评论进行删除
  330. emit(
  331. "deleteFun",
  332. {
  333. params: [c_data.id],
  334. mode: props.deleteMode,
  335. },
  336. (res) => {
  337. uni.hideLoading();
  338. emit("update:tableTotal", props.tableTotal - c_data.children.length + 1);
  339. dataList.value.splice(index1, 1);
  340. }
  341. );
  342. break;
  343. default:
  344. // all
  345. // 收集子评论id, 提交给后端统一删除
  346. let delIdArr = [c_data.id];
  347. c_data.children.forEach((_, i) => {
  348. delIdArr.push(_.id);
  349. });
  350. emit("deleteFun", { params: delIdArr, mode: props.deleteMode }, (res) => {
  351. uni.hideLoading();
  352. emit("update:tableTotal", props.tableTotal - c_data.children.length + 1);
  353. dataList.value.splice(index1, 1);
  354. });
  355. break;
  356. }
  357. } else {
  358. // 一级评论无回复, 直接删除
  359. emit("deleteFun", { params: [c_data.id], mode: props.deleteMode }, (res) => {
  360. uni.hideLoading();
  361. emit("update:tableTotal", props.tableTotal - 1);
  362. dataList.value.splice(index1, 1);
  363. });
  364. }
  365. }
  366. delCloseFun();
  367. }
  368. // 展开评论if
  369. function expandTxtShow({ item1, index1 }) {
  370. return item1.childrenShow?.length && item1.children.length - item1.childrenShow.length;
  371. }
  372. // 展开更多评论
  373. function expandReplyFun({ item1, index1 }) {
  374. let csLen = dataList.value[index1].childrenShow.length;
  375. dataList.value[index1].childrenShow.push(
  376. ...dataList.value[index1].children.slice(csLen, csLen + 6) // 截取5条评论
  377. );
  378. }
  379. // 收起评论if
  380. function shrinkTxtShow({ item1, index1 }) {
  381. return item1.childrenShow?.length >= 2 && item1.children.length - item1.childrenShow.length === 0;
  382. }
  383. // 收起更多评论
  384. function shrinkReplyFun({ item1, index1 }) {
  385. let csLen = dataList.value[index1].childrenShow.length;
  386. dataList.value[index1].childrenShow = [];
  387. dataList.value[index1].childrenShow.push(
  388. ...dataList.value[index1].children.slice(0, 1) // 截取1条评论
  389. );
  390. }
  391. </script>
  392. <style lang="scss" scoped>
  393. ////////////////////////
  394. .center {
  395. display: flex;
  396. align-items: center;
  397. }
  398. ////////////////////////
  399. .c_total {
  400. padding: 20rpx 30rpx 0 30rpx;
  401. font-size: 28rpx;
  402. }
  403. .empty_box {
  404. display: flex;
  405. justify-content: center;
  406. align-items: center;
  407. flex-direction: column;
  408. padding: 150rpx 10rpx;
  409. font-size: 28rpx;
  410. .txt {
  411. color: $uni-text-color-disable;
  412. }
  413. .click {
  414. color: $uni-color-primary;
  415. }
  416. }
  417. .c_comment {
  418. padding: 20rpx 30rpx;
  419. font-size: 28rpx;
  420. .children_item {
  421. padding: 20rpx 30rpx;
  422. margin-top: 10rpx;
  423. margin-left: 80rpx;
  424. background-color: $uni-bg-color-grey;
  425. .expand_reply,
  426. .shrink_reply {
  427. margin-top: 10rpx;
  428. margin-left: 80rpx;
  429. .txt {
  430. font-weight: 600;
  431. color: $uni-color-primary;
  432. }
  433. }
  434. }
  435. }
  436. .c_popup_box {
  437. background-color: #fff;
  438. .reply_text {
  439. @extend .center;
  440. padding: 20rpx 20rpx 0 20rpx;
  441. font-size: 26rpx;
  442. .text_aid {
  443. color: $uni-text-color-grey;
  444. margin-right: 5rpx;
  445. }
  446. .user_avatar {
  447. width: 48rpx;
  448. height: 48rpx;
  449. border-radius: 50%;
  450. margin-right: 6rpx;
  451. margin-left: 12rpx;
  452. }
  453. .text_main {
  454. }
  455. }
  456. .content {
  457. @extend .center;
  458. .text_area {
  459. flex: 1;
  460. padding: 20rpx;
  461. }
  462. .send_btn {
  463. @extend .center;
  464. justify-content: center;
  465. width: 120rpx;
  466. height: 60rpx;
  467. border-radius: 20rpx;
  468. font-size: 28rpx;
  469. color: #fff;
  470. background-color: $uni-color-primary;
  471. margin-right: 20rpx;
  472. margin-left: 5rpx;
  473. }
  474. }
  475. }
  476. </style>