cc-comment.vue 14 KB

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