MomentsDetailPannel.vue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <template>
  2. <div>
  3. <div class="px-5">
  4. <div class="bg-white mt-[12px] p-4 rounded-lg">
  5. <moment-info :info="moments"></moment-info>
  6. </div>
  7. </div>
  8. <div class="title px-5 py-4 flex items-center relative">
  9. <span class="text-base font-medium">全部评论</span>
  10. <span class="text-xs text-[#797A8A] ml-[2px]">({{ totals }})</span>
  11. <img class="absolute bottom-2 left-[38px] w-[28px]" src="@/assets/png-xian.png" alt="" />
  12. </div>
  13. <div class="overflow-hidden pb-11">
  14. <div
  15. v-for="(item, index) in list"
  16. class="comment-box"
  17. :class="{ 'comment-center': showList[index] }"
  18. :key="index"
  19. ref="commentRef"
  20. >
  21. <comment-info :info="item"></comment-info>
  22. </div>
  23. <div class="loading flex items-center justify-center opacity-0" ref="loadingRef">加载中</div>
  24. <div class="empty py-6" v-if="!loading && !totals">
  25. <img class="w-3/5 block mx-[auto]" :src="isDark ? emptyDarkImg : emptyImg" alt="" />
  26. <div class="text-sm text-[#939599] text-center py-3">暂无评论</div>
  27. </div>
  28. </div>
  29. <div class="bottom bg-white px-[10px] py-[6px]">
  30. <NConfigProvider
  31. :theme-overrides="{
  32. Button: {
  33. fontSizeLarge: '24px',
  34. heightLarge: '40px'
  35. },
  36. Input: {
  37. color: '#F5F6F8',
  38. colorFocus: '#F5F6F8'
  39. }
  40. }"
  41. >
  42. <div class="flex">
  43. <n-input
  44. size="large"
  45. v-model:value="msg"
  46. type="text"
  47. @keydown.enter="send"
  48. clearable
  49. placeholder="来说点什么…"
  50. maxlength="200"
  51. />
  52. <n-button class="!ml-4" type="primary" size="large" @click="send">
  53. <template #icon>
  54. <n-icon><telegram-plane /></n-icon>
  55. </template>
  56. </n-button>
  57. </div>
  58. </NConfigProvider>
  59. </div>
  60. </div>
  61. </template>
  62. <script setup lang="ts">
  63. import { MomentInfo, CommentInfo } from '@/components/common'
  64. import { ref, watch, nextTick } from 'vue'
  65. import { useWindowScroll, useElementBounding, useWindowSize, useScroll, useIntersectionObserver } from '@vueuse/core'
  66. import { fetchGetMomentsDetail, fetchSendComment, fetchComments } from '@/api'
  67. import { useRoute } from 'vue-router'
  68. import { NInput, NButton, NIcon, NConfigProvider, useMessage, NSpin } from 'naive-ui'
  69. import { TelegramPlane } from '@vicons/fa'
  70. import { useUserStore } from '@/store'
  71. import emptyImg from '@/assets/empty.png'
  72. import emptyDarkImg from '@/assets/empty-dark.png'
  73. import { useTheme } from '@/hooks/useTheme'
  74. const { isDark } = useTheme()
  75. const route = useRoute()
  76. const momentsId = route.query.id
  77. const commentRef = ref([])
  78. const showList = ref([] as any)
  79. const { y } = useWindowScroll()
  80. const { width, height } = useWindowSize()
  81. const list = ref([] as any)
  82. const totals = ref(0)
  83. const page = ref(1)
  84. const finish = ref(false)
  85. function getComments(isSend = false) {
  86. loading.value = true
  87. let data = {
  88. page: {
  89. page: page.value,
  90. limit: 20
  91. },
  92. search: {
  93. where: {
  94. momentsId: momentsId
  95. },
  96. order: {
  97. createdAt: 'DESC'
  98. }
  99. }
  100. }
  101. if (isSend) {
  102. data.page = {
  103. page: 1,
  104. limit: 1
  105. }
  106. }
  107. fetchComments(data).then((res: any) => {
  108. loading.value = false
  109. if (isSend) {
  110. list.value = [...res.items, ...list.value]
  111. } else {
  112. list.value = [...list.value, ...res.items]
  113. }
  114. totals.value = res.meta.totalItems
  115. if (res.meta.totalPages <= res.meta.currentPage) {
  116. finish.value = true
  117. } else {
  118. finish.value = false
  119. page.value = page.value + 1
  120. }
  121. })
  122. }
  123. watch(
  124. y,
  125. () => {
  126. nextTick(() => {
  127. showList.value = commentRef.value.map((item: any) => {
  128. return useElementBounding(item).top.value < height.value - 30
  129. })
  130. })
  131. },
  132. {
  133. immediate: true
  134. }
  135. )
  136. watch(
  137. list,
  138. () => {
  139. nextTick(() => {
  140. showList.value = commentRef.value.map((item: any) => {
  141. return useElementBounding(item).top.value < height.value - 30
  142. })
  143. })
  144. },
  145. {
  146. immediate: true
  147. }
  148. )
  149. const moments = ref({})
  150. fetchGetMomentsDetail(momentsId).then((res: any) => {
  151. moments.value = res
  152. })
  153. const msg = ref('')
  154. const message = useMessage()
  155. const userStore = useUserStore()
  156. function send() {
  157. if (!msg.value) {
  158. message.info('评论不能为空')
  159. return
  160. }
  161. fetchSendComment({
  162. content: msg.value,
  163. userId: userStore.userInfo.id,
  164. userName: userStore.userInfo.name,
  165. momentsId: momentsId
  166. }).then(res => {
  167. message.success('发送成功')
  168. msg.value = ''
  169. getComments(true)
  170. window.scrollTo(0, 0)
  171. })
  172. }
  173. const loadingRef = ref(null)
  174. const loading = ref(false)
  175. useIntersectionObserver(loadingRef, ([{ isIntersecting }]) => {
  176. if (isIntersecting && !loading.value && !finish.value) {
  177. getComments()
  178. }
  179. })
  180. </script>
  181. <style lang="less" scoped>
  182. .comment-box {
  183. opacity: 0.2;
  184. transition: all ease-in-out 0.3s;
  185. &:nth-child(2n) {
  186. transform: translate(-50px, 90px);
  187. }
  188. &:nth-child(2n + 1) {
  189. transform: translate(50px, 90px);
  190. }
  191. &.comment-center {
  192. transform: translate(0, 0);
  193. opacity: 1;
  194. }
  195. }
  196. .bottom {
  197. position: fixed;
  198. bottom: 0;
  199. z-index: 20;
  200. left: 0;
  201. right: 0;
  202. }
  203. </style>