cc-comment.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  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. this.setLike(item);
  189. this.$emit("likeFun", {
  190. params: item
  191. }, (res) => {
  192. // 请求后端失败, 重置点赞
  193. setLike(item);
  194. });
  195. },
  196. // 回复
  197. replyClick({
  198. item1,
  199. index1,
  200. item2,
  201. index2
  202. }) {
  203. this.replyTemp = JSON.parse(JSON.stringify({
  204. item1,
  205. index1,
  206. item2,
  207. index2
  208. }));
  209. this.$refs["cPopupRef"].open();
  210. },
  211. // 发起新评论
  212. newCommentFun() {
  213. this.isNewComment = true;
  214. this.$refs["cPopupRef"].open();
  215. },
  216. // 评论弹窗
  217. popChange(e) {
  218. // 关闭弹窗
  219. if (!e.show) {
  220. this.commentValue = ""; // 清空输入框值
  221. this.replyTemp = {}; // 清空被回复人信息
  222. this.isNewComment = false; // 恢复是否为新评论默认值
  223. }
  224. this.focus = e.show;
  225. },
  226. // 发送评论
  227. sendClick({
  228. item1,
  229. index1,
  230. item2,
  231. index2
  232. } = this.replyTemp) {
  233. let item = item2 || item1;
  234. let params = {};
  235. // 新评论
  236. if (this.isNewComment) {
  237. params = {
  238. id: Math.random(), // 评论id
  239. parent_id: null, // 父级评论id
  240. reply_id: null, // 被回复评论id
  241. reply_name: null, // 被回复人名称
  242. };
  243. } else {
  244. // 回复评论
  245. params = {
  246. id: Math.random(), // 评论id
  247. parent_id: item?.parent_id ?? item.id, // 父级评论id
  248. reply_id: item.id, // 被回复评论id
  249. reply_name: item.user_name, // 被回复人名称
  250. };
  251. }
  252. params = {
  253. ...params,
  254. user_id: this.myInfo.user_id, // 用户id
  255. user_name: this.myInfo.user_name, // 用户名
  256. user_avatar: this.myInfo.user_avatar, // 用户头像地址
  257. user_content: this.commentValue, // 用户评论内容
  258. is_like: false, // 是否点赞
  259. like_count: 0, // 点赞数统计
  260. create_time: "刚刚", // 创建时间
  261. owner: true, // 是否为所有者 所有者可以进行删除 管理员默认true
  262. };
  263. uni.showLoading({
  264. title: "正在发送",
  265. mask: true,
  266. });
  267. this.$emit("replyFun", {
  268. params
  269. }, (res) => {
  270. uni.hideLoading();
  271. // 拿到后端返回的id赋值, 因为删除要用到id
  272. params = {
  273. ...params,
  274. id: res.id
  275. };
  276. // 新评论
  277. if (this.isNewComment) {
  278. this.dataList.push(params);
  279. } else {
  280. // 回复
  281. let c_data = this.dataList[index1];
  282. (c_data.children || (c_data.children = [])).push(params);
  283. // 如果已展开所有回复, 那么此时插入children长度会大于childrenShow长度1, 所以就直接展开显示即可
  284. if (c_data.children.length === (c_data.childrenShow || (c_data.childrenShow = [])).length +
  285. 1) {
  286. c_data.childrenShow.push(params);
  287. }
  288. }
  289. this.$emit("update:tableTotal", this.tableTotal + 1);
  290. this.$refs["cPopupRef"].close();
  291. });
  292. },
  293. //删除
  294. deleteClick({
  295. item1,
  296. index1,
  297. item2,
  298. index2
  299. }) {
  300. this.delTemp = JSON.parse(JSON.stringify({
  301. item1,
  302. index1,
  303. item2,
  304. index2
  305. }));
  306. this.$refs["delPopupRef"].open();
  307. },
  308. // 关闭删除弹窗
  309. delCloseFun() {
  310. this.delTemp = {};
  311. this.$refs["delPopupRef"].close();
  312. },
  313. // 确定删除
  314. delConfirmFun({
  315. item1,
  316. index1,
  317. item2,
  318. index2
  319. } = this.delTemp) {
  320. const deleteMode = this.deleteMode;
  321. let c_data = this.dataList[index1];
  322. uni.showLoading({
  323. title: "正在删除",
  324. mask: true,
  325. });
  326. // 删除二级评论
  327. if (index2 >= 0) {
  328. this.$emit("deleteFun", {
  329. params: [c_data.children[index2].id],
  330. mode: deleteMode
  331. }, (res) => {
  332. uni.hideLoading();
  333. this.$emit("update:tableTotal", this.tableTotal - 1);
  334. c_data.children.splice(index2, 1);
  335. c_data.childrenShow.splice(index2, 1);
  336. });
  337. } else {
  338. // 删除一级评论
  339. if (c_data?.children?.length) {
  340. // 如果一级评论包含回复评论
  341. switch (deleteMode) {
  342. case "bind":
  343. // 一级评论内容展示修改为: 当前评论内容已被移除
  344. this.$emit(
  345. "deleteFun", {
  346. params: [c_data.id],
  347. mode: deleteMode,
  348. },
  349. (res) => {
  350. uni.hideLoading();
  351. c_data.user_content = "当前评论内容已被移除";
  352. }
  353. );
  354. break;
  355. case "only":
  356. // 后端自行根据删除的一级评论id, 查找关联的子评论进行删除
  357. this.$emit(
  358. "deleteFun", {
  359. params: [c_data.id],
  360. mode: deleteMode,
  361. },
  362. (res) => {
  363. uni.hideLoading();
  364. this.$emit("update:tableTotal", this.tableTotal - c_data.children.length + 1);
  365. this.dataList.splice(index1, 1);
  366. }
  367. );
  368. break;
  369. default:
  370. // all
  371. // 收集子评论id, 提交给后端统一删除
  372. let delIdArr = [c_data.id];
  373. c_data.children.forEach((_, i) => {
  374. delIdArr.push(_.id);
  375. });
  376. this.$emit("deleteFun", {
  377. params: delIdArr,
  378. mode: deleteMode
  379. }, (res) => {
  380. uni.hideLoading();
  381. this.$emit("update:tableTotal", this.tableTotal - c_data.children.length + 1);
  382. this.dataList.splice(index1, 1);
  383. });
  384. break;
  385. }
  386. } else {
  387. // 一级评论无回复, 直接删除
  388. this.$emit("deleteFun", {
  389. params: [c_data.id],
  390. mode: deleteMode
  391. }, (res) => {
  392. uni.hideLoading();
  393. this.$emit("update:tableTotal", this.tableTotal - 1);
  394. this.dataList.splice(index1, 1);
  395. });
  396. }
  397. }
  398. this.delCloseFun();
  399. },
  400. // 展开评论if
  401. expandTxtShow({
  402. item1,
  403. index1
  404. }) {
  405. return item1.childrenShow?.length && item1.children.length - item1.childrenShow.length;
  406. },
  407. // 展开更多评论
  408. expandReplyFun({
  409. item1,
  410. index1
  411. }) {
  412. let csLen = this.dataList[index1].childrenShow.length;
  413. this.dataList[index1].childrenShow.push(
  414. ...this.dataList[index1].children.slice(csLen, csLen + 6) // 截取5条评论
  415. );
  416. },
  417. // 收起评论if
  418. shrinkTxtShow({
  419. item1,
  420. index1
  421. }) {
  422. return item1.childrenShow?.length >= 2 && item1.children.length - item1.childrenShow.length === 0;
  423. },
  424. // 收起更多评论
  425. shrinkReplyFun({
  426. item1,
  427. index1
  428. }) {
  429. this.dataList[index1].childrenShow = [];
  430. this.dataList[index1].childrenShow.push(
  431. ...this.dataList[index1].children.slice(0, 1) // 截取1条评论
  432. );
  433. },
  434. },
  435. };
  436. </script>
  437. <style lang="scss" scoped>
  438. ////////////////////////
  439. .center {
  440. display: flex;
  441. align-items: center;
  442. }
  443. ////////////////////////
  444. .c_total {
  445. padding: 20rpx 30rpx 0 30rpx;
  446. font-size: 28rpx;
  447. }
  448. .empty_box {
  449. display: flex;
  450. justify-content: center;
  451. align-items: center;
  452. flex-direction: column;
  453. padding: 150rpx 10rpx;
  454. font-size: 28rpx;
  455. .txt {
  456. color: $uni-text-color-disable;
  457. }
  458. .click {
  459. color: $uni-color-primary;
  460. }
  461. }
  462. .c_comment {
  463. padding: 20rpx 30rpx;
  464. font-size: 28rpx;
  465. .children_item {
  466. padding: 20rpx 30rpx;
  467. margin-top: 10rpx;
  468. margin-left: 80rpx;
  469. background-color: $uni-bg-color-grey;
  470. .expand_reply,
  471. .shrink_reply {
  472. margin-top: 10rpx;
  473. margin-left: 80rpx;
  474. .txt {
  475. font-weight: 600;
  476. color: $uni-color-primary;
  477. }
  478. }
  479. }
  480. }
  481. .c_popup_box {
  482. background-color: #fff;
  483. margin-bottom: 0rpx;
  484. .reply_text {
  485. @extend .center;
  486. padding: 20rpx 20rpx 0 20rpx;
  487. font-size: 26rpx;
  488. .text_aid {
  489. color: $uni-text-color-grey;
  490. margin-right: 5rpx;
  491. }
  492. .user_avatar {
  493. width: 48rpx;
  494. height: 48rpx;
  495. border-radius: 50%;
  496. margin-right: 6rpx;
  497. margin-left: 12rpx;
  498. }
  499. }
  500. .content {
  501. @extend .center;
  502. .text_area {
  503. flex: 1;
  504. padding: 20rpx;
  505. .textarea {
  506. width: 100%;
  507. min-height: 80rpx;
  508. font-size: 28rpx;
  509. color: #ff0000;
  510. background: transparent;
  511. border: 2rpx solid rgba(255,255,255,0.1);
  512. border-radius: 8rpx;
  513. padding: 16rpx;
  514. }
  515. }
  516. .send_btn {
  517. @extend .center;
  518. justify-content: center;
  519. width: 120rpx;
  520. height: 60rpx;
  521. border-radius: 20rpx;
  522. font-size: 28rpx;
  523. color: #fff;
  524. background-color: $uni-color-primary;
  525. margin-right: 20rpx;
  526. margin-left: 5rpx;
  527. }
  528. }
  529. }
  530. </style>