index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. <template>
  2. <view>
  3. <view class="TUI-message-input-container">
  4. <view class="TUI-commom-function">
  5. <view v-for="(item, index) in commonFunction" :key="index" class="TUI-commom-function-item" :data-function="item" @tap="handleCommonFunctions">
  6. {{ item.name }}
  7. </view>
  8. </view>
  9. <view class="TUI-message-input">
  10. <image class="TUI-icon" @tap="switchAudio" :src="isAudio ? '/static/static/assets/keyboard.svg' : '/static/static/assets/audio.svg'"></image>
  11. <view v-if="!isAudio" class="TUI-message-input-main">
  12. <input
  13. class="TUI-message-input-area"
  14. :adjust-position="true"
  15. cursor-spacing="20"
  16. v-model="inputText"
  17. @input="onInputValueChange"
  18. maxlength="140"
  19. type="text"
  20. placeholder-class="input-placeholder"
  21. placeholder="说点什么呢~"
  22. @focus="inputBindFocus"
  23. @blur="inputBindBlur"
  24. />
  25. </view>
  26. <view
  27. v-else
  28. class="TUI-message-input-main"
  29. @longpress="handleLongPress"
  30. @touchmove="handleTouchMove"
  31. @touchend="handleTouchEnd"
  32. style="display: flex; justify-content: center; font-size: 32rpx; font-family: PingFangSC-Regular;"
  33. >
  34. <text>{{ text }}</text>
  35. </view>
  36. <view class="TUI-message-input-functions" hover-class="none">
  37. <image class="TUI-icon" @tap="handleEmoji" src="/static/static/assets/face-emoji.svg"></image>
  38. <view v-if="!sendMessageBtn" @tap="handleExtensions"><image class="TUI-icon" src="/static/static/assets/more.svg"></image></view>
  39. <view v-else class="TUI-sendMessage-btn" @tap="sendTextMessage">发送</view>
  40. </view>
  41. </view>
  42. <view v-if="displayFlag === 'emoji'" class="TUI-Emoji-area"><TUI-Emoji @enterEmoji="appendMessage"></TUI-Emoji></view>
  43. <view v-if="displayFlag === 'extension'" class="TUI-Extensions">
  44. <!-- TODO: 这里功能还没实现 -->
  45. <!-- <camera device-position="back" flash="off" binderror="error" style="width: 100%; height: 300px;"></camera>-->
  46. <view class="TUI-Extension-slot" @tap="handleSendPicture">
  47. <image class="TUI-Extension-icon" src="/static/static/assets/take-photo.svg"></image>
  48. <view class="TUI-Extension-slot-name">拍摄照片</view>
  49. </view>
  50. <view class="TUI-Extension-slot" @tap="handleSendImage">
  51. <image class="TUI-Extension-icon" src="/static/static/assets/send-img.svg"></image>
  52. <view class="TUI-Extension-slot-name">发送图片</view>
  53. </view>
  54. <view class="TUI-Extension-slot" @tap="handleShootVideo">
  55. <image class="TUI-Extension-icon" src="/static/static/assets/take-video.svg"></image>
  56. <view class="TUI-Extension-slot-name">拍摄视频</view>
  57. </view>
  58. <view class="TUI-Extension-slot" @tap="handleSendVideo">
  59. <image class="TUI-Extension-icon" src="/static/static/assets/send-video.svg"></image>
  60. <view class="TUI-Extension-slot-name">发送视频</view>
  61. </view>
  62. <view class="TUI-Extension-slot" @tap="handleCalling(1)">
  63. <image class="TUI-Extension-icon" src="/static/static/assets/audio-calling.svg"></image>
  64. <view class="TUI-Extension-slot-name">语音通话</view>
  65. </view>
  66. <view class="TUI-Extension-slot" @tap="handleCalling(2)">
  67. <image class="TUI-Extension-icon" src="/static/static/assets/video-calling.svg"></image>
  68. <view class="TUI-Extension-slot-name">视频通话</view>
  69. </view>
  70. <!-- <view class="TUI-Extension-slot" @tap="handleServiceEvaluation">
  71. <image class="TUI-Extension-icon" src="/static/static/assets/service-assess.svg"></image>
  72. <view class="TUI-Extension-slot-name">服务评价</view>
  73. </view>
  74. <view class="TUI-Extension-slot" @tap="handleSendOrder">
  75. <image class="TUI-Extension-icon" src="/static/static/assets/send-order.svg"></image>
  76. <view class="TUI-Extension-slot-name">发送订单</view>
  77. </view> -->
  78. </view>
  79. <TUI-Common-Words class="tui-cards" :display="displayCommonWords" @sendMessage="$handleSendTextMessage" @close="$handleCloseCards"></TUI-Common-Words>
  80. <TUI-Order-List class="tui-cards" :display="displayOrderList" @sendCustomMessage="$handleSendCustomMessage" @close="$handleCloseCards"></TUI-Order-List>
  81. <TUI-Service-Evaluation
  82. class="tui-cards"
  83. :display="displayServiceEvaluation"
  84. @sendCustomMessage="$handleSendCustomMessage"
  85. @close="$handleCloseCards"
  86. ></TUI-Service-Evaluation>
  87. </view>
  88. <view class="record-modal" v-if="popupToggle" @longpress="handleLongPress" @touchmove="handleTouchMove" @touchend="handleTouchEnd">
  89. <view class="wrapper"><view class="modal-loading"></view></view>
  90. <view class="modal-title">{{ title }}</view>
  91. </view>
  92. </view>
  93. </template>
  94. <script>
  95. import TUIEmoji from '../message-elements/emoji/index';
  96. import TUICommonWords from '../message-private/common-words/index';
  97. import TUIOrderList from '../message-private/order-list/index';
  98. import TUIServiceEvaluation from '../message-private/service-evaluation/index';
  99. export default {
  100. data() {
  101. return {
  102. // todo conversation
  103. // conversation: {},
  104. firstSendMessage: true,
  105. inputText: '',
  106. extensionArea: false,
  107. sendMessageBtn: false,
  108. displayFlag: '',
  109. isAudio: false,
  110. bottomVal: 0,
  111. startPoint: 0,
  112. popupToggle: false,
  113. isRecording: false,
  114. canSend: true,
  115. text: '按住说话',
  116. title: ' ',
  117. notShow: false,
  118. isShow: true,
  119. recordTime: 0,
  120. recordTimer: null,
  121. commonFunction: [
  122. {
  123. name: '常用语',
  124. key: '0'
  125. }
  126. // {
  127. // name: '发送订单',
  128. // key: '1'
  129. // },
  130. // {
  131. // name: '服务评价',
  132. // key: '2'
  133. // }
  134. ],
  135. displayServiceEvaluation: false,
  136. displayCommonWords: false,
  137. displayOrderList: false
  138. };
  139. },
  140. components: {
  141. TUIEmoji,
  142. TUICommonWords,
  143. TUIOrderList,
  144. TUIServiceEvaluation
  145. },
  146. props: {
  147. conversation: {
  148. type: Object,
  149. default: () => {}
  150. }
  151. },
  152. watch: {
  153. conversation: {
  154. handler: function(newVal) {
  155. // todo 值会被改变
  156. // this.setData({
  157. // conversation: newVal
  158. // });
  159. },
  160. immediate: true,
  161. deep: true
  162. }
  163. },
  164. beforeMount() {
  165. // 加载声音录制管理器
  166. this.recorderManager = uni.getRecorderManager();
  167. this.recorderManager.onStop(res => {
  168. clearInterval(this.recordTimer);
  169. // 兼容 uniapp 打包app,duration 和 fileSize 需要用户自己补充
  170. // 文件大小 = (音频码率) x 时间长度(单位:秒) / 8
  171. let msg = {
  172. duration: res.duration ? res.duration : this.recordTime * 1000,
  173. tempFilePath: res.tempFilePath,
  174. fileSize: res.fileSize ? res.fileSize : ((48 * this.recordTime) / 8) * 1024
  175. };
  176. uni.hideLoading();
  177. // 兼容 uniapp 语音消息没有duration
  178. if (this.canSend) {
  179. if (msg.duration < 1000) {
  180. uni.showToast({
  181. title: '录音时间太短',
  182. icon: 'none'
  183. });
  184. } else {
  185. // res.tempFilePath 存储录音文件的临时路径
  186. const message = uni.$TUIKit.createAudioMessage({
  187. to: this.getToAccount(),
  188. conversationType: this.conversation.type,
  189. payload: {
  190. file: msg
  191. }
  192. });
  193. this.$sendTIMMessage(message);
  194. }
  195. }
  196. this.setData({
  197. startPoint: 0,
  198. popupToggle: false,
  199. isRecording: false,
  200. canSend: true,
  201. title: ' ',
  202. text: '按住说话'
  203. });
  204. });
  205. },
  206. methods: {
  207. switchAudio() {
  208. this.setData({
  209. isAudio: !this.isAudio,
  210. text: '按住说话'
  211. });
  212. },
  213. handleLongPress(e) {
  214. this.recorderManager.start({
  215. duration: 60000,
  216. // 录音的时长,单位 ms,最大值 600000(10 分钟)
  217. sampleRate: 44100,
  218. // 采样率
  219. numberOfChannels: 1,
  220. // 录音通道数
  221. encodeBitRate: 192000,
  222. // 编码码率
  223. format: 'aac' // 音频格式,选择此格式创建的音频消息,可以在即时通信 IM 全平台(Android、iOS、微信小程序和Web)互通
  224. });
  225. this.setData({
  226. startPoint: e.touches[0],
  227. title: '正在录音',
  228. // isRecording : true,
  229. // canSend: true,
  230. notShow: true,
  231. isShow: false,
  232. isRecording: true,
  233. popupToggle: true,
  234. recordTime: 0
  235. });
  236. this.recordTimer = setInterval(() => {
  237. this.recordTime++;
  238. }, 1000);
  239. },
  240. // 录音时的手势上划移动距离对应文案变化
  241. handleTouchMove(e) {
  242. if (this.isRecording) {
  243. if (this.startPoint.clientY - e.touches[e.touches.length - 1].clientY > 100) {
  244. this.setData({
  245. text: '抬起停止',
  246. title: '松开手指,取消发送',
  247. canSend: false
  248. });
  249. } else if (this.startPoint.clientY - e.touches[e.touches.length - 1].clientY > 20) {
  250. this.setData({
  251. text: '抬起停止',
  252. title: '上划可取消',
  253. canSend: true
  254. });
  255. } else {
  256. this.setData({
  257. text: '抬起停止',
  258. title: '正在录音',
  259. canSend: true
  260. });
  261. }
  262. }
  263. },
  264. // 手指离开页面滑动
  265. handleTouchEnd() {
  266. this.setData({
  267. isRecording: false,
  268. popupToggle: false
  269. });
  270. uni.hideLoading();
  271. this.recorderManager.stop();
  272. },
  273. handleEmoji() {
  274. let targetFlag = 'emoji';
  275. if (this.displayFlag === 'emoji') {
  276. targetFlag = '';
  277. }
  278. this.setData({
  279. displayFlag: targetFlag
  280. });
  281. },
  282. handleExtensions() {
  283. let targetFlag = 'extension';
  284. if (this.displayFlag === 'extension') {
  285. targetFlag = '';
  286. }
  287. this.setData({
  288. displayFlag: targetFlag
  289. });
  290. },
  291. error(e) {
  292. console.log(e.detail);
  293. },
  294. handleSendPicture() {
  295. this.sendImageMessage('camera');
  296. },
  297. handleSendImage() {
  298. this.sendImageMessage('album');
  299. },
  300. sendImageMessage(type) {
  301. uni.chooseImage({
  302. sourceType: [type],
  303. count: 1,
  304. success: res => {
  305. if (res) {
  306. const message = uni.$TUIKit.createImageMessage({
  307. to: this.getToAccount(),
  308. conversationType: this.conversation.type,
  309. payload: {
  310. file: res
  311. },
  312. onProgress: percent => {
  313. message.percent = percent;
  314. }
  315. });
  316. this.$sendTIMMessage(message);
  317. }
  318. }
  319. });
  320. },
  321. handleShootVideo() {
  322. this.sendVideoMessage('camera');
  323. },
  324. handleSendVideo() {
  325. this.sendVideoMessage('album');
  326. },
  327. sendVideoMessage(type) {
  328. uni.chooseVideo({
  329. sourceType: [type],
  330. // 来源相册或者拍摄
  331. maxDuration: 60,
  332. // 设置最长时间60s
  333. camera: 'back',
  334. // 后置摄像头
  335. success: res => {
  336. if (res) {
  337. const message = uni.$TUIKit.createVideoMessage({
  338. to: this.getToAccount(),
  339. conversationType: this.conversation.type,
  340. payload: {
  341. file: res
  342. },
  343. onProgress: percent => {
  344. message.percent = percent;
  345. }
  346. });
  347. this.$sendTIMMessage(message);
  348. }
  349. }
  350. });
  351. },
  352. handleCommonFunctions(e) {
  353. switch (e.target.dataset.function.key) {
  354. case '0':
  355. this.setData({
  356. displayCommonWords: true
  357. });
  358. break;
  359. case '1':
  360. this.setData({
  361. displayOrderList: true
  362. });
  363. break;
  364. case '2':
  365. this.setData({
  366. displayServiceEvaluation: true
  367. });
  368. break;
  369. default:
  370. break;
  371. }
  372. },
  373. handleSendOrder() {
  374. this.setData({
  375. displayOrderList: true
  376. });
  377. },
  378. appendMessage(e) {
  379. this.setData({
  380. inputText: this.inputText + e.detail.message,
  381. sendMessageBtn: true
  382. });
  383. },
  384. getToAccount() {
  385. if (!this.conversation || !this.conversation.conversationID) {
  386. return '';
  387. }
  388. switch (this.conversation.type) {
  389. case 'C2C':
  390. return this.conversation.conversationID.replace('C2C', '');
  391. case 'GROUP':
  392. return this.conversation.conversationID.replace('GROUP', '');
  393. default:
  394. return this.conversation.conversationID;
  395. }
  396. },
  397. handleCalling(value) {
  398. // todo 目前支持单聊
  399. if (this.conversation.type === 'GROUP') {
  400. uni.showToast({
  401. title: '群聊暂不支持',
  402. icon: 'none'
  403. });
  404. return;
  405. }
  406. const { userID } = this.conversation.userProfile;
  407. // #ifdef APP-PLUS
  408. if(typeof(uni.$TUICalling) === 'undefined') {
  409. logger.error('请使用真机运行并且自定义基座调试,可能影响音视频功能~ 插件地址:https://ext.dcloud.net.cn/plugin?id=7097 , 调试地址:https://nativesupport.dcloud.net.cn/NativePlugin/use/use');
  410. uni.showToast({
  411. title: '请使用真机运行并且自定义基座调试,可能影响音视频功能~ ',
  412. icon: 'none',
  413. duration: 3000
  414. });
  415. } else {
  416. uni.$TUICalling.call(
  417. {
  418. userID: userID,
  419. type: value
  420. },
  421. res => {
  422. console.log(JSON.stringify(res));
  423. }
  424. );
  425. }
  426. // #endif
  427. // #ifdef MP-WEIXIN
  428. uni.showToast({
  429. title: '微信小程序暂不支持',
  430. icon: 'none'
  431. });
  432. // uni.$wxTUICalling.call({userID, type: value})
  433. // #endif
  434. },
  435. sendTextMessage(msg, flag) {
  436. const to = this.getToAccount();
  437. const text = flag ? msg : this.inputText;
  438. const message = uni.$TUIKit.createTextMessage({
  439. to,
  440. conversationType: this.conversation.type,
  441. payload: {
  442. text
  443. }
  444. });
  445. this.setData({
  446. inputText: '',
  447. sendMessageBtn: false
  448. });
  449. this.$sendTIMMessage(message);
  450. },
  451. onInputValueChange(event) {
  452. if (event.detail.value) {
  453. this.setData({
  454. sendMessageBtn: true
  455. });
  456. } else {
  457. this.setData({
  458. sendMessageBtn: false
  459. });
  460. }
  461. },
  462. $handleSendTextMessage(event) {
  463. this.sendTextMessage(event.detail.message, true);
  464. this.setData({
  465. displayCommonWords: false
  466. });
  467. },
  468. $handleSendCustomMessage(e) {
  469. const message = uni.$TUIKit.createCustomMessage({
  470. to: this.getToAccount(),
  471. conversationType: this.conversation.type,
  472. payload: e.detail.payload
  473. });
  474. this.$sendTIMMessage(message);
  475. this.setData({
  476. displayOrderList: false
  477. });
  478. },
  479. $handleCloseCards(e) {
  480. switch (e.detail.key) {
  481. case '0':
  482. this.setData({
  483. displayCommonWords: false
  484. });
  485. break;
  486. case '1':
  487. this.setData({
  488. displayOrderList: false
  489. });
  490. break;
  491. case '2':
  492. this.setData({
  493. displayServiceEvaluation: false
  494. });
  495. break;
  496. default:
  497. break;
  498. }
  499. },
  500. $sendTIMMessage(message) {
  501. const SDKAppID = getApp().globalData.SDKAppID;
  502. this.$emit('sendMessage', {
  503. detail: {
  504. message
  505. }
  506. });
  507. uni.$TUIKit.sendMessage(message).then((res) => {
  508. if(this.firstSendMessage) {
  509. uni.$aegis.reportEvent({
  510. name: 'sendMessage',
  511. ext1: 'sendMessage-success',
  512. ext2: 'uniTuikitExternal',
  513. ext3: `${SDKAppID}`,
  514. })
  515. }
  516. this.firstSendMessage = false
  517. }).catch((error) => {
  518. uni.$aegis.reportEvent({
  519. name: 'sendMessage',
  520. ext1: `sendMessage-failed#error: ${error}`,
  521. ext2: 'uniTuikitExternal',
  522. ext3: `${SDKAppID}`,
  523. })
  524. })
  525. this.setData({
  526. displayFlag: ''
  527. });
  528. },
  529. handleClose() {
  530. this.setData({
  531. displayFlag: ''
  532. });
  533. },
  534. handleServiceEvaluation() {
  535. this.setData({
  536. displayServiceEvaluation: true
  537. });
  538. },
  539. inputBindFocus() {
  540. console.log('占位:函数 inputBindFocus 未声明');
  541. },
  542. inputBindBlur() {
  543. console.log('占位:函数 inputBindBlur 未声明');
  544. }
  545. }
  546. };
  547. </script>
  548. <style>
  549. @import './index.css';
  550. </style>