index.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968
  1. <template>
  2. <view class="chating_footer" :style="{ paddingBottom: keyboardHeight + 'px' }">
  3. <view class="forbidden_footer" v-if="getPlaceholder.length > 0">
  4. <image style="margin-right: 8rpx;width:50rpx" src="../../../../../static/images/forbidden_footer.png" mode="aspectFit" />
  5. <text>{{ getPlaceholder }}</text>
  6. </view>
  7. <view v-show="getPlaceholder.length <= 0" :style="{ 'pointer-events': getPlaceholder ? 'none' : 'auto' }">
  8. <view class="chat_footer">
  9. <image v-if="showRecord"
  10. @click="updateRecordBtn"
  11. style="width: 26px; height: 26px;"
  12. src="../../../../../static/images/chating_footer_audio_recording.png" />
  13. <image v-else
  14. @click="updateRecordBtn"
  15. style="width: 26px; height: 26px;"
  16. src="../../../../../static/images/chating_footer_audio.png" />
  17. <view class="input_content">
  18. <!-- 临时替换为 CustomTextarea,原 CustomEditor 已注释 -->
  19. <CustomTextarea
  20. :placeholder="getPlaceholder"
  21. :value="inputHtml"
  22. v-show="!showRecord"
  23. class="custom_editor"
  24. ref="customEditor"
  25. @focus="editorFocus"
  26. @blur="editorBlur"
  27. @input="editorInput"
  28. @tryAt="showAtPanel"
  29. @confirm="sendTextMessage"
  30. />
  31. <!-- <CustomEditor
  32. :placeholder="getPlaceholder"
  33. v-show="!showRecord"
  34. class="custom_editor"
  35. ref="customEditor"
  36. @ready="editorReady"
  37. @focus="editorFocus"
  38. @blur="editorBlur"
  39. @input="editorInput"
  40. @tryAt="showAtPanel"/> -->
  41. <view v-if="storeQuoteMessage" class="quote_message">
  42. <view class="content">
  43. <u-parse :content="getQuotedContent" />
  44. </view>
  45. <image @click="cancelQuote" style="width: 16px; height: 16px" src="../../../../../static/images/chating_footer_quote_close.png" alt="" />
  46. </view>
  47. <button class="record_btn" @longpress="longpressRecord" @touchmove="touchMoveSpeech" @touchend="endRecord" v-show="showRecord" customStyle="height:30px;">
  48. {{ recording ? '松手发送' : '按住说话' }}
  49. </button>
  50. </view>
  51. <view class="footer_action_area">
  52. <image @click.prevent="updateEmojiBar" class="emoji_action" src="../../../../../static/images/chating_footer_emoji.png" alt="" srcset="" />
  53. <image @click.prevent="updateActionBar" v-show="!hasContent" src="../../../../../static/images/chating_footer_add.png" alt="" srcset="" />
  54. <button class="btnSend" v-show="hasContent && !emojiBarVisible" @touchend.prevent="sendTextMessage">发送</button>
  55. <button class="btnSend" v-show="hasContent && emojiBarVisible" @touchend.prevent="sendTextMessage">发送</button>
  56. <!-- <image v-show="hasContent && !emojiBarVisible" @touchend.prevent="sendTextMessage" src="../../../../../static/images/send_btn.png" alt="" srcset="" /> -->
  57. <!-- <image v-show="hasContent && emojiBarVisible" @click.prevent="sendTextMessage" src="../../../../../static/images/send_btn.png" alt="" srcset="" /> -->
  58. </view>
  59. </view>
  60. <chating-action-bar @sendMessage="sendMessage" @prepareMediaMessage="prepareMediaMessage" v-show="actionBarVisible" />
  61. <chating-emoji-bar @insertEmoji="emojiClick" @backspace="backspace" v-show="emojiBarVisible" />
  62. <u-action-sheet
  63. :safeAreaInsetBottom="true"
  64. round="12"
  65. :actions="actionSheetMenu"
  66. @select="selectClick"
  67. :closeOnClickOverlay="true"
  68. :closeOnClickAction="true"
  69. :show="showActionSheet"
  70. @close="showActionSheet = false"></u-action-sheet>
  71. <record-overlay @recordFinish="recordFinish" ref="recordOverlayRef" :visible="recording" />
  72. </view>
  73. </view>
  74. </template>
  75. <script>
  76. import { mapGetters, mapActions } from 'vuex';
  77. import { formatInputHtml, getPurePath, html2Text } from '../../../../../util/common';
  78. import { parseMessageByType, parseAt } from '../../../../../util/imCommon';
  79. import { ChatingFooterActionTypes, UpdateMessageTypes, GroupMemberListTypes, PageEvents } from '../../../../../constant';
  80. import IMSDK, { GroupMemberRole, GroupStatus, IMMethods, MessageStatus, MessageType, SessionType } from 'openim-uniapp-polyfill';
  81. import UParse from '../../../../../components/gaoyia-parse/parse.vue';
  82. import CustomEditor from './CustomEditor.vue';
  83. import CustomTextarea from './CustomTextarea.vue';
  84. import ChatingActionBar from './ChatingActionBar.vue';
  85. import ChatingEmojiBar from './ChatingEmojiBar.vue';
  86. import RecordOverlay from './RecordOverlay.vue';
  87. import { gotoAppPermissionSetting, requestAndroidPermission, judgeIosPermission } from '../../../../../util/permission';
  88. import { he } from 'date-fns/locale';
  89. const needClearTypes = [
  90. // MessageType.AdvanceTextMessage,
  91. MessageType.TextMessage,
  92. MessageType.AtTextMessage,
  93. MessageType.QuoteMessage
  94. ];
  95. const albumChoose = [
  96. {
  97. name: '图片',
  98. type: ChatingFooterActionTypes.Album,
  99. idx: 0
  100. },
  101. {
  102. name: '视频',
  103. type: ChatingFooterActionTypes.Album,
  104. idx: 1
  105. }
  106. ];
  107. const cameraChoose = [
  108. {
  109. name: '拍照',
  110. type: ChatingFooterActionTypes.Camera,
  111. idx: 0
  112. },
  113. {
  114. name: '录制',
  115. type: ChatingFooterActionTypes.Camera,
  116. idx: 1
  117. }
  118. ];
  119. export default {
  120. components: {
  121. CustomEditor,
  122. CustomTextarea,
  123. ChatingActionBar,
  124. ChatingEmojiBar,
  125. UParse,
  126. RecordOverlay
  127. },
  128. props: {
  129. footerOutsideFlag: Number
  130. },
  131. data() {
  132. return {
  133. customEditorCtx: null,
  134. inputHtml: '',
  135. showRecord: false,
  136. actionBarVisible: false,
  137. emojiBarVisible: false,
  138. isInputFocus: false,
  139. actionSheetMenu: [],
  140. showActionSheet: false,
  141. recording: false,
  142. recordTop: uni.getWindowInfo().windowHeight - 130,
  143. conversationID: null,
  144. isJoinGroup: true,
  145. isSending: false, // 发送状态标志
  146. isClickAction:false,
  147. isClickEmoji:false,
  148. keyboardHeight: 0 // 记录键盘高度
  149. };
  150. },
  151. computed: {
  152. ...mapGetters(['storeQuoteMessage', 'storeCurrentConversation', 'storeCurrentGroup', 'storeCurrentMemberInGroup', 'storeBlackList', 'storeRevokeMap','timStore']),
  153. getQuotedContent() {
  154. if (!this.storeQuoteMessage) {
  155. return '';
  156. }
  157. return `回复${parseMessageByType(this.storeQuoteMessage)}`;
  158. },
  159. hasContent() {
  160. return html2Text(this.inputHtml) !== '';
  161. },
  162. isNomal() {
  163. return this.storeCurrentMemberInGroup.roleLevel === GroupMemberRole.Nomal;
  164. },
  165. getPlaceholder() {
  166. const isSingle = this.storeCurrentConversation.conversationType === SessionType.Single;
  167. if (!isSingle && this.storeCurrentGroup.status === GroupStatus.Muted) {
  168. return !this.isNomal ? '' : '群主或管理员已开启全体禁言';
  169. }
  170. if (!isSingle && !this.isJoinGroup) {
  171. return '您已不在该群组';
  172. }
  173. if (!isSingle && this.storeCurrentMemberInGroup.muteEndTime > Date.now()) {
  174. return `您已被管理员或群主禁言!`;
  175. }
  176. if (isSingle && this.storeBlackList.find((black) => black.userID === this.storeCurrentConversation.userID)) {
  177. return '对方已被拉入黑名单';
  178. }
  179. return '';
  180. },
  181. imType() {
  182. return this.timStore?.imType ?? '' ;
  183. },
  184. orderId() {
  185. return this.timStore?.orderId ?? '' ;
  186. },
  187. orderType() {
  188. return this.timStore?.orderType ?? '' ;
  189. },
  190. },
  191. watch: {
  192. footerOutsideFlag(newVal) {
  193. this.onClickActionBarOutside();
  194. this.onClickEmojiBarOutside();
  195. },
  196. actionBarVisible(val) {
  197. this.checkComplaintButtonVisible();
  198. },
  199. emojiBarVisible(val) {
  200. this.checkComplaintButtonVisible();
  201. }
  202. },
  203. mounted() {
  204. this.setIMListener();
  205. this.setKeyboardListener();
  206. if (this.storeCurrentConversation.groupID) {
  207. IMSDK.asyncApi(IMMethods.IsJoinGroup, IMSDK.uuid(), this.storeCurrentConversation.groupID).then((res) => {
  208. this.isJoinGroup = res.data;
  209. });
  210. }
  211. this.conversationID = this.storeCurrentConversation.conversationID;
  212. this.parseDraftText(this.storeCurrentConversation.draftText);
  213. IMSDK.asyncApi(IMMethods.SetConversationDraft, IMSDK.uuid(), {
  214. conversationID: this.conversationID,
  215. draftText: ''
  216. });
  217. uni.$on(PageEvents.ReEditMessage, this.setReEditMessage);
  218. uni.$on("sendIM", this.sendPackageImMsg);
  219. uni.$on("sendIMCts", this.sendCtsImMsg);
  220. },
  221. beforeDestroy() {
  222. uni.$emit('showComplaintButton');
  223. this.disposeIMListener();
  224. this.disposeKeyboardListener();
  225. if (this.inputHtml && this.inputHtml.trim()) {
  226. IMSDK.asyncApi(IMMethods.SetConversationDraft, IMSDK.uuid(), {
  227. conversationID: this.conversationID,
  228. draftText: html2Text(this.inputHtml)
  229. });
  230. }
  231. uni.$off("sendIM", this.sendPackageImMsg);
  232. uni.$off("sendIMCts", this.sendCtsImMsg);
  233. },
  234. methods: {
  235. ...mapActions('message', ['pushNewMessage', 'updateOneMessage', 'updateQuoteMessageRevoke']),
  236. checkComplaintButtonVisible() {
  237. this.$nextTick(() => {
  238. if (this.actionBarVisible || this.emojiBarVisible) {
  239. uni.$emit('hideComplaintButton');
  240. } else {
  241. uni.$emit('showComplaintButton');
  242. }
  243. });
  244. },
  245. setReEditMessage(id) {
  246. if (this.$refs.customEditor) {
  247. this.$refs.customEditor.clear();
  248. }
  249. setTimeout(() => {
  250. this.$refs.customEditor.insertEmoji(this.storeRevokeMap[id].text);
  251. this.$store.commit('message/SET_QUOTE_MESSAGE', this.storeRevokeMap[id].quoteMessage);
  252. }, 50);
  253. },
  254. parseDraftText(draftText) {
  255. let currentText = '';
  256. for (let i = 0; i < draftText.length; i++) {
  257. const currentChar = draftText[i];
  258. if (currentChar === '<') {
  259. if (currentText) {
  260. const text = currentText;
  261. setTimeout(() => {
  262. this.$refs.customEditor.insertEmoji(text);
  263. }, 100);
  264. currentText = '';
  265. }
  266. const imgEndIndex = draftText.indexOf('>', i);
  267. if (imgEndIndex !== -1) {
  268. const imgTag = draftText.substring(i, imgEndIndex + 1);
  269. if (imgTag.includes('class="at_el"')) {
  270. const customDataReg = /data-custom=".+"/;
  271. const atInfoArr = imgTag.match(customDataReg)[0].slice(13, -1).split('&amp;');
  272. this.$refs.customEditor.createCanvasData(atInfoArr[0].slice(7), atInfoArr[1].slice(15));
  273. }
  274. i = imgEndIndex;
  275. }
  276. } else {
  277. currentText += currentChar;
  278. }
  279. }
  280. if (currentText) {
  281. setTimeout(() => {
  282. this.$refs.customEditor.insertEmoji(currentText);
  283. }, 700);
  284. }
  285. },
  286. longpressRecord() {
  287. this.recording = true;
  288. this.$nextTick(() => this.getRecordAreaData());
  289. },
  290. touchMoveSpeech(e) {
  291. uni.$u.throttle(this.$refs.recordOverlayRef.touchMoveSpeech(e, this.recordTop), 250);
  292. },
  293. endRecord() {
  294. this.recording = false;
  295. this.$refs.recordOverlayRef.checkStopType();
  296. },
  297. async recordFinish(path, duration) {
  298. const message = await IMSDK.asyncApi(IMMethods.CreateSoundMessageFromFullPath, IMSDK.uuid(), {
  299. soundPath: getPurePath(path),
  300. duration
  301. });
  302. this.sendMessage(message);
  303. },
  304. getRecordAreaData() {
  305. this.getEl('.record_area').then((data) => {
  306. if (data) {
  307. this.recordTop = data.top;
  308. }
  309. });
  310. },
  311. getEl(el) {
  312. return new Promise((resolve) => {
  313. const query = uni.createSelectorQuery().in(this);
  314. query.select(el).boundingClientRect((data) => {
  315. resolve(data);
  316. }).exec();
  317. });
  318. },
  319. async createTextMessage() {
  320. let message = '';
  321. // 切换为 textarea 后,inputHtml 实际上是纯文本
  322. const text = String(this.inputHtml || '');
  323. console.log('createTextMessage text:',text);
  324. // 简单的 @ 解析逻辑:查找 @Name 格式
  325. // 这里暂时简化处理,如果需要精确的 @ 功能,需要维护一个 @ 用户列表
  326. // 暂时只发送普通文本,后续可以增强 @ 解析
  327. if (this.storeQuoteMessage) {
  328. console.log("1111");
  329. message = await IMSDK.asyncApi(IMMethods.CreateQuoteMessage, IMSDK.uuid(), {
  330. text,
  331. message: this.storeQuoteMessage
  332. });
  333. } else {
  334. console.log("2222",text);
  335. message = await IMSDK.asyncApi(IMMethods.CreateTextMessage, IMSDK.uuid(), text);
  336. }
  337. if (!message) {
  338. throw new Error('CreateTextMessage returned null/undefined');
  339. }
  340. return message;
  341. },
  342. async sendTextMessage() {
  343. console.log('sendTextMessage inputHtml:', this.inputHtml);
  344. if (!this.inputHtml || !this.inputHtml.trim()) {
  345. console.log('sendTextMessage: input is empty');
  346. return;
  347. }
  348. // 非关键操作,忽略错误
  349. try {
  350. IMSDK.asyncApi('changeInputStates', IMSDK.uuid(), {
  351. conversationID: this.storeCurrentConversation.conversationID,
  352. focus: false
  353. });
  354. } catch (e) {
  355. console.warn('changeInputStates failed:', e);
  356. }
  357. try {
  358. this.isSending = true; // 开始发送,设置标志位
  359. const message = await this.createTextMessage();
  360. console.log('createTextMessage result:', message);
  361. await this.sendMessage(message);
  362. // 发送成功后清空
  363. this.inputHtml = '';
  364. if (this.$refs.customEditor) {
  365. this.$refs.customEditor.clear();
  366. }
  367. } catch (error) {
  368. console.error('发送消息失败:', error);
  369. // 详细打印 error 对象
  370. if (typeof error === 'object') {
  371. console.log('Error details:', JSON.stringify(error));
  372. }
  373. uni.showToast({
  374. title: '发送失败: ' + (error.errMsg || '未知错误'),
  375. icon: 'none'
  376. });
  377. } finally {
  378. this.isSending = false; // 发送完成,重置标志位
  379. }
  380. },
  381. async sendCtsPackageMessage(item){
  382. let payloadJson={
  383. payload:{
  384. data:"package",
  385. extension:{
  386. packageId:item.packageId,
  387. title:item.packageName,
  388. companyId:item.companyId,
  389. companyUserId:item.companyUserId,
  390. image:item.imgUrl
  391. }
  392. }
  393. };
  394. let message = await IMSDK.asyncApi(IMMethods.CreateCustomMessage, IMSDK.uuid(), {
  395. data: JSON.stringify(payloadJson),
  396. extension: '',
  397. description: 'package'
  398. });
  399. this.sendMessage(message);
  400. },
  401. sendPackageImMsg(item){
  402. console.log("qxj package:"+item.packageId);
  403. this.sendCtsPackageMessage(item);
  404. },
  405. async sendCtsMessage(item){
  406. let payloadJson={};
  407. if(item.type=="couponPackage"){
  408. payloadJson={
  409. payload:{
  410. data:item.type,
  411. extension:{
  412. couponId:item.couponId,
  413. title:item.title,
  414. price:item.price,
  415. minPrice:item.minPrice,
  416. limitTime:item.limitTime,
  417. companyId:item.companyId,
  418. companyUserId:item.companyUserId,
  419. //image:item.imgUrl
  420. }
  421. }
  422. };
  423. }
  424. if(item.type=="inquirySelect"){
  425. payloadJson={
  426. payload:{
  427. data:item.type,
  428. extension:{
  429. packageId:item.packageId,
  430. title:item.lable,
  431. type:item.value,
  432. companyId:item.companyId,
  433. companyUserId:item.companyUserId,
  434. //image:item.imgUrl
  435. }
  436. }
  437. };
  438. }
  439. let message = await IMSDK.asyncApi(IMMethods.CreateCustomMessage, IMSDK.uuid(), {
  440. data: JSON.stringify(payloadJson),
  441. extension: '',
  442. description: item.type
  443. });
  444. this.sendMessage(message);
  445. },
  446. sendCtsImMsg(item){
  447. this.sendCtsMessage(item);
  448. },
  449. sendMessage(message) {
  450. var customerData={
  451. type:this.timStore?.type,
  452. imType:this.timStore?.imType,
  453. orderId:this.timStore?.orderId,
  454. followId:this.timStore?.followId,
  455. orderType:this.timStore?.orderType
  456. }
  457. let userId=this.storeCurrentConversation.userID;
  458. if(userId!=undefined && (userId!="" || userId.length>0)){
  459. if(userId.indexOf('D')!==-1){
  460. message.ex=JSON.stringify(customerData);
  461. }
  462. }
  463. this.pushNewMessage(message);
  464. if (needClearTypes.includes(message.contentType)) {
  465. if(this.$refs.customEditor){
  466. this.$refs.customEditor.clear();
  467. }
  468. }
  469. let offlinePushInfo={
  470. title:message.senderNickname,
  471. desc: parseMessageByType(message),
  472. ex: "",
  473. iOSPushSound: "",
  474. iOSBadgeCount: true
  475. };
  476. this.$emit('scrollToBottom');
  477. IMSDK.asyncApi(IMMethods.SendMessage, IMSDK.uuid(), {
  478. recvID: this.storeCurrentConversation.userID,
  479. groupID: this.storeCurrentConversation.groupID,
  480. message,
  481. offlinePushInfo
  482. }).then(({ data }) => {
  483. this.updateOneMessage({
  484. message: data,
  485. isSuccess: true
  486. });
  487. }).catch(({ data, errCode, errMsg }) => {
  488. this.updateOneMessage({
  489. message: data,
  490. type: UpdateMessageTypes.KeyWords,
  491. keyWords: [
  492. {
  493. key: 'status',
  494. value: MessageStatus.Failed
  495. },
  496. {
  497. key: 'errCode',
  498. value: errCode
  499. }
  500. ]
  501. });
  502. });
  503. if (this.storeQuoteMessage) {
  504. this.$store.commit('message/SET_QUOTE_MESSAGE', undefined);
  505. }
  506. },
  507. // action
  508. cancelQuote() {
  509. this.$store.commit('message/SET_QUOTE_MESSAGE', undefined);
  510. },
  511. onClickActionBarOutside() {
  512. if (this.actionBarVisible) {
  513. this.actionBarVisible = false;
  514. }
  515. },
  516. onClickEmojiBarOutside() {
  517. if (this.emojiBarVisible) {
  518. this.emojiBarVisible = false;
  519. }
  520. },
  521. updateEmojiBar() {
  522. if (this.recording) {
  523. return;
  524. }
  525. if (this.showRecord) {
  526. this.showRecord = false;
  527. }
  528. this.onClickActionBarOutside();
  529. if(!this.emojiBarVisible){
  530. this.emojiBarVisible=true;
  531. this.$emit('scrollToBottom');
  532. }
  533. this.isClickEmoji=true;
  534. setTimeout(()=>{
  535. this.isClickEmoji=false;
  536. },300);
  537. //this.emojiBarVisible = !this.emojiBarVisible;
  538. },
  539. updateActionBar() {
  540. if (this.recording) {
  541. return;
  542. }
  543. if (this.showRecord) {
  544. this.showRecord = false;
  545. }
  546. this.onClickEmojiBarOutside();
  547. this.actionBarVisible = !this.actionBarVisible;
  548. this.isClickAction=true;
  549. setTimeout(()=>{
  550. this.isClickAction=false;
  551. },300);
  552. },
  553. editorInput(e) {
  554. // 适配 Textarea 的 input 事件
  555. const value = e.detail.value || '';
  556. this.inputHtml = value;
  557. },
  558. editorReady(res) {
  559. // Textarea 不需要 context
  560. this.$emit('ready', res);
  561. },
  562. editorFocus() {
  563. this.isInputFocus = true;
  564. this.$emit('focus');
  565. // 优化:键盘弹起前先给个预估高度,防止被遮挡
  566. if (this.keyboardHeight === 0) {
  567. const lastHeight = uni.getStorageSync('lastKeyboardHeight') || 300;
  568. this.keyboardHeight = lastHeight;
  569. }
  570. },
  571. editorBlur() {
  572. this.isInputFocus = false;
  573. this.$emit('blur');
  574. },
  575. showAtPanel() {
  576. if (!this.$store.getters.storeCurrentConversation.groupID) return;
  577. uni.$u.route('/pages_im/pages/conversation/groupMemberList/index', {
  578. type: GroupMemberListTypes.ChooseAt,
  579. groupID: this.$store.getters.storeCurrentConversation.groupID
  580. });
  581. },
  582. backspace() {
  583. if (this.$refs.customEditor) {
  584. this.$refs.customEditor.backspace();
  585. }
  586. },
  587. updateRecordBtn() {
  588. if (this.recording) {
  589. return;
  590. }
  591. if (!this.showRecord) {
  592. this.checkRecordPermission();
  593. }
  594. this.showRecord = !this.showRecord;
  595. this.onClickActionBarOutside();
  596. this.onClickEmojiBarOutside();
  597. this.$emit('scrollToBottom');
  598. },
  599. async checkRecordPermission() {
  600. if (uni.$u.os() === 'ios') {
  601. const iosFlag = judgeIosPermission('record');
  602. if (iosFlag === 0) {
  603. uni.$u.toast('您已禁止访问麦克风,请前往开启');
  604. setTimeout(() => gotoAppPermissionSetting(), 250);
  605. }
  606. if (iosFlag === -1) {
  607. let recorderManager = uni.getRecorderManager();
  608. recorderManager.onStop(() => (recorderManager = null));
  609. recorderManager.start();
  610. setTimeout(() => recorderManager.stop());
  611. }
  612. } else {
  613. const androidFlag = await requestAndroidPermission('android.permission.RECORD_AUDIO');
  614. if (androidFlag === -1) {
  615. uni.$u.toast('您已禁止访问麦克风,请前往开启');
  616. setTimeout(() => gotoAppPermissionSetting(), 250);
  617. }
  618. }
  619. },
  620. prepareMediaMessage(data) {
  621. // if (data.type === ChatingFooterActionTypes.Album) {
  622. // this.actionSheetMenu = [...albumChoose];
  623. // } else {
  624. // this.actionSheetMenu = [...cameraChoose];
  625. // }
  626. // this.showActionSheet = false;
  627. console.log("qxj prepareMediaMessage data:"+JSON.stringify(data));
  628. this.selectClick(data);
  629. },
  630. updateTyping() {
  631. if (this.storeCurrentConversation.conversationType === SessionType.Single) {
  632. IMSDK.asyncApi('changeInputStates', IMSDK.uuid(), {
  633. conversationID: this.storeCurrentConversation.conversationID,
  634. focus: true
  635. });
  636. }
  637. },
  638. // from comp
  639. emojiClick(emoji) {
  640. this.$refs.customEditor.insertEmoji(emoji);
  641. },
  642. batchCreateImageMesage(paths) {
  643. paths.forEach(async (path) => {
  644. const message = await IMSDK.asyncApi(IMMethods.CreateImageMessageFromFullPath, IMSDK.uuid(), getPurePath(path));
  645. this.sendMessage(message);
  646. });
  647. },
  648. selectClick(data) {
  649. if (data.index === 0) {
  650. if (data.type === ChatingFooterActionTypes.Album) {
  651. this.chooseOrShotImage(['album']).then((paths) => this.batchCreateImageMesage(paths));
  652. } else {
  653. this.chooseOrShotImage(['camera']).then((paths) => this.batchCreateImageMesage(paths));
  654. }
  655. }
  656. else {
  657. const whenGetFile = (data) => {
  658. const purePath = getPurePath(data.path);
  659. IMSDK.getVideoCover(purePath).then(async ({ path }) => {
  660. console.log(getPurePath(path));
  661. const message = await IMSDK.asyncApi(IMMethods.CreateVideoMessageFromFullPath, IMSDK.uuid(), {
  662. videoPath: purePath,
  663. videoType: data.videoType,
  664. duration: Number(data.duration.toFixed()),
  665. snapshotPath: getPurePath(path)
  666. });
  667. this.sendMessage(message);
  668. });
  669. };
  670. if (data.type === ChatingFooterActionTypes.Album) {
  671. this.chooseOrShotVideo(['album']).then(whenGetFile);
  672. } else {
  673. this.chooseOrShotVideo(['camera']).then(whenGetFile);
  674. }
  675. }
  676. },
  677. chooseOrShotImage(sourceType) {
  678. return new Promise((resolve, reject) => {
  679. uni.chooseImage({
  680. count: 9,
  681. sizeType: ['compressed'],
  682. sourceType,
  683. success: function ({ tempFilePaths }) {
  684. resolve(tempFilePaths);
  685. },
  686. fail: function (err) {
  687. console.log(err);
  688. reject(err);
  689. }
  690. });
  691. });
  692. },
  693. chooseOrShotVideo(sourceType) {
  694. return new Promise((resolve, reject) => {
  695. uni.chooseVideo({
  696. compressed: true,
  697. sourceType,
  698. extension: ['mp4'],
  699. success: function ({ tempFilePath, duration }) {
  700. const idx = tempFilePath.lastIndexOf('.');
  701. const videoType = tempFilePath.slice(idx + 1);
  702. if (tempFilePath.includes('_doc/')) {
  703. tempFilePath = `file://${plus.io.convertLocalFileSystemURL(tempFilePath)}`;
  704. }
  705. console.log(tempFilePath);
  706. resolve({
  707. path: tempFilePath,
  708. videoType,
  709. duration
  710. });
  711. },
  712. fail: function (err) {
  713. console.log(err);
  714. reject(err);
  715. }
  716. });
  717. });
  718. },
  719. insertAt(userID, nickname) {
  720. const { atUsersInfo } = formatInputHtml(this.inputHtml);
  721. if (atUsersInfo.find((item) => item.atUserID === userID)) {
  722. return;
  723. }
  724. this.$refs.customEditor.createCanvasData(userID, nickname);
  725. },
  726. // im listener
  727. sendProgressHandler({ data: { progress, message } }) {
  728. this.updateOneMessage({
  729. message,
  730. type: UpdateMessageTypes.KeyWords,
  731. keyWords: {
  732. key: 'uploadProgress',
  733. value: progress
  734. }
  735. });
  736. },
  737. joinedGroupChangeHandler({ data }) {
  738. if (data.groupID === this.storeCurrentConversation.groupID) {
  739. IMSDK.asyncApi(IMMethods.IsJoinGroup, IMSDK.uuid(), data.groupID).then((res) => {
  740. this.isJoinGroup = res.data;
  741. });
  742. }
  743. },
  744. setIMListener() {
  745. IMSDK.subscribe("SendMessageProgress", this.sendProgressHandler);
  746. uni.$on(IMSDK.IMEvents.OnJoinedGroupDeleted, this.joinedGroupChangeHandler);
  747. uni.$on(IMSDK.IMEvents.OnJoinedGroupAdded, this.joinedGroupChangeHandler);
  748. },
  749. disposeIMListener() {
  750. IMSDK.unsubscribe("SendMessageProgress", this.sendProgressHandler);
  751. uni.$off(IMSDK.IMEvents.OnJoinedGroupDeleted, this.joinedGroupChangeHandler);
  752. uni.$off(IMSDK.IMEvents.OnJoinedGroupAdded, this.joinedGroupChangeHandler);
  753. },
  754. // keyboard
  755. setKeyboardListener() {
  756. uni.onKeyboardHeightChange((res) => {
  757. this.keyboardChangeHander(res);
  758. });
  759. },
  760. disposeKeyboardListener() {
  761. uni.offKeyboardHeightChange(this.keyboardChangeHander);
  762. },
  763. keyboardChangeHander(res) {
  764. // 当键盘高度变化时,如果需要手动调整 footer 位置(adjust-position=false时)
  765. // 目前我们的布局通常是 flex,可能不需要额外设置 bottom,除非是 fixed 定位
  766. // 但为了确保万无一失,我们可以通知父组件或调整自身样式
  767. // 更新键盘高度,用于动态调整 padding-bottom
  768. //console.log('keyboardChangeHander', res.height);
  769. this.keyboardHeight = res.height;
  770. if (res.height > 0) {
  771. uni.setStorage({ key: 'lastKeyboardHeight', data: res.height });
  772. }
  773. // 只有当高度真正变化时才触发
  774. // 这里我们主要处理一些状态同步,比如表情面板的互斥
  775. if (res.height > 0) {
  776. // 键盘弹起,关闭表情和功能面板
  777. if (this.actionBarVisible) {
  778. this.actionBarVisible = false;
  779. }
  780. if (this.emojiBarVisible) {
  781. this.emojiBarVisible = false;
  782. }
  783. this.$emit('keyboardHeightChange', res.height);
  784. } else {
  785. this.$emit('keyboardHeightChange', 0);
  786. }
  787. // 滚动到底部
  788. if (res.height > 0) {
  789. setTimeout(() => {
  790. this.$emit('scrollToBottom');
  791. }, 200);
  792. }
  793. }
  794. }
  795. };
  796. </script>
  797. <script module="snap" lang="renderjs">
  798. export default {
  799. methods: {
  800. getSnapFlagUpdate(newValue, oldValue, ownerVm, vm) {
  801. if (newValue === null) {
  802. return;
  803. }
  804. const {
  805. path
  806. } = newValue
  807. this.getVideoSnshot(path).then(base64 => {
  808. ownerVm.callMethod('receiveSnapBase64', {
  809. ...newValue,
  810. base64
  811. })
  812. })
  813. },
  814. getVideoSnshot(item) {
  815. return new Promise((reslove, reject) => {
  816. var video = document.createElement("VIDEO");
  817. video.style.display = 'none';
  818. video.setAttribute('crossOrigin', 'anonymous');
  819. video.setAttribute("autoplay", "autoplay");
  820. video.setAttribute("muted", "muted");
  821. video.innerHTML = "<source src=" + item + ' type="audio/mp4">';
  822. var canvas = document.createElement("canvas");
  823. var ctx = canvas.getContext("2d");
  824. video.addEventListener("canplay", function() {
  825. var anw = document.createAttribute("width");
  826. anw.nodeValue = video.videoWidth;
  827. var anh = document.createAttribute("height");
  828. anh.nodeValue = video.videoHeight;
  829. canvas.setAttributeNode(anw);
  830. canvas.setAttributeNode(anh);
  831. ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
  832. var base64 = canvas.toDataURL("image/png");
  833. video.pause();
  834. reslove(base64);
  835. });
  836. });
  837. },
  838. },
  839. }
  840. </script>
  841. <style lang="scss" scoped>
  842. .chating_footer{
  843. }
  844. .custom_editor {
  845. img {
  846. vertical-align: sub;
  847. }
  848. }
  849. .forbidden_footer {
  850. width: 100%;
  851. height: 112rpx;
  852. color: #8e9ab0;
  853. display: flex;
  854. flex-direction: row;
  855. justify-content: center;
  856. align-items: center;
  857. background: #f0f2f6;
  858. flex-shrink: 0;
  859. flex-grow: 0;
  860. }
  861. .chat_footer {
  862. display: flex;
  863. align-items: center;
  864. // background-color: #e9f4ff;
  865. background: #f0f2f6;
  866. // height: 50px;
  867. max-height: 120px;
  868. padding: 24rpx 20rpx;
  869. .input_content {
  870. flex: 1;
  871. min-height: 30px;
  872. max-height: 120px;
  873. margin: 0 24rpx;
  874. border-radius: 8rpx;
  875. position: relative;
  876. font-size: 36rpx;
  877. .record_btn {
  878. // background-color: #3c9cff;
  879. background: #fff;
  880. color: black;
  881. height: 30px;
  882. line-height: 30px;
  883. }
  884. }
  885. .quote_message {
  886. @include vCenterBox();
  887. justify-content: space-between;
  888. margin-top: 12rpx;
  889. padding: 8rpx;
  890. // padding-top: 20rpx;
  891. border-radius: 6rpx;
  892. background-color: #fff;
  893. color: #666;
  894. .content {
  895. ::v-deep uni-view {
  896. @include ellipsisWithLine(2);
  897. }
  898. }
  899. }
  900. .footer_action_area {
  901. display: flex;
  902. align-items: center;
  903. .emoji_action {
  904. margin-right: 24rpx;
  905. }
  906. .btnSend{
  907. font-size: 36rpx;
  908. color: #fff;
  909. border-radius:6px;
  910. background-color:#1ab84d ;
  911. padding:20rpx 26rpx;
  912. }
  913. image {
  914. width: 26px;
  915. height: 26px;
  916. }
  917. }
  918. .send_btn {
  919. height: 30px;
  920. line-height: 30px;
  921. background-color: #4a9cfc;
  922. padding: 0 8px;
  923. border-radius: 4px;
  924. color: #fff;
  925. }
  926. }
  927. </style>