index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. <template>
  2. <z-paging
  3. ref="paging"
  4. use-page-scroll
  5. @closeF2="closeF2"
  6. v-model="oneCommentList"
  7. @query="queryList"
  8. class="comment-paging"
  9. >
  10. <view
  11. class="comment-box"
  12. v-for="oneItem in oneCommentList"
  13. :key="oneItem.id"
  14. >
  15. <!-- 一级评论 -->
  16. <view
  17. :class="[
  18. 'item-comment',
  19. oneItem.replyList && oneItem.replyList.length > 0 ? 'child' : '',
  20. ]"
  21. >
  22. <view class="avatar">
  23. <up-avatar
  24. @click="toUserPage(oneItem.userId)"
  25. shape="circle"
  26. :src="oneItem.usePicture"
  27. ></up-avatar>
  28. </view>
  29. <view class="content">
  30. <view class="user-box" @click="toUserPage(oneItem.userId)">
  31. <text class="username">{{ oneItem.userName }}</text>
  32. <text class="author-tag" v-if="oneItem.authorMark">作者</text>
  33. </view>
  34. <view
  35. @longpress="longpress"
  36. :data-one-level-id="oneItem.id"
  37. class="reply-box"
  38. @click="handleClickComment(oneItem)"
  39. >
  40. <text class="text">{{ oneItem.content }}</text>
  41. <text class="time">{{
  42. timeFormat(oneItem.createTime, "yyyy-mm-dd")
  43. }}</text>
  44. <text class="reply-tip">回复</text>
  45. </view>
  46. </view>
  47. <view class="right-box">
  48. <uni-icons
  49. v-show="!oneItem.likeMark"
  50. customPrefix="iconfont"
  51. size="18"
  52. color="#2E2E2E"
  53. class="icon-state icon-dianzan"
  54. :class="{
  55. animated: likeAnimationIds.includes(oneItem.id),
  56. heartBeat: likeAnimationIds.includes(oneItem.id),
  57. }"
  58. />
  59. <uni-icons
  60. v-show="oneItem.likeMark"
  61. customPrefix="iconfont"
  62. size="18"
  63. color="#FF2442"
  64. class="icon-state icon-dianzanxuanzhong"
  65. :class="{
  66. animated: likeAnimationIds.includes(oneItem.id),
  67. heartBeat: likeAnimationIds.includes(oneItem.id),
  68. }"
  69. />
  70. <text class="count">{{ oneItem.likeCount }}</text>
  71. </view>
  72. </view>
  73. <!-- 二级评论 -->
  74. <div
  75. class="child-comment-box"
  76. v-if="oneItem.replyList && oneItem.replyList.length > 0"
  77. >
  78. <view
  79. class="item-comment sub-comment"
  80. v-for="(twoItem, idx) in oneItem.replyList"
  81. :key="idx"
  82. >
  83. <view class="avatar" >
  84. <up-avatar
  85. class="sub-avatar"
  86. shape="circle"
  87. size="25"
  88. :src="twoItem.usePicture"
  89. ></up-avatar>
  90. </view>
  91. <view class="content">
  92. <view class="user-box" >
  93. <text class="username">{{ twoItem.userName }}</text>
  94. <text class="author-tag" v-if="twoItem.authorMark">作者</text>
  95. </view>
  96. <view
  97. @longpress="longpress"
  98. :data-one-level-id="oneItem.id"
  99. :data-two-level-id="twoItem.id"
  100. class="reply-box"
  101. >
  102. <text class="text" v-if="twoItem.replyMark"
  103. >回复<text>{{ twoItem.replyName }}:&nbsp;</text
  104. >{{ twoItem.content }}</text
  105. >
  106. <text class="text" v-else>{{ twoItem.content }}</text>
  107. <text class="time">{{
  108. timeFormat(twoItem.createTime, "yyyy-mm-dd")
  109. }}</text>
  110. <text class="reply-tip">回复</text>
  111. </view>
  112. </view>
  113. <view class="right-box">
  114. <uni-icons
  115. v-show="!twoItem.likeMark"
  116. customPrefix="iconfont"
  117. size="18"
  118. color="#2E2E2E"
  119. class="icon-state icon-dianzan"
  120. :class="{
  121. animated: likeAnimationIds.includes(twoItem.id),
  122. heartBeat: likeAnimationIds.includes(twoItem.id),
  123. }"
  124. />
  125. <uni-icons
  126. v-show="twoItem.likeMark"
  127. customPrefix="iconfont"
  128. size="18"
  129. color="#FF2442"
  130. class="icon-state icon-dianzanxuanzhong"
  131. :class="{
  132. animated: likeAnimationIds.includes(twoItem.id),
  133. heartBeat: likeAnimationIds.includes(twoItem.id),
  134. }"
  135. />
  136. <text class="count">{{ twoItem.likeCount }}</text>
  137. </view>
  138. </view>
  139. <view
  140. v-if="
  141. oneItem.replyList &&
  142. oneItem.replyList.length > 0 &&
  143. oneItem.replyPage.hasMore &&
  144. oneItem.replyCount > 1
  145. "
  146. class="load-more"
  147. >
  148. <!-- <text>展示{{ twoItem.allReply }}条回复</text> -->
  149. <text>展开更多回复</text>
  150. </view>
  151. </div>
  152. </view>
  153. </z-paging>
  154. <up-action-sheet
  155. :actions="actionList"
  156. :closeOnClickOverlay="true"
  157. cancelText="取消"
  158. :show="showActionSheet"
  159. @close="closeActionSheet"
  160. @select="clickActionSheet"
  161. ></up-action-sheet>
  162. </template>
  163. <script setup>
  164. import { ref, computed } from "vue";
  165. import useZPaging from "@/uni_modules/z-paging/components/z-paging/js/hooks/useZPaging.js";
  166. import { onLoad } from "@dcloudio/uni-app";
  167. // import {
  168. // getCommentOne,
  169. // getCommentTwo,
  170. // setUserState,
  171. // addComment,
  172. // deleteComment,
  173. // } from "@/api/book";
  174. import { timeFormat } from "@/uni_modules/uview-plus";
  175. import { useToast } from "@/hooks/useToast";
  176. const { Toast } = useToast();
  177. const emit = defineEmits(["clickComment", "moveMessageTop", "inputDone"]);
  178. // defineExpose({ handleAddComment });
  179. const props = defineProps({
  180. /**
  181. * @param add 新增评论
  182. * @param reply 回复评论
  183. */
  184. commentType: {
  185. type: String,
  186. default: "add",
  187. },
  188. });
  189. const showActionSheet = ref(false);
  190. function closeActionSheet() {
  191. showActionSheet.value = false;
  192. }
  193. function clickActionSheet(item) {
  194. if (item.value === 0) {
  195. // 删除评论
  196. handleDeleteComment();
  197. } else if (item.value === 1) {
  198. // 回复评论
  199. let comment = null;
  200. if (pressTwoLevelId.value !== 0) {
  201. // 二级评论
  202. const result = findCommentById(pressTwoLevelId.value, "two");
  203. if (result && result.twoItem) {
  204. comment = result.twoItem;
  205. }
  206. } else if (pressOneLevelId.value !== 0) {
  207. // 一级评论
  208. comment = findCommentById(pressOneLevelId.value, "one");
  209. }
  210. if (comment) {
  211. handleClickComment(comment);
  212. }
  213. }
  214. showActionSheet.value = false;
  215. }
  216. // 删除评论
  217. async function handleDeleteComment() {
  218. try {
  219. await deleteComment(deleteId.value);
  220. Toast({ title: "删除成功" });
  221. removeItemComment();
  222. } catch (error) {
  223. console.error("deleteComment", error);
  224. } finally {
  225. pressOneLevelId.value = pressTwoLevelId.value = deleteId.value = 0;
  226. }
  227. }
  228. // 接口删除成功后 需要将数据列表中的评论删除
  229. function removeItemComment() {
  230. // 删除一级评论
  231. if (pressOneLevelId.value !== 0 && pressTwoLevelId.value === 0) {
  232. const oneItem = findCommentById(pressOneLevelId.value, "one");
  233. if (oneItem) {
  234. const itemIndex = oneCommentList.value.findIndex(
  235. (v) => v.id === oneItem.id
  236. );
  237. if (itemIndex !== -1) {
  238. oneCommentList.value.splice(itemIndex, 1);
  239. }
  240. }
  241. } else if (pressTwoLevelId.value !== 0) {
  242. // 删除二级评论
  243. const result = findCommentById(pressTwoLevelId.value, "two");
  244. if (result && result.oneItem && result.twoItem) {
  245. const twoIndex = result.oneItem.replyList.findIndex(
  246. (two) => two.id === result.twoItem.id
  247. );
  248. if (twoIndex !== -1) {
  249. result.oneItem.replyList.splice(twoIndex, 1);
  250. }
  251. }
  252. }
  253. }
  254. // 根据id查找评论(一级或二级)
  255. function findCommentById(id, type = "one") {
  256. if (type === "one") {
  257. // 查找一级评论
  258. return oneCommentList.value.find((item) => item.id === id) || null;
  259. } else if (type === "two") {
  260. // 查找二级评论,返回 {oneItem, twoItem}
  261. for (const oneItem of oneCommentList.value) {
  262. if (Array.isArray(oneItem.replyList)) {
  263. const twoItem = oneItem.replyList.find((two) => two.id === id);
  264. if (twoItem) {
  265. return { oneItem, twoItem };
  266. }
  267. }
  268. }
  269. return null;
  270. }
  271. return null;
  272. }
  273. // 长按某条评论时触发
  274. const deleteId = ref(0);
  275. const pressOneLevelId = ref(0);
  276. const pressTwoLevelId = ref(0);
  277. function longpress(event) {
  278. // 如果是二级评论 直接删除二级
  279. if (event.currentTarget.dataset?.twoLevelId) {
  280. deleteId.value = pressTwoLevelId.value =
  281. event.currentTarget.dataset.twoLevelId;
  282. // 没有二级评论 删除一级
  283. } else if (event.currentTarget.dataset?.oneLevelId) {
  284. deleteId.value = pressOneLevelId.value =
  285. event.currentTarget.dataset?.oneLevelId;
  286. }
  287. showActionSheet.value = true;
  288. }
  289. const actionList = computed(() => {
  290. return [
  291. { name: "删除", value: 0 },
  292. { name: "回复", value: 1 },
  293. ];
  294. });
  295. const articleId = ref("");
  296. onLoad((options) => {
  297. if (!options.id) return;
  298. articleId.value = options.id;
  299. });
  300. // // 获取一级评论
  301. const oneCommentList = ref([]);
  302. // async function fetchCommentOne(page, pageSize) {
  303. // try {
  304. // const params = {
  305. // page,
  306. // limit: pageSize,
  307. // bookId: articleId.value,
  308. // };
  309. // const { data } = await getCommentOne(params);
  310. // const list = data.list.map((item) => {
  311. // return {
  312. // ...item,
  313. // replyList: item.replyList || [],
  314. // replyPage: {
  315. // page: 1,
  316. // limit: 5,
  317. // hasMore: true,
  318. // loading: false,
  319. // },
  320. // };
  321. // });
  322. // paging.value.completeByTotal(list, data.total);
  323. // } catch (error) {
  324. // paging.value.complete(false)
  325. // console.error("oneCommentList error", error);
  326. // }
  327. // }
  328. // // 获取二级评论
  329. // const twoCommentTotalPage = ref(1);
  330. // async function loadMoreComment(oneItem) {
  331. // try {
  332. // const params = {
  333. // page: oneItem.replyPage.page,
  334. // limit: oneItem.replyPage.limit,
  335. // bookId: articleId.value,
  336. // oneLevelId: oneItem.id,
  337. // };
  338. // const { data } = await getCommentTwo(params);
  339. // twoCommentTotalPage.value = data.totalPage;
  340. // if (oneItem.replyPage.page >= data.totalPage) {
  341. // oneItem.replyPage.hasMore = false;
  342. // }
  343. // // 当第一页时直接赋值
  344. // if (oneItem.replyPage.page === 1) {
  345. // oneItem.replyList = data.list;
  346. // } else {
  347. // // 更多页时才合并数据
  348. // oneItem.replyList = [...oneItem.replyList, ...data.list];
  349. // }
  350. // oneItem.replyPage.page += 1;
  351. // } catch (error) {
  352. // console.error("loadMoreComment error", error);
  353. // }
  354. // }
  355. // 点击评论触发弹窗事件
  356. const parentId = ref(0);
  357. const currentReplyId = ref(0);
  358. const replyId = ref(0);
  359. const oneLevelId = ref(0);
  360. function handleClickComment(comment) {
  361. // 点击了一级评论
  362. if (comment.parentId === 0) {
  363. currentReplyId.value = comment.id;
  364. parentId.value = comment.id;
  365. oneLevelId.value = comment.id;
  366. } else {
  367. // 点击了二级评论
  368. currentReplyId.value = comment.id;
  369. parentId.value = comment.parentId;
  370. oneLevelId.value = comment.oneLevelId;
  371. }
  372. replyId.value = comment.userId;
  373. emit("clickComment", comment);
  374. }
  375. // // 评论事件
  376. // async function handleAddComment(value) {
  377. // try {
  378. // // 回复评论参数
  379. // const replyParams = {
  380. // bookId: articleId.value, // 文章ID
  381. // content: value, // 评论内容
  382. // parentId: currentReplyId.value, // 父评论ID
  383. // replyId: replyId.value, // 回复的用户ID
  384. // oneLevelId: oneLevelId.value,
  385. // };
  386. // // 新增评论参数
  387. // const addParams = {
  388. // bookId: articleId.value,
  389. // content: value,
  390. // };
  391. // let params = props.commentType === "add" ? addParams : replyParams;
  392. // let { data } = await addComment(params);
  393. // Toast({ title: "您的评论已发布" });
  394. // // 新增一级评论
  395. // if (props.commentType === "add") {
  396. // data.replyPage = {
  397. // page: 1,
  398. // limit: 5,
  399. // hasMore: true,
  400. // loading: false,
  401. // };
  402. // // oneCommentList.value.unshift(data);
  403. // paging.value.addDataFromTop(data);
  404. // // 新增评论后定位评论区到最顶部
  405. // emit("moveMessageTop");
  406. // } else {
  407. // // 回复别人的评论
  408. // console.log("oneLevelId", oneLevelId.value);
  409. // // 根据oneLevelId找到这一条一级评论
  410. // const oneCommentItem = oneCommentList.value.find(
  411. // (v) => v.id === oneLevelId.value
  412. // );
  413. // console.log("oneCommentItem", oneCommentItem);
  414. // // 一级评论可能没有子评论接口返回为null,这里需要初始化一下
  415. // if (
  416. // !oneCommentItem?.replyList ||
  417. // !Array.isArray(oneCommentItem?.replyList)
  418. // ) {
  419. // oneCommentItem.replyList = [];
  420. // }
  421. // // 向子评论列表的第一项插入最新评论
  422. // // paging.value.addDataFromTop(data);
  423. // oneCommentItem.replyList.unshift(data);
  424. // // 如果二级评论列表的page大于 二级评论的总页数,隐藏加载更多
  425. // if (oneCommentItem.replyPage.page >= twoCommentTotalPage.value) {
  426. // oneCommentItem.replyPage.hasMore = false;
  427. // }
  428. // console.log("oneCommentItem", oneCommentItem);
  429. // }
  430. // emit("inputDone");
  431. // } catch (error) {
  432. // console.error("addComment error", error);
  433. // emit("inputDone");
  434. // Toast({ title: "发送失败" });
  435. // }
  436. // }
  437. function closeF2() {}
  438. // paging相关
  439. const paging = ref(null);
  440. useZPaging(paging);
  441. // z-paging绑定的数据刷新时调用
  442. function queryList(page, pageSize) {
  443. // fetchCommentOne(page, pageSize);
  444. }
  445. // 点赞相关
  446. const likeLoading = ref(false);
  447. const likeAnimationIds = ref([]);
  448. // async function handleLike(item) {
  449. // try {
  450. // if (likeLoading.value) return;
  451. // likeLoading.value = true;
  452. // await setUserState({ type: 3, commentId: item.id });
  453. // item.likeMark = !item.likeMark;
  454. // if (item.likeMark) {
  455. // item.likeCount += 1;
  456. // const idx = likeAnimationIds.value.indexOf(item.id);
  457. // if (idx > -1) likeAnimationIds.value.splice(idx, 1);
  458. // likeAnimationIds.value.push(item.id);
  459. // } else {
  460. // item.likeCount -= 1;
  461. // const idx2 = likeAnimationIds.value.indexOf(item.id);
  462. // if (idx2 > -1) likeAnimationIds.value.splice(idx2, 1);
  463. // }
  464. // likeLoading.value = false;
  465. // } catch (error) {
  466. // likeLoading.value = false;
  467. // Toast({ title: "点赞失败" });
  468. // console.error("handleLike", error);
  469. // }
  470. // }
  471. // 跳转用户详情页
  472. function toUserPage(id) {
  473. uni.navigateTo({ url: `/pages/user/personal?id=${id}` });
  474. }
  475. </script>
  476. <style lang="scss" scoped>
  477. .comment-paging {
  478. // min-height: 80vh;
  479. // height: 500px;
  480. }
  481. .comment-box {
  482. // display: flex;
  483. }
  484. .item-comment {
  485. width: 100%;
  486. display: flex;
  487. margin-bottom: 50rpx;
  488. &.child {
  489. margin-bottom: 0;
  490. }
  491. .avatar {
  492. margin-right: 20rpx;
  493. }
  494. .content {
  495. flex: 1;
  496. font-size: 26rpx;
  497. .user-box {
  498. color: #9d9d9d;
  499. .author-tag {
  500. color: rgb(246, 40, 77);
  501. background-color: rgba(246, 40, 77, 0.2);
  502. padding: 2rpx 10rpx;
  503. border-radius: 50rpx;
  504. font-size: 20rpx;
  505. margin-left: 10rpx;
  506. font-weight: 700;
  507. }
  508. }
  509. .reply-box {
  510. display: flex;
  511. flex-wrap: wrap;
  512. align-items: center;
  513. line-height: 38rpx;
  514. margin: 8rpx 0;
  515. .text {
  516. overflow-wrap: break-word;
  517. margin-right: 16rpx;
  518. }
  519. .time {
  520. // margin-left: 16rpx;
  521. white-space: nowrap;
  522. color: #999;
  523. font-size: 22rpx;
  524. margin-right: 10rpx;
  525. }
  526. .reply-tip {
  527. font-size: 22rpx;
  528. color: #999;
  529. }
  530. }
  531. }
  532. .right-box {
  533. display: flex;
  534. align-items: center;
  535. .icon-state {
  536. margin-right: 5rpx;
  537. // vertical-align: mid;
  538. }
  539. .count {
  540. // vertical-align: middle;
  541. }
  542. }
  543. }
  544. .child-comment-box {
  545. padding-left: 80rpx;
  546. margin-bottom: 50rpx;
  547. .load-more {
  548. padding-left: 65rpx;
  549. color: #223b72;
  550. font-size: 26rpx;
  551. margin-top: 10rpx;
  552. }
  553. .sub-comment {
  554. margin-top: 20rpx;
  555. margin-bottom: 0;
  556. .avatar {
  557. margin-right: 15rpx;
  558. }
  559. .reply-box {
  560. display: block; // 关键:不要用flex
  561. .text,
  562. .time,
  563. .reply-tip {
  564. display: inline;
  565. vertical-align: middle;
  566. }
  567. .text {
  568. margin-right: 10rpx;
  569. overflow-wrap: break-word;
  570. line-height: 35rpx;
  571. }
  572. .time {
  573. color: #999;
  574. font-size: 22rpx;
  575. margin-right: 10rpx;
  576. white-space: nowrap;
  577. }
  578. .reply-tip {
  579. font-size: 22rpx;
  580. color: #999;
  581. }
  582. }
  583. }
  584. }
  585. </style>