detail.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. <template>
  2. <view class="page-session" :style="{
  3. height,
  4. }" @touchmove="onTouchMove">
  5. <view class="header">
  6. <cl-topbar>
  7. <view class="cl-topbar__text" @click="toDetail()">
  8. <text class="cl-topbar__title">{{nickName}}</text>
  9. </view>
  10. <template slot="append">
  11. <template v-if="conversationType=='GROUP'">
  12. <image class="member-icon" src="../../static/icon/member.png" @tap="member"></image>
  13. </template>
  14. <template v-else>
  15. <view class="cl-topbar__icon" @tap="report">
  16. <!-- <cl-icon name="more"></cl-icon> -->
  17. 举报
  18. </view>
  19. </template>
  20. </template>
  21. </cl-topbar>
  22. </view>
  23. <progress :percent="percent" show-info stroke-width="3" v-if="progressShow" />
  24. <!-- 消息列表 -->
  25. <view class="message-list" @tap="onRetract">
  26. <scroll-view class="scroller" scroll-y scroll-with-animation :scroll-top="scroller.top"
  27. :upper-threshold="20" @scrolltoupper="refresh()">
  28. <!-- 加载更多 -->
  29. <view class="loadmore" v-if="loading">
  30. <cl-loadmore :finish="isCompleted" loading :divider="false"></cl-loadmore>
  31. </view>
  32. <!-- 内容块 -->
  33. <chat-message ref="message" :list="list" :conversationType="conversationType"></chat-message>
  34. </scroll-view>
  35. <!-- 录音弹窗 -->
  36. <cl-popup :visible.sync="voice.visible" direction="center" padding="0" border-radius="10rpx">
  37. <view class="popup-voice" :class="[
  38. {
  39. 'is-cancel': voiceIsCancel,
  40. },
  41. ]">
  42. <text class="popup-voice__time">{{ voice.duration | voice_duration }}</text>
  43. <text class="popup-voice__desc">松开发送,上滑取消</text>
  44. </view>
  45. </cl-popup>
  46. </view>
  47. <!-- 操作栏 -->
  48. <view class="opbar">
  49. <view class="main">
  50. <!-- 麦克风 -->
  51. <!-- #ifndef H5 -->
  52. <view class="icon">
  53. <text class="chat-iconfont icon-microphone" v-if="!op.isMicrophone" @tap="showMicrophone"></text>
  54. <text class="chat-iconfont icon-keyboard" @tap="hideMicrophone" v-else></text>
  55. </view>
  56. <!-- #endif -->
  57. <!-- 输入框 -->
  58. <view class="input">
  59. <button v-if="op.isMicrophone" class="press-btn" @longpress="onLongPress" @touchend="onRelease">
  60. {{ voice.visible ? "松开结束" : "按住说话" }}
  61. </button>
  62. <cl-input v-else v-model="value" confirm-type="send" confirm-hold fill :placeholder="placeholder"
  63. :cursor-spacing="10" :adjust-position="false" @focus="onFocus" @blur="onBlur"
  64. @confirm="onTextSend" @keyboardheightchange="onKeyBoardHeightChange"></cl-input>
  65. </view>
  66. <!-- 表情图标 -->
  67. <view class="icon">
  68. <text class="chat-iconfont icon-emoji" v-if="!op.isEmoji" @tap="showEmoji"></text>
  69. <text class="chat-iconfont icon-keyboard" v-else @tap="hideEmoji"></text>
  70. </view>
  71. <!-- 工具栏图标 -->
  72. <template v-if="value == ''">
  73. <view class="icon send">
  74. <text class="chat-iconfont icon-add-circle" v-if="!op.isTools" @tap="showTools"></text>
  75. <text class="chat-iconfont icon-subtract-circle" @tap="hideTools" v-else></text>
  76. </view>
  77. </template>
  78. <template v-else>
  79. <!-- 发送按钮 -->
  80. <view class="icon send">
  81. <text class="send-button" @tap="onTextSend">发送</text>
  82. </view>
  83. </template>
  84. </view>
  85. <view class="append">
  86. <!-- 工具栏 -->
  87. <chat-tools :visible="op.isTools" :userId="userID" :conversationType="conversationType"></chat-tools>
  88. <!-- 表情 -->
  89. <chat-emoji :visible="op.isEmoji" @select="onEmojiSelect"></chat-emoji>
  90. </view>
  91. </view>
  92. <!-- 视频弹窗 -->
  93. <cl-popup :visible.sync="video.visible" direction="center" force-update padding="0" border-radius="10rpx"
  94. @close="onVideoClose">
  95. <video id="video" autoplay :src="video.url" style="display: block"></video>
  96. </cl-popup>
  97. <member ref="member"></member>
  98. </view>
  99. </template>
  100. <script>
  101. import {
  102. debounce
  103. } from "../../uni_modules/cl-uni/utils";
  104. import ChatTools from "./components/tools";
  105. import ChatMessage from "./components/message";
  106. import ChatEmoji from "./components/emoji";
  107. import Member from "./components/member";
  108. // 录音设备
  109. const recorderManager = uni.getRecorderManager();
  110. // 平台
  111. const {
  112. platform
  113. } = uni.getSystemInfoSync();
  114. export default {
  115. components: {
  116. ChatTools,
  117. ChatMessage,
  118. ChatEmoji,
  119. Member,
  120. },
  121. data() {
  122. return {
  123. userInfos: [],
  124. placeholder: "",
  125. conversationID: "",
  126. conversationType: "",
  127. percent: 0,
  128. progressShow: false,
  129. firstLoad: true,
  130. isCompleted: false,
  131. nextReqMessageID: null,
  132. userID: 0,
  133. nickName: "",
  134. // 平台
  135. platform,
  136. // 输入框文本
  137. value: "",
  138. // 键盘高度
  139. keyBoardHeight: 0,
  140. // 聊天记录数据
  141. list: [{
  142. contentType: 0,
  143. type: 'TIMTextElem',
  144. from: 1,
  145. avatar: "https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/avatar/5.jpg",
  146. payload: {
  147. text: "Hello",
  148. },
  149. create_time: '2024-09-15 12:00:20'
  150. },
  151. {
  152. contentType: 2,
  153. type: 'TIMTextElem',
  154. from: 2,
  155. name: "神仙都没用",
  156. avatar: "https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/avatar/5.jpg",
  157. payload: {
  158. text: "Hello",
  159. },
  160. content: {
  161. text: "Hello",
  162. imageUrl: "https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/face-with-party-horn-and-party-hat.png",
  163. },
  164. create_time: '2024-09-15 12:00:20'
  165. },
  166. ],
  167. // 底部操作栏配置
  168. // list: [],
  169. op: {
  170. isMicrophone: false,
  171. isEmoji: false,
  172. isTools: false,
  173. },
  174. // 滚动条配置
  175. scroller: {
  176. top: 0,
  177. intoView: "",
  178. },
  179. // 视频配置
  180. video: {
  181. visible: false,
  182. },
  183. // 音频配置
  184. voice: {
  185. visible: false,
  186. duration: 0,
  187. timer: null,
  188. down: 0,
  189. move: 0,
  190. },
  191. // 加载进度
  192. loading: false,
  193. };
  194. },
  195. onLoad(option) {
  196. this.userID = "p1";
  197. this.nickName = "老杨";
  198. this.conversationID = "single";
  199. this.conversationType = "C2C"; //GROUP
  200. // this.userID = JSON.parse(decodeURIComponent(option.userID));
  201. // this.nickName = JSON.parse(decodeURIComponent(option.nickName));
  202. // this.conversationID = JSON.parse(decodeURIComponent(option.conversationID));
  203. // this.conversationType = JSON.parse(decodeURIComponent(option.conversationType));
  204. // if (this.conversationType == "GROUP") {
  205. // this.placeholder = "发布违规言论会被禁言哦~";
  206. // }
  207. // this.TIM.setMessageRead(this.conversationID);
  208. // this.setList();
  209. this.getUserInfo();
  210. uni.$on('messageUpdate', this.acceptMessage)
  211. uni.$on('messageProgress', this.messageProgress)
  212. },
  213. computed: {
  214. // 计算屏幕高度
  215. height() {
  216. return this.keyBoardHeight > 0 ?
  217. `calc(100% - ${this.keyBoardHeight}px + env(safe-area-inset-bottom))` :
  218. "100%";
  219. },
  220. // 录音滑动是否取消
  221. voiceIsCancel() {
  222. return this.voice.move ? this.voice.down - this.voice.move > 50 : false;
  223. },
  224. },
  225. filters: {
  226. voice_duration(t) {
  227. return `00:${t < 10 ? `0${t}` : t}`;
  228. },
  229. },
  230. methods: {
  231. async getUserInfo() {
  232. // let [err, res] = await this.$http.get('Group/members', {
  233. // 'id': this.userID,
  234. // 'type': this.conversationType
  235. // });
  236. // if (!this.$http.errorCheck(err, res)) {
  237. // return;
  238. // }
  239. // this.userInfos = res.data.data;
  240. this.userInfos = [{
  241. id: 1,
  242. user_icon: "https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/avatar/5.jpg",
  243. username: 'ab',
  244. sex: 1
  245. },
  246. {
  247. id: 2,
  248. user_icon: "https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/avatar/5.jpg",
  249. username: 'cd',
  250. sex: 1
  251. }
  252. ];
  253. this.setUserInfo();
  254. },
  255. setUserInfo() {
  256. if (this.list.length == 0 || this.userInfos.length == 0) {
  257. return
  258. }
  259. for (let i = 0; i < this.list.length; i++) {
  260. let fromID = this.list[i].from
  261. for (let j = 0; j < this.userInfos.length; j++) {
  262. if (fromID == this.userInfos[j].id) {
  263. this.list[i].avatar = this.userInfos[j].user_icon;
  264. this.list[i].sex = this.userInfos[j].sex;
  265. break;
  266. }
  267. }
  268. }
  269. this.list = this.list
  270. },
  271. toDetail() {
  272. if (this.conversationType == "C2C") {
  273. uni.navigateTo({
  274. url: "/pages/detail/index?uid=" + this.userID,
  275. });
  276. }
  277. },
  278. report() {
  279. setTimeout(() => {
  280. uni.showToast({
  281. title: '举报成功,我们会尽快进行处理!',
  282. icon: "none"
  283. });
  284. }, 500);
  285. },
  286. member() {
  287. this.$refs.member.open(this.userID);
  288. },
  289. refresh() {
  290. this.loading = true;
  291. if (this.isCompleted) {
  292. return;
  293. }
  294. // this.setList();
  295. },
  296. messageProgress(e) {
  297. this.percent = e * 100;
  298. if (e == 1) {
  299. this.progressShow = false;
  300. } else {
  301. this.progressShow = true;
  302. }
  303. },
  304. timestampToTime(timestamp) {
  305. var date = new Date(timestamp * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
  306. var Y = date.getFullYear() + '-';
  307. var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';
  308. //var D = date.getDate() + ' ';
  309. var D = (date.getDate() + 1 < 10 ? '0' + (date.getDate()) : date.getDate()) + ' ';
  310. var h = date.getHours() + ':';
  311. var m = (date.getMinutes() + 1 < 10 ? '0' + (date.getMinutes()) : date.getMinutes()) + ' ';
  312. var s = date.getSeconds();
  313. return M + D + h + m;
  314. },
  315. acceptMessage(message) {
  316. for (let i = 0; i < message.length; i++) {
  317. if (message[i].conversationID == this.conversationID) {
  318. this.list.push(message[i]);
  319. this.scrollToBottom();
  320. }
  321. }
  322. },
  323. async setList() {
  324. return;
  325. let res = await this.TIM.getMessageList(this.conversationID, this.nextReqMessageID);
  326. let messageList = res.data.messageList;
  327. this.isCompleted = res.data.isCompleted;
  328. this.nextReqMessageID = res.data.nextReqMessageID;
  329. for (let i = 0; i < messageList.length; i++) {
  330. messageList[i].create_time = this.timestampToTime(messageList[i].time);
  331. messageList[i].sex = "3";
  332. }
  333. if (this.firstLoad) {
  334. this.list = messageList;
  335. this.scrollToBottom();
  336. } else {
  337. this.list = messageList.concat(this.list);
  338. }
  339. this.firstLoad = false;
  340. uni.stopPullDownRefresh();
  341. this.loading = false;
  342. this.setUserInfo();
  343. },
  344. // 监听键盘高度
  345. onKeyBoardHeightChange(e) {
  346. this.keyBoardHeight = e.detail.height;
  347. },
  348. // 滑动监听
  349. onTouchMove(e) {
  350. if (this.voice.visible) {
  351. // 记录移动的位置
  352. this.voice.move = e.changedTouches[0].clientY;
  353. }
  354. },
  355. // 长按说话
  356. onLongPress(e) {
  357. // uni.authorize({
  358. // scope: 'scope.record',
  359. // success() {
  360. // uni.getLocation()
  361. // }
  362. // });
  363. // 关闭已存在播放声音
  364. this.$refs["message"].voicePause();
  365. console.log("按下按钮了");
  366. this.$nextTick(() => {
  367. // 记录按下位置
  368. this.voice.down = e.touches[0].pageY;
  369. // 清空移动位置
  370. this.voice.move = 0;
  371. // 显示弹窗
  372. this.voice.visible = true;
  373. // 开始录音
  374. recorderManager.start({
  375. // duration: 60000,
  376. // sampleRate: 44100,
  377. // numberOfChannels: 1,
  378. // encodeBitRate: 192000,
  379. // format: "aac",
  380. });
  381. // 计数器
  382. this.voice.timer = setInterval(() => {
  383. if (this.voice.duration >= 60) {
  384. this.onRelease(e);
  385. } else {
  386. this.voice.duration += 1;
  387. }
  388. }, 1000);
  389. });
  390. },
  391. // 松开
  392. onRelease(e) {
  393. // 记录移动位置
  394. this.voice.move = e.changedTouches[0].clientY;
  395. let duration = this.voice.duration * 1000;
  396. console.log("松开按钮了");
  397. // 暂停事件
  398. recorderManager.onStop((res) => {
  399. // 判断是否取消
  400. if (!this.voiceIsCancel) {
  401. res.duration = duration;
  402. res.size = 34271;
  403. this.TIM.sendAudioMessage(res, this.userID, this.conversationType);
  404. }
  405. });
  406. // 清除计时器
  407. clearInterval(this.voice.timer);
  408. // 暂停录音
  409. recorderManager.stop();
  410. // 关闭弹窗
  411. this.voice.visible = false;
  412. // 清空时常
  413. this.voice.duration = 0;
  414. },
  415. // 显示麦克风
  416. showMicrophone() {
  417. this.op.isMicrophone = true;
  418. this.hideTools();
  419. this.hideEmoji();
  420. this.hideKeyBoard();
  421. },
  422. // 隐藏麦克风
  423. hideMicrophone() {
  424. this.op.isMicrophone = false;
  425. },
  426. // 显示表情
  427. showEmoji() {
  428. this.op.isEmoji = true;
  429. this.hideTools();
  430. this.hideMicrophone();
  431. this.scrollToBottom();
  432. },
  433. // 隐藏表情
  434. hideEmoji() {
  435. this.op.isEmoji = false;
  436. },
  437. // 显示工具栏
  438. showTools() {
  439. this.op.isTools = true;
  440. this.hideEmoji();
  441. this.hideMicrophone();
  442. this.scrollToBottom();
  443. },
  444. // 隐藏工具栏
  445. hideTools() {
  446. this.op.isTools = false;
  447. },
  448. // 隐藏键盘
  449. hideKeyBoard() {
  450. this.keyBoardHeight = 0;
  451. },
  452. // 滑动到底部
  453. scrollToBottom: debounce(function() {
  454. this.$nextTick(() => {
  455. this.scroller.top = 2000000 + parseInt(Math.random() * 100);
  456. });
  457. }, 100),
  458. // 收起
  459. onRetract() {
  460. this.hideTools();
  461. this.hideEmoji();
  462. },
  463. // 聚焦
  464. onFocus() {
  465. this.hideEmoji();
  466. this.hideTools();
  467. this.scrollToBottom();
  468. },
  469. // 失焦
  470. onBlur() {
  471. this.hideKeyBoard();
  472. },
  473. // 发送消息
  474. onTextSend() {
  475. if (this.value) {
  476. this.TIM.sendTextMessage(this.value, this.userID, this.conversationType);
  477. this.value = "";
  478. }
  479. if (this.userID == "51") {
  480. setTimeout(() => {
  481. this.hackMessage()
  482. }, 1000);
  483. }
  484. },
  485. hackMessage() {
  486. if (this.list.length > 0) {
  487. let item = JSON.parse(JSON.stringify(this.list[0]))
  488. item.avatar = "/static/logo.png";
  489. item._mode = "text";
  490. item.payload.text = "客服小姐姐正在赶来,请稍后~"
  491. item.from = "51"
  492. this.list.push(item)
  493. } else {
  494. this.list.push({
  495. avatar: "/static/logo.png",
  496. _mode: "text",
  497. payload: {
  498. text: "客服小姐姐正在赶来,请稍后~"
  499. },
  500. from: "51"
  501. })
  502. }
  503. },
  504. // 表情选择
  505. onEmojiSelect(e) {
  506. this.value = this.value + e;
  507. },
  508. // 追加数据到开头
  509. prepend(...data) {
  510. this.list.unshift(...data.filter(Boolean).reverse());
  511. },
  512. // 追加数据到结尾
  513. append(...data) {
  514. this.list.push(
  515. ...data
  516. .map((e) => {
  517. e.animation = true;
  518. return e;
  519. })
  520. .filter(Boolean)
  521. );
  522. this.scrollToBottom();
  523. },
  524. // 关闭视频弹窗
  525. onVideoClose() {
  526. const video = uni.createVideoContext("video");
  527. video.pause();
  528. }
  529. },
  530. };
  531. </script>
  532. <style lang="scss">
  533. @import "../../static/css/iconfont.scss";
  534. page {
  535. height: 100%;
  536. overflow: hidden;
  537. background-color: #fff;
  538. }
  539. </style>
  540. <style lang="scss" scoped>
  541. .page-session {
  542. display: flex;
  543. flex-direction: column;
  544. padding-bottom: env(safe-area-inset-bottom);
  545. box-sizing: border-box;
  546. height: 100%;
  547. .message-list {
  548. flex: 1;
  549. overflow: hidden;
  550. background-color: #f7f7f7;
  551. position: relative;
  552. .loadmore {
  553. margin: 10rpx 0;
  554. }
  555. .scroller {
  556. height: 100%;
  557. }
  558. /deep/.cl-popup {
  559. &__wrapper {
  560. position: absolute;
  561. }
  562. }
  563. }
  564. .opbar {
  565. flex-shrink: 0;
  566. z-index: 9;
  567. background-color: #fff;
  568. border-top: 1rpx solid #f7f7f7;
  569. .main {
  570. display: flex;
  571. align-items: center;
  572. height: 100rpx;
  573. .send {
  574. margin-right: 30rpx;
  575. }
  576. .icon {
  577. height: 80rpx;
  578. width: 80rpx;
  579. line-height: 80rpx;
  580. text-align: center;
  581. flex-shrink: 0;
  582. .chat-iconfont {
  583. font-size: 60rpx;
  584. }
  585. .send-button {
  586. background-color: #66C67D;
  587. color: #F7FFFB;
  588. border-radius: 10rpx;
  589. padding: 5rpx 10rpx;
  590. font-size: 30rpx;
  591. }
  592. }
  593. .input {
  594. flex: 1;
  595. margin: 0 10rpx;
  596. height: 70rpx;
  597. line-height: 70rpx;
  598. .press-btn {
  599. display: inline-block;
  600. height: 70rpx;
  601. width: 100%;
  602. line-height: 70rpx;
  603. color: #666;
  604. border: 1rpx solid #dcdfe6;
  605. font-size: 24rpx;
  606. background-color: #fff;
  607. margin: 0;
  608. border-radius: 70rpx;
  609. box-sizing: border-box;
  610. &::after {
  611. border: 0;
  612. }
  613. &:active {
  614. background-color: #f7f7f7;
  615. }
  616. }
  617. }
  618. }
  619. }
  620. .popup-voice {
  621. display: flex;
  622. flex-direction: column;
  623. align-items: center;
  624. padding: 20rpx;
  625. &.is-cancel {
  626. background-color: red;
  627. color: #fff;
  628. }
  629. &__time {
  630. font-size: 28rpx;
  631. margin-bottom: 20rpx;
  632. letter-spacing: 1rpx;
  633. }
  634. &__desc {
  635. font-size: 24rpx;
  636. }
  637. }
  638. }
  639. .cl-topbar__icon {
  640. font-size: 25rpx;
  641. color: red;
  642. padding: 0rpx 10rpx;
  643. }
  644. .member-icon {
  645. width: 50rpx;
  646. height: 50rpx;
  647. margin-right: 30rpx;
  648. }
  649. .cl-topbar {
  650. width: 100%;
  651. height: 100rpx;
  652. display: flex;
  653. flex-direction: row;
  654. justify-content: space-between;
  655. align-items: center;
  656. padding: 10rpx 20rpx;
  657. }
  658. </style>