single.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. <template>
  2. <div>
  3. <van-nav-bar fixed @click-left="onClickLeft" @click-right="onClickRight">
  4. <template #left>
  5. <div>
  6. <van-icon name="arrow-left" color="black" size="18" />
  7. </div>
  8. </template>
  9. <template #title>
  10. <div class="text-2xl text-black-3 text-semibold">
  11. <div class="inline-block w-220 line-clamp-1">
  12. {{ title }}
  13. </div>
  14. <!-- <span class="ml-7 iconfont icon-close-remind text-black-9" style="font-size: 16px"></span> -->
  15. </div>
  16. </template>
  17. <template #right>
  18. <van-icon name="ellipsis" color="black" size="18" />
  19. </template>
  20. </van-nav-bar>
  21. <van-pull-refresh v-model="loading" @refresh="onRefresh">
  22. <div class="w-full h-[100vh] pt-55">
  23. <div ref="messageBoxRef" class="w-full box-border px-12 overflow-y-auto scrollbar">
  24. <van-list
  25. v-model:loading="loading"
  26. :finished="finished"
  27. finished-text="没有更多了"
  28. @load="onLoad"
  29. >
  30. <template v-for="(item, index) in receiveGetter" :key="index">
  31. <!-- 右侧是自己的消息 -->
  32. <template v-if="item.sendUserId == user.pass">
  33. <!-- 右侧消息 图片 -->
  34. <template v-if="item.messageType == 1">
  35. <div class="w-full text-center text-black/[0.4] mt-24 text-sm">
  36. {{ item.createTime }}
  37. </div>
  38. <div class="flex justify-end items-start">
  39. <div
  40. class="mr-10 text-left inline-block min-h-46 max-w-full break-words overflow-auto bg-[#FEF4E6] box-border text-base p-12 text-wrap"
  41. >
  42. {{ item.messageContent }}
  43. </div>
  44. <div class="w-40 h-40 rounded-full overflow-hidden shrink-0">
  45. <img
  46. v-if="user?.headImageUrl"
  47. class="w-full h-full object-cover"
  48. :src="user?.headImageUrl"
  49. alt=""
  50. />
  51. <img
  52. v-else
  53. class="w-full h-full object-cover"
  54. src="~/assets/img/default_avatar.png"
  55. alt=""
  56. />
  57. </div>
  58. </div>
  59. </template>
  60. <!-- 文字消息 -->
  61. <template v-if="item.messageType == 0 && item.messageContent.trim()">
  62. <div class="w-full text-center text-black/[0.4] mt-24 text-sm">
  63. {{ formatTimestamp(item.createTime) }}
  64. </div>
  65. <div class="flex justify-end items-start">
  66. <div
  67. class="mr-10 text-left inline-block min-h-46 max-w-full break-words overflow-auto rounded-b-2xl rounded-tl-2xl bg-white box-border text-base p-12 text-wrap"
  68. >
  69. {{ messageContentParse(item.messageContent) }}
  70. </div>
  71. </div>
  72. <div class="w-40 h-40 rounded-full overflow-hidden shrink-0">
  73. <img
  74. v-if="user?.headImageUrl"
  75. class="w-full h-full object-cover"
  76. :src="user?.headImageUrl"
  77. alt=""
  78. />
  79. <img
  80. v-else
  81. class="w-full h-full object-cover"
  82. src="~/assets/img/default_avatar.png"
  83. alt=""
  84. />
  85. </div>
  86. </template>
  87. </template>
  88. <!-- 左侧他人的消息 -->
  89. <template v-else>
  90. <!-- 左侧消息 图片 -->
  91. <template v-if="item.messageType == 1">
  92. <div class="w-full text-center text-black/[0.4] mt-24 text-sm">
  93. {{ item.createTime }}
  94. </div>
  95. <div class="flex justify-end items-start">
  96. <div
  97. class="mr-10 text-left inline-block min-h-46 max-w-full break-words overflow-auto rounded-[4px] bg-white box-border text-base p-12 text-wrap"
  98. >
  99. <!-- {{ item.messageContent }} -->
  100. </div>
  101. <div class="w-40 h-40 rounded-full overflow-hidden shrink-0">
  102. <img
  103. v-if="user?.headImageUrl"
  104. class="w-full h-full object-cover"
  105. :src="user?.headImageUrl"
  106. alt=""
  107. />
  108. <img
  109. v-else
  110. class="w-full h-full object-cover"
  111. src="~/assets/img/default_avatar.png"
  112. alt=""
  113. />
  114. </div>
  115. </div>
  116. </template>
  117. <!-- 左侧文字 -->
  118. <!-- <template v-if="item.messageType == 0 && item.messageContent.trim()"> -->
  119. <div class="w-full text-center text-black/[0.4] mt-24 text-sm">
  120. {{ formatTimestamp(item.createTime) }}
  121. </div>
  122. <div class="flex justify-start items-start">
  123. <div class="w-40 h-40 rounded-full overflow-hidden shrink-0">
  124. <img
  125. v-if="user?.headImageUrl"
  126. class="w-full h-full object-cover"
  127. :src="user?.headImageUrl"
  128. alt=""
  129. />
  130. <img
  131. v-else
  132. class="w-full h-full object-cover"
  133. src="~/assets/img/default_avatar.png"
  134. alt=""
  135. />
  136. </div>
  137. <div
  138. class="ml-8 inline-block rounded-b-2xl rounded-tr-2xl min-h-46 max-w-full break-words overflow-auto bg-[#F3F3F3] box-border text-base p-12 text-wrap"
  139. >
  140. {{ messageContentParse(item.messageContent) }}
  141. </div>
  142. </div>
  143. <!-- </template> -->
  144. </template>
  145. </template>
  146. <div ref="msgBottomRef"></div>
  147. </van-list>
  148. </div>
  149. </div>
  150. </van-pull-refresh>
  151. <ProfileNewsChatInput
  152. :shareGroup="false"
  153. @on-select-Img="selectImg"
  154. @on-send-message="sendMessage"
  155. ></ProfileNewsChatInput>
  156. </div>
  157. </template>
  158. <script setup>
  159. import { messageContentParse, formatTimestamp } from '~/utils/detalTime'
  160. const route = useRoute()
  161. const router = useRouter()
  162. const uploadUrl = `${import.meta.env.VITE_APP_BASE_URL}/website/tourMessage/upload`
  163. const chatStore = useChatStore()
  164. const { ws, curConversiton, receive, receiveGetter, connectSta, onNewMessage } =
  165. storeToRefs(chatStore)
  166. const user = computed(() => chatStore.user)
  167. // 消息接收者的用户id
  168. const getUserId = computed(() => curConversiton.value.toUserId)
  169. // 消息发送者:当前登录用户的加密id
  170. const sendUserId = computed(() => user.value.pass)
  171. // 会话id
  172. const groupId = computed(() => route.query.groupId)
  173. // 用户在群聊中艾特的人
  174. const specialUserId = ref('')
  175. // 用户输入的文本消息
  176. const messageContent = ref('')
  177. // 聊天类型 1单聊 2群聊 3系统消息 4关注信息
  178. const noticeType = computed(() => curConversiton.value.noticeType)
  179. const messageBoxRef = ref(null)
  180. const msgBottomRef = ref(null)
  181. const inputBoxRef = ref(null)
  182. // 当前websocket连接状态 0: 未连接 1: 连接中 2: 已连接 3: 已断开
  183. const wsConnect = computed(() => connectSta.value)
  184. const followStatus = ref(0)
  185. const querySwParams = reactive({
  186. getUserId: computed(() => route.query.getUserId ?? ''),
  187. groupId: computed(() => route.query.groupId ?? ''),
  188. sendUserId: computed(() => route.query.sendUserId ?? '')
  189. })
  190. // 本地生成一个唯一消息id
  191. function getLocalId() {
  192. const random = Math.floor(Math.random() * 10000)
  193. return Date.now() + '' + random
  194. }
  195. // 刷新次数
  196. const count = ref(0)
  197. const loading = ref(false)
  198. const finished = ref(false)
  199. const messageList = ref([])
  200. // 单聊的标题
  201. const title = computed(() => route.query.groupRemark)
  202. // 刷新
  203. const onRefresh = () => {}
  204. const onClickLeft = () => router.back()
  205. const onClickRight = () => {
  206. navigateTo({
  207. path: '/chat/set-single',
  208. query: {
  209. toUserId: route.query.getUserId
  210. }
  211. })
  212. }
  213. // 发送消息的方法
  214. // 发送文本消息
  215. function sendMessage(messageParams) {
  216. console.log(messageParams, 'messageParams')
  217. console.log(getUserId.value, '5555')
  218. console.log(noticeType, '5555')
  219. if (!messageParams.trim()) return
  220. let msg = {
  221. // getUserId: getUserId.value,
  222. // sendUserId: sendUserId.value,
  223. // groupId: groupId.value,
  224. ...querySwParams,
  225. specialUserId: specialUserId.value,
  226. messageContent: messageParams,
  227. messageType: 0,
  228. noticeType: 1,
  229. object: {
  230. id: getLocalId()
  231. }
  232. }
  233. receive.value.push({
  234. ...msg,
  235. messageContent: JSON.stringify({ messageContent: msg.messageContent })
  236. })
  237. messageContent.value = ''
  238. messageBoxRef.value.scrollTop = messageBoxRef.value.scrollHeight
  239. msg = JSON.stringify(msg)
  240. ws.value.send(msg)
  241. }
  242. const pageSize = ref(10)
  243. const messageCount = ref(0)
  244. // 获取聊天记录
  245. async function getChatHistory(messageId = '') {
  246. if (curConversiton.value.isLocal) return
  247. if (!groupId.value) return
  248. if (receive.value.length >= messageCount.value && receive.value.length > 0) return
  249. if (receive.value.length && !messageId) return
  250. const query = {
  251. pageSize: pageSize.value,
  252. groupId: groupId.value,
  253. messageId
  254. }
  255. const {
  256. data: { data = [], count = [] }
  257. } = await request('/website/tourMessage/getMessageByGroupId', { query })
  258. messageCount.value = count || 0
  259. if (Array.isArray(data)) {
  260. receive.value = [...data, ...receive.value]
  261. }
  262. // 获取到数据后,滚动到底部
  263. if (!messageId) {
  264. nextTick(() => {
  265. setTimeout(() => {
  266. messageBoxRef.value && messageBoxRef.value.scrollTo({ top: msgBottomRef.value.offsetTop })
  267. }, 100)
  268. })
  269. }
  270. console.log('历史记录:', data)
  271. }
  272. // 监听输入框的enter事件
  273. function addEventListenerTextarea() {
  274. nextTick(() => {
  275. if (inputBoxRef.value) {
  276. inputBoxRef.value.removeEventListener('keydown', () => {})
  277. inputBoxRef.value.addEventListener('keydown', function (event) {
  278. if (event.key === 'Enter') {
  279. event.preventDefault()
  280. sendMessage()
  281. }
  282. })
  283. }
  284. })
  285. }
  286. // 监听消息列表的滚动事件
  287. function addEventListenerMessage() {
  288. nextTick(() => {
  289. if (messageBoxRef.value) {
  290. messageBoxRef.value.removeEventListener('scroll', () => {})
  291. messageBoxRef.value.addEventListener('scroll', (e) => {
  292. if (messageBoxRef.value.scrollTop == 0 && receive.value[0]?.id) {
  293. console.log('滚动到顶部了')
  294. console.log(receive.value[0].id)
  295. getChatHistory(receive.value[0].id)
  296. }
  297. })
  298. }
  299. })
  300. }
  301. // 选择发送图片
  302. function selectImg(e) {
  303. const file = e.target.files[0]
  304. const { size, type, name } = file
  305. const IMIETypes = ['image/jpeg', 'image/png', 'image/gif']
  306. if (!IMIETypes.includes(type)) {
  307. showToast('请上传图片')
  308. return
  309. }
  310. const maxSize = 1024 * 1024 * 20
  311. if (size > maxSize) {
  312. showFailToast('图片大小不能超过10M')
  313. return
  314. }
  315. const formData = new FormData()
  316. formData.append('uploadFile', file)
  317. formData.append('fieldName', 'messageContent')
  318. formData.append('asImage', true)
  319. request(uploadUrl, { method: 'post', body: formData }).then((res) => {
  320. const {
  321. data: { fileUrl }
  322. } = res
  323. if (fileUrl) {
  324. const msg = {
  325. getUserId: getUserId.value,
  326. sendUserId: sendUserId.value,
  327. specialUserId: '',
  328. groupId: groupId.value,
  329. messageContent: fileUrl,
  330. messageType: 1,
  331. noticeType: noticeType.value,
  332. object: {
  333. id: getLocalId()
  334. }
  335. }
  336. ws.value.send(JSON.stringify(msg))
  337. }
  338. })
  339. }
  340. // 获取我与对方的关注情况
  341. async function isFollow() {
  342. if (noticeType.value !== 1) return //只有单聊中才需要获取关注情况
  343. const query = {
  344. userId: curConversiton.value.toUserId
  345. }
  346. const { data: status = 0 } = await request('/website/tourGroup/isFollow', { query })
  347. followStatus.value = status
  348. console.log('关注情况:', status)
  349. }
  350. watch(groupId, async () => {
  351. // 消息置空
  352. receive.value = []
  353. // 监听消息输入框键盘enter事件
  354. addEventListenerTextarea()
  355. // 监听聊天框消息滚动事件
  356. addEventListenerMessage()
  357. // 获取与当前会话用户的关注状态
  358. isFollow()
  359. // 获取前会话用户的聊天记录
  360. getChatHistory()
  361. })
  362. watch(onNewMessage, getUserId, sendUserId, () => {})
  363. onMounted(() => {
  364. // 获取前会话用户的聊天记录
  365. getChatHistory()
  366. addEventListenerTextarea()
  367. addEventListenerMessage()
  368. // 获取与当前会话用户的关注状态
  369. isFollow()
  370. })
  371. definePageMeta({
  372. layout: false
  373. })
  374. </script>
  375. <style lang="scss" scoped></style>