chat.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. <script>
  2. import { mapState } from 'vuex'
  3. import moment from 'moment'
  4. import MescrollMixin from '@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js'
  5. import api from '@/api'
  6. export default {
  7. mixins: [MescrollMixin], // 使用mixin
  8. data() {
  9. return {
  10. downOption: {
  11. autoShowLoading: true, // 显示下拉刷新的进度条
  12. textColor: '#adadad' // 下拉刷新的文本颜色
  13. },
  14. upOption: {
  15. use: false, // 禁止上拉
  16. toTop: {
  17. src: '' // 不显示回到顶部按钮
  18. }
  19. },
  20. pageNum: 1, // 页码
  21. pageSize: 10, // 页长
  22. isEnd: false, // 是否无消息
  23. msgList: [], // 消息列表
  24. send_msg: '', // 发送的消息
  25. send_img: '', // 发送的图片
  26. timer: '', // 定时器 timer
  27. isScroll: false, // 是否滚动到底部
  28. deny_ids: '' // 测回的消息id
  29. }
  30. },
  31. computed: {
  32. ...mapState({
  33. user: 'user',
  34. token: 'token'
  35. }),
  36. // 最新的消息id
  37. last_id() {
  38. return this.msgList.length && this.msgList[this.msgList.length - 1].id
  39. },
  40. // 最'旧'的消息id
  41. first_id() {
  42. return this.msgList.length && this.msgList[0].id
  43. },
  44. denyList() {
  45. return this.deny_ids.split('|')
  46. },
  47. msgListFilter() {
  48. let list = JSON.parse(JSON.stringify(this.msgList))
  49. for (let i = 0; i < this.denyList.length; i++) {
  50. for (let j = 0; j < list.length; j++) {
  51. if (this.denyList[i] === list[j].id) {
  52. list.splice(j, 1)
  53. break
  54. }
  55. }
  56. }
  57. return list
  58. },
  59. actionList() {
  60. return this.user.user_type === 99 ? ['复制', '+1', '撤回', '封禁'] : ['复制', '+1']
  61. }
  62. },
  63. methods: {
  64. /*上拉刷新的回调 */
  65. async downCallback() {
  66. try {
  67. let res = await api.chatGetOldMessage(this.first_id)
  68. res.reverse()
  69. // 需自行维护页码
  70. this.pageNum++
  71. // 先隐藏下拉刷新的状态
  72. this.mescroll.endSuccess()
  73. // 不满一页,说明已经无更多消息 (建议根据您实际接口返回的总页码数,总消息量,是否有消息的字段来判断)
  74. if (res.length < this.pageSize) {
  75. this.isEnd = true // 标记已无更多消息
  76. this.mescroll.lockDownScroll(true) // 锁定下拉
  77. }
  78. // 生成VIEW_ID,大写,避免污染源数据
  79. res.forEach(val => {
  80. val.VIEW_ID = 'msg' + val.id // 不以数字开头
  81. })
  82. // 获取当前最顶部的VIEW_ID (注意是写在res.concat前面)
  83. let topMsg = this.msgList[0]
  84. //设置列表数据
  85. this.msgList = res.concat(this.msgList) // 注意不是this.msgList.concat
  86. // 开启定时器
  87. if (!this.timer) this.msgLoop()
  88. this.$nextTick(() => {
  89. if (this.pageNum <= 2) {
  90. // 第一页直接滚动到底部 ( this.pageNum已在前面加1 )
  91. this.toBottom()
  92. } else if (topMsg) {
  93. // 保持顶部消息的位置
  94. let view = uni.createSelectorQuery().select('#' + topMsg.VIEW_ID)
  95. view
  96. .boundingClientRect(v => {
  97. // console.log('节点离页面顶部的距离=' + v.top)
  98. this.mescroll.myScrollTo(v.top - 92, 0) // 减去上偏移量100
  99. })
  100. .exec()
  101. }
  102. })
  103. } catch (e) {
  104. this.pageNum-- // 联网失败,必须回减页码
  105. this.mescroll.endErr() // 隐藏下拉刷新的状态
  106. }
  107. },
  108. async sendMsg(type = '1') {
  109. if ((type === '1' && !this.send_msg) || (type === '2' && !this.send_img)) return
  110. let content = type === '1' ? this.send_msg : this.send_img
  111. // 置空防止二次点击
  112. this.send_msg = ''
  113. this.send_img = ''
  114. this.isScroll = true // 新消息请求后滚动到底部
  115. await api.chatSendMessage({ type, content })
  116. // 停止定时器
  117. clearInterval(this.timer)
  118. this.timer = ''
  119. // 手动刷新
  120. await this.getNewMsg()
  121. },
  122. async getNewMsg() {
  123. if (!this.token) return
  124. try {
  125. let { deny_ids, msg_list } = await api.chatGetNewMessage(this.last_id)
  126. msg_list.reverse()
  127. this.msgList = [...this.msgList, ...msg_list]
  128. this.deny_ids = deny_ids
  129. } catch (e) {}
  130. if (this.isScroll) {
  131. this.toBottom()
  132. this.isScroll = false
  133. }
  134. // 如果定时器被清除则重新开启轮询
  135. if (!this.timer) {
  136. this.msgLoop()
  137. }
  138. },
  139. // 消息轮询
  140. msgLoop() {
  141. this.timer = setInterval(() => {
  142. this.getNewMsg()
  143. }, 3000)
  144. },
  145. chooseImg() {
  146. uni.chooseImage({
  147. count: 1,
  148. sizeType: ['compressed'], // 只传压缩图
  149. success: async ({ tempFilePaths }) => {
  150. let res = await api.uploadImg({
  151. filePath: tempFilePaths[0],
  152. formData: {
  153. type: 'message'
  154. }
  155. })
  156. let result = JSON.parse(res).data
  157. this.send_img = result.base_url
  158. await this.sendMsg('2')
  159. }
  160. })
  161. },
  162. previewImg(src) {
  163. uni.previewImage({
  164. urls: [src]
  165. })
  166. },
  167. isShowTime(pre, cur) {
  168. if (pre < 0) return
  169. let preTimestamp = moment(this.msgList[pre].create_time).valueOf()
  170. let curTimestamp = moment(this.msgList[cur].create_time).valueOf()
  171. return curTimestamp - preTimestamp > 300000 // 如果大于五分钟就显示时间
  172. },
  173. actionSheet(msg) {
  174. uni.showActionSheet({
  175. itemList: this.actionList,
  176. success: ({ tapIndex }) => {
  177. switch (tapIndex) {
  178. case 0:
  179. this.$common.copy(msg.content)
  180. break
  181. case 1:
  182. this.send_msg = msg.content
  183. this.sendMsg()
  184. break
  185. case 2:
  186. this.backMessage(msg)
  187. break
  188. case 3:
  189. this.deny(msg)
  190. break
  191. default:
  192. break
  193. }
  194. }
  195. })
  196. },
  197. async backMessage(msg) {
  198. await api.chatBackMessage(msg.id)
  199. this.$common.toast('已撤回')
  200. },
  201. async deny(msg) {
  202. await api.chatDenyUser(msg.uid)
  203. this.$common.toast('封禁成功')
  204. },
  205. toBottom() {
  206. setTimeout(() => {
  207. this.mescroll.myScrollTo(99999, 0)
  208. uni.pageScrollTo({ scrollTop: 99999, duration: 50 })
  209. })
  210. }
  211. },
  212. beforeDestroy() {
  213. clearInterval(this.timer)
  214. }
  215. }
  216. </script>
  217. <template>
  218. <view class="bg">
  219. <!-- 需配置bottom的偏移量, 用于底部留白 -->
  220. <mescroll-uni
  221. ref="mescrollRef"
  222. @init="mescrollInit"
  223. :down="downOption"
  224. @down="downCallback"
  225. :up="upOption"
  226. :bottom="160"
  227. >
  228. <!-- 无更多消息 -->
  229. <view v-if="isEnd" class="msg-end">没有更多消息了</view>
  230. <!-- 消息列表 (必须配置id,以便定位) -->
  231. <view v-for="(msg, i) in msgListFilter" :key="msg.id" :id="msg.VIEW_ID">
  232. <view class="time" v-if="isShowTime(i - 1, i)">
  233. {{ msg.create_time }}
  234. </view>
  235. <view class="msg flex" :class="{ self: msg.uid === user.id }">
  236. <view class="avatar">
  237. <u-avatar :src="msg.avatar" size="80rpx"></u-avatar>
  238. </view>
  239. <view
  240. class="flex1"
  241. :class="{
  242. flex: msg.uid === user.id,
  243. 'flex-end': msg.uid === user.id
  244. }"
  245. @click="
  246. () => {
  247. if (user.user_type === 99) actionSheet(msg)
  248. }
  249. "
  250. >
  251. <view class="nickname" v-if="msg.uid !== user.id">{{ msg.nick_name }}</view>
  252. <view class="msg-warp flex">
  253. <view v-if="msg.type === '1'" class="msg-content">
  254. <text>{{ msg.content }}</text>
  255. </view>
  256. <u-image
  257. v-else
  258. mode="widthFix"
  259. width="250rpx"
  260. :src="msg.content"
  261. @click.native.stop="previewImg(msg.content)"
  262. ></u-image>
  263. </view>
  264. </view>
  265. </view>
  266. </view>
  267. </mescroll-uni>
  268. <!-- 底部 fixed定位 -->
  269. <view class="bottom flex flex-center">
  270. <view class="flex1 input-box flex align-center">
  271. <input
  272. type="text"
  273. v-model="send_msg"
  274. class="send-input"
  275. :hold-keyboard="false"
  276. auto-blur
  277. @confirm="
  278. ({ detail }) => {
  279. send_msg = detail.value
  280. sendMsg()
  281. }
  282. "
  283. confirm-type="send"
  284. :class="{ 'text-center': user.user_type !== 99 }"
  285. :disabled="user.user_type !== 99"
  286. :placeholder="user.user_type !== 99 ? '全体禁言中' : ''"
  287. />
  288. <u-button
  289. text="发送"
  290. shape="circle"
  291. color="var(--theme)"
  292. class="send_btn"
  293. @click="sendMsg()"
  294. :disabled="user.user_type !== 99"
  295. ></u-button>
  296. </view>
  297. <view v-if="user.user_type === 99" class="send-img-box flex flex-center" @click="chooseImg">
  298. <image src="/static/images/chat/list-msg-img.png" class="send-img"></image>
  299. </view>
  300. </view>
  301. </view>
  302. </template>
  303. <style lang="scss">
  304. .bg {
  305. background-color: #f7f7f7;
  306. }
  307. /* 无更多消息 */
  308. .msg-end {
  309. padding: 40rpx 0;
  310. font-size: 24rpx;
  311. text-align: center;
  312. color: #adadad;
  313. }
  314. .time {
  315. margin: 16rpx 0 0;
  316. font-size: 22rpx;
  317. font-weight: 500;
  318. color: #adadad;
  319. text-align: center;
  320. }
  321. /*消息列表*/
  322. .msg {
  323. margin: 30rpx 0;
  324. &.self {
  325. justify-content: end;
  326. flex-direction: row-reverse;
  327. }
  328. .avatar {
  329. position: relative;
  330. margin: 0 20rpx 0 25rpx;
  331. }
  332. .nickname {
  333. font-size: 20rpx;
  334. font-weight: 500;
  335. color: #adadad;
  336. margin-bottom: 16rpx;
  337. }
  338. .msg-warp {
  339. .msg-content {
  340. max-width: 450rpx;
  341. min-width: 20rpx;
  342. min-height: 45rpx;
  343. padding: 16rpx 24rpx;
  344. background: #ffffff;
  345. border-radius: 10px;
  346. word-break: break-all;
  347. font-size: 30rpx;
  348. font-weight: 500;
  349. color: #232323;
  350. line-height: 44rpx;
  351. }
  352. .msg-img {
  353. max-width: 250rpx;
  354. }
  355. }
  356. &.self {
  357. .avatar {
  358. margin: 0 25rpx 0 20rpx;
  359. }
  360. .msg-content {
  361. background-color: #d4ddfc;
  362. }
  363. }
  364. }
  365. .bottom {
  366. position: fixed;
  367. bottom: 0;
  368. width: 100%;
  369. padding: 0 30rpx 0 30rpx;
  370. box-sizing: border-box;
  371. height: 149rpx;
  372. background: #ffffff;
  373. .input-box {
  374. background: #f6f6f6;
  375. border-radius: 41rpx;
  376. .send-input {
  377. flex: 1;
  378. height: 82rpx;
  379. padding: 0 44rpx;
  380. font-size: 28rpx;
  381. font-weight: 500;
  382. color: #959595;
  383. }
  384. .send_btn {
  385. width: 130rpx;
  386. height: 80rpx;
  387. }
  388. }
  389. .send-img-box {
  390. width: 80rpx;
  391. height: 80rpx;
  392. .send-img {
  393. width: 42rpx;
  394. height: 40rpx;
  395. }
  396. }
  397. }
  398. </style>