qq.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. <template>
  2. <div style="width: 100%;height: 100%">
  3. <el-tabs class="im-tabs" type="card" v-model="appKey" @tab-click="qwUserChange">
  4. <el-tab-pane
  5. v-for="item in qwUserList"
  6. :key="item.id"
  7. :label="item.qwUserId"
  8. :name="item.appKey"/>
  9. </el-tabs>
  10. <div class="imui-center qq-lemon-imui" v-show="showQW">
  11. <lemon-imui class="lemon-slot"
  12. :width="isMaximized ? '100vw' : windowWidth"
  13. :height="isMaximized ? '90vh' : windowHeight"
  14. :user="userData"
  15. ref="IMUI"
  16. :contact-contextmenu="contactContextmenu"
  17. :theme="theme"
  18. :hide-menu="hideMenu"
  19. :hide-menu-avatar="hideMenuAvatar"
  20. :hide-message-name="hideMessageName"
  21. :hide-message-time="hideMessageTime"
  22. :messageTimeFormat="messageTimeFormat"
  23. @change-menu="handleChangeMenu"
  24. @pull-messages="handlePullMessages"
  25. @change-contact="handleChangeContact"
  26. @change-conversation="handleChangeConversation"
  27. @message-click="handleMessageClick"
  28. @menu-avatar-click="handleMenuAvatarClick"
  29. @pick-image="handleImageClick"
  30. @send="handleSend">
  31. <template #sidebar-message-top>
  32. <div style="padding: 8px;">
  33. <el-input
  34. v-model="searchKeyword"
  35. prefix-icon="el-icon-search"
  36. placeholder="搜索"
  37. size="small"
  38. clearable
  39. @input="handleSearch"
  40. style="width: 100%;"
  41. />
  42. </div>
  43. </template>
  44. <template #message-title="contact">
  45. <!-- <div @click="openDrawer('right')" style="position: absolute;right: 14px;top: 10px;">-->
  46. <!-- <i class="el-icon-more" style="cursor: pointer"/>-->
  47. <!-- </div>-->
  48. <div>
  49. <el-button
  50. type="primary"
  51. size="small"
  52. style="position: absolute;right: 14px;top: 10px;z-index: 1000"
  53. @click="showDetail(contact.extId)"
  54. >详情</el-button>
  55. </div>
  56. </template>
  57. </lemon-imui>
  58. </div>
  59. <el-dialog :visible.sync="dialogVisible" append-to-body width="35%">
  60. <img style="width:100%;height:auto" :src="dialogImageUrl" alt="" />
  61. </el-dialog>
  62. <el-dialog :visible.sync="dialogVideoVisible"
  63. :close-on-click-modal="false"
  64. :destroy-on-close="true"
  65. :width="aplayer.pWidth"
  66. :height="aplayer.height"
  67. @close="dialogVideoVisible=false"
  68. ref="player">
  69. <videoPlayer :width="aplayer.width" :height="aplayer.height" :videoWidth="aplayer.videoWidth" :videoHeight="aplayer.videoHeight" :vid="aplayer.vid" :autoplay="true"
  70. :source="dialogVideoUrl" :cover="dialogVideoCover" ref="player"></videoPlayer>
  71. </el-dialog>
  72. <!-- 用户详情 -->
  73. <el-drawer
  74. append-to-body
  75. :with-header="false"
  76. size="35%"
  77. :destroy-on-close="true"
  78. :title="detail.title" :visible.sync="detail.open">
  79. <userDetail ref="userDetail" />
  80. </el-drawer>
  81. </div>
  82. </template>
  83. <script>
  84. import {getConversations,getMessageList,getConversation,sendMsg,getQwUserList} from '@/api/qw/im';
  85. import Conversations from "@/components/LemonUI/database/conversations";
  86. import EmojiData from "@/components/LemonUI/database/emoji";
  87. import '@/components/LemonUI/index.css';
  88. import VideoPlayer from '@/components/VideoPlayer/VueAliplayer.vue'
  89. import UserDetail from "@/views/qw/qwChat/userDetail/index.vue";
  90. import {ImSocket} from "@/utils/ImSocket";
  91. import {getToken} from '@/utils/auth'
  92. import {uploadOss} from "@/api/common";
  93. import {mapState} from "vuex";
  94. import {generateUUID} from "@/components/LemonUI/utils";
  95. let pages = {};
  96. export default {
  97. name: "qqChat",
  98. components: {
  99. VideoPlayer,
  100. UserDetail
  101. },
  102. props: {
  103. showQw: Boolean,
  104. isMaximized: Boolean
  105. },
  106. data() {
  107. return {
  108. contactName:'',
  109. appKey:null,
  110. qwUserId:null,
  111. qwUserList:[],
  112. theme: "default",
  113. IMUI:null,
  114. contactContextmenu: [],
  115. hideMenuAvatar: false,
  116. hideMenu: false,
  117. hideMessageName: false,
  118. hideMessageTime: true,
  119. qwUser:{},
  120. showQW:false,
  121. userData: {},
  122. contactData:null,
  123. conversationData:Conversations,
  124. dialogImageUrl: '',
  125. dialogVisible: false,
  126. dialogVideoVisible:false,
  127. dialogVideoUrl:'',
  128. dialogVideoCover:'',
  129. imageArr:[],
  130. pickUploadImgData:null,
  131. aplayer: {
  132. vid: "bf9b7e4a36d84aea8cee769765fbc28b",
  133. pWidth:"1040px",
  134. width:"1000px",
  135. height:"900px",
  136. videoWidth:"1000px",
  137. videoHeight:"900px"
  138. },
  139. player: null,
  140. roomMembers:[],
  141. roomAdmins:[],
  142. roomInfo:null,
  143. windowWidth: document.documentElement.clientWidth*0.6, //实时屏幕宽度
  144. windowHeight: document.documentElement.clientHeight*0.7, //实时屏幕高度
  145. queryParams: {
  146. pageNum: 1,
  147. pageSize: 10,
  148. conversationId: null,
  149. userId:null
  150. },
  151. detail: {
  152. title: '',
  153. open: false
  154. },
  155. imSocket: null,
  156. imUrl: process.env.VUE_APP_IM_WS_URL,
  157. searchKeyword: '',
  158. };
  159. },
  160. created(){
  161. //获取企微列表
  162. getQwUserList().then(response => {
  163. this.qwUserList = response.data;
  164. if (this.qwUserList.length>0){
  165. this.qwUser = this.qwUserList[0];
  166. this.appKey = this.qwUser.appKey;
  167. this.setQwUserInfo();
  168. this.getConversation();
  169. this.initImSocket(this.qwUser.companyId)
  170. }else {
  171. this.qwUser={};
  172. }
  173. });
  174. },
  175. computed: {
  176. ...mapState('qwIm', ['shareCourse'])
  177. },
  178. watch: {
  179. showQw(nv, ov) {
  180. if (nv) {
  181. this.$nextTick(() => {
  182. this.$refs.IMUI.messageViewToBottom();
  183. });
  184. }
  185. },
  186. shareCourse(nv) {
  187. if (nv) {
  188. // 发送小程序卡片消息
  189. const IMUI = this.$refs.IMUI;
  190. const contact = IMUI.currentContact;
  191. if (!contact) {
  192. return;
  193. }
  194. // 创建消息对象
  195. let message = {
  196. type: 'miniprogram',
  197. content: nv,
  198. toContactId: contact.conversationId,
  199. };
  200. this.detail.open = false
  201. this.$store.dispatch('qwIm/shareCourse', null)
  202. // 使用handleSend方法发送消息
  203. this.handleSend(
  204. message,
  205. (replaceMessage = { status: "succeed" }) => {
  206. IMUI.updateMessage(Object.assign(message, replaceMessage));
  207. },
  208. null
  209. );
  210. }
  211. }
  212. },
  213. mounted() {
  214. this.showQW=true;
  215. const IMUI = this.$refs.IMUI;
  216. const conversationData = Conversations.filter(item => item.conversationId <= 1);
  217. conversationData.sort((a1, a2) => {
  218. return a2.lastSendTime - a1.lastSendTime;
  219. });
  220. IMUI.initMenus([
  221. {
  222. name: "messages",
  223. }
  224. ]);
  225. IMUI.initEmoji(EmojiData);
  226. IMUI.initEditorTools([
  227. { name: "emoji" },
  228. { name: "uploadImage" }
  229. ])
  230. },
  231. beforeDestroy() {
  232. if (this.imSocket) {
  233. this.imSocket.close();
  234. }
  235. },
  236. methods: {
  237. initImSocket(companyId) {
  238. this.imSocket = new ImSocket(`${this.imUrl}/${companyId}?token=${getToken()}`);
  239. this.imSocket.onMessage(data => {
  240. const { IMUI } = this.$refs;
  241. let message = JSON.parse(data);
  242. this.appendRemoteMessage(message)
  243. let conversation = IMUI.findConversation(message.toContactId);
  244. if (conversation.msgId) {
  245. conversation.lastSendTime = message.sendTime;
  246. conversation.lastContent = IMUI.lastContentRender(message)//message.content;
  247. IMUI.topPopConversations(conversation);
  248. }
  249. })
  250. },
  251. // 切换企微账号
  252. qwUserChange(tab, event){
  253. this.appKey = tab.name;
  254. let index= this.qwUserList.findIndex(item => item.appKey === tab.name);
  255. this.qwUser=this.qwUserList[index];
  256. this.getConversation(); //获取会话信息
  257. this.setQwUserInfo();
  258. },
  259. setQwUserInfo(){
  260. this.userData.id=this.qwUser.id;
  261. this.userData.displayName=this.qwUser.qwUserName;
  262. this.userData.avatar="https://cos.his.cdwjyyh.com/fs/20241231/22a765a96da247d1b83ea94fef438a41.png";
  263. },
  264. getConversation(){
  265. const IMUI = this.$refs.IMUI;
  266. getConversations(this.qwUser.id).then(response => {
  267. this.conversationData = response.data;
  268. IMUI.initConversations(response.data);
  269. const fstConversation = this.conversationData[0];
  270. if(fstConversation) {
  271. IMUI.changeContact(fstConversation.conversationId);
  272. }
  273. });
  274. },
  275. messageTimeFormat(time) {
  276. return this.friendlyDate(time);
  277. },
  278. openDrawer(position) {
  279. const IMUI = this.$refs.IMUI;
  280. const params = {
  281. width: '30%',
  282. position,
  283. render: contact => {
  284. return (
  285. <div style="padding:15px">
  286. <h5>{contact.displayName}</h5>
  287. </div>
  288. );
  289. },
  290. };
  291. IMUI.changeDrawer(params);
  292. },
  293. handlePullMessages(contact, next,instance) {
  294. const { IMUI } = this.$refs;
  295. let isEnd = false;
  296. getMessageList(this.queryParams).then(response => {
  297. if(response.code==200){
  298. isEnd=response.data.isLastPage;
  299. next(response.data.list, isEnd);
  300. if(!isEnd){
  301. pages[contact.conversationId]++;
  302. this.queryParams.pageNum=pages[contact.conversationId];
  303. this.queryParams.userId = this.qwUser.id
  304. }
  305. }
  306. });
  307. },
  308. handleChangeConversation(conversation, instance) {
  309. if (!pages[conversation.conversationId]){
  310. pages[conversation.conversationId] =1;
  311. }
  312. this.queryParams.pageNum=pages[conversation.conversationId];
  313. this.queryParams.conversationId=conversation.conversationId;
  314. this.queryParams.userId = this.qwUser.id
  315. if(conversation.unread>0){
  316. conversation.unread=0;
  317. instance.updateContact(conversation);
  318. }
  319. },
  320. handleChangeContact(contact, instance) {
  321. console.log("ChangeContact:", contact, instance)
  322. },
  323. //收到消息后添加消息显示
  324. appendMessageAction(msgData){
  325. if(msgData.type=="text" || msgData.type=="image" || msgData.type=="voice"){ //文本 text image video voice
  326. const message = {
  327. id: msgData.id,
  328. status: msgData.status,
  329. type: msgData.type,
  330. sendTime: msgData.sendTime,
  331. content: msgData.content,
  332. params1: "1",
  333. params2: "2",
  334. toContactId: msgData.toContactId,
  335. fromUser: msgData.fromUser,
  336. };
  337. this.appendRemoteMessage(message);
  338. }
  339. else if(msgData.type=="file"){
  340. const message = {
  341. id: msgData.id,
  342. status: msgData.status,
  343. type: msgData.type,
  344. sendTime: msgData.sendTime,
  345. content: msgData.content,
  346. toContactId: msgData.toContactId,
  347. fromUser: msgData.fromUser,
  348. fileName:msgData.fileName,
  349. fileSize:msgData.fileSize
  350. };
  351. this.appendRemoteMessage(message);
  352. }
  353. else{
  354. const message = {
  355. id: msgData.id,
  356. status: msgData.status,
  357. type: msgData.type,
  358. sendTime: msgData.sendTime,
  359. content: msgData.content,
  360. toContactId: msgData.toContactId,
  361. fromUser: msgData.fromUser,
  362. };
  363. this.appendRemoteMessage(message);
  364. }
  365. },
  366. //发送消息
  367. handleSend(message, next, file) {
  368. const IMUI = this.$refs.IMUI;
  369. let params = {};
  370. if(message.type === "text"){ //text image voice video
  371. params = {"sessionId":message.toContactId,"appKey":this.qwUser.appKey,"content":message.content, "msgType": 1};
  372. sendMsg(params).then(response => {
  373. const {code, data} = response
  374. if(code === 200){
  375. this.appendRemoteMessage(data)
  376. let conversation = IMUI.findConversation(message.toContactId);
  377. conversation.lastSendTime = message.sendTime;
  378. conversation.lastContent = IMUI.lastContentRender(message)
  379. IMUI.topPopConversations(conversation);
  380. next();
  381. } else {
  382. next({status:'failed'})
  383. }
  384. });
  385. }
  386. // image
  387. else if(message.type === "image"){
  388. const formData = new FormData();
  389. formData.append("file", file);
  390. uploadOss(formData).then(response => {
  391. const {code, url} = response
  392. if (code === 200) {
  393. params = {"sessionId":message.toContactId,"appKey":this.qwUser.appKey,"content":url, "msgType": 2};
  394. sendMsg(params).then(response => {
  395. const {code} = response
  396. if(code === 200){
  397. let conversation = IMUI.findConversation(message.toContactId);
  398. conversation.lastSendTime = message.sendTime;
  399. conversation.lastContent = IMUI.lastContentRender(message)
  400. IMUI.topPopConversations(conversation);
  401. next();
  402. } else {
  403. next({status:'failed'})
  404. }
  405. });
  406. }
  407. })
  408. }
  409. // 小程序
  410. else if(message.type === "miniprogram"){
  411. // 小程序消息参数
  412. params = {
  413. "sessionId": message.toContactId,
  414. "appKey": this.qwUser.appKey,
  415. "content": message.content,
  416. "msgType": 5 // 小程序消息类型
  417. };
  418. sendMsg(params).then(response => {
  419. const {code, data} = response
  420. if(code === 200){
  421. this.appendRemoteMessage(data)
  422. let conversation = IMUI.findConversation(message.toContactId);
  423. conversation.lastSendTime = message.sendTime;
  424. conversation.lastContent = IMUI.lastContentRender(message)
  425. IMUI.topPopConversations(conversation);
  426. }
  427. });
  428. }
  429. // file
  430. else if(message.type === "file"){
  431. console.log("Event:file-click", message, next, file)
  432. }
  433. },
  434. handleMenuAvatarClick() {
  435. console.log("Event:menu-avatar-click");
  436. },
  437. //聊天工具栏点击图片
  438. handleImageClick() {
  439. // this.$refs.material.openMaterial(this.qwUser.deviceId);
  440. console.log("Event:handleImageClick");
  441. },
  442. //选择图片框确定按钮回调
  443. handlePickImageDone(data){
  444. console.log("handlePickImageDone:"+JSON.stringify(data));
  445. this.pickUploadImgData=data;
  446. const IMUI = this.$refs.IMUI;
  447. IMUI._handleRemoteImage(data.url);
  448. },
  449. tooglePlayVideo(data){
  450. this.dialogVideoVisible=true;
  451. this.dialogVideoUrl=data.content;
  452. this.dialogVideoCover=data.url;
  453. const player = this.$refs.player.instance
  454. player && player.play()
  455. },
  456. handleMessageClick(e, key, message, instance) {
  457. if (key === 'avatar') {
  458. this.qwUser.id !== message.fromUser.id && this.showDetail(message.extId)
  459. return
  460. }
  461. if(message.type=="image"){
  462. var url=!!message.url?message.url:message.content;
  463. this.handlePicturePreview(url);
  464. }
  465. else if(message.type=="video"){
  466. this.tooglePlayVideo(message);
  467. }
  468. else if(message.type=="file"){
  469. }
  470. if (key == "status") {
  471. instance.updateMessage({
  472. id: message.id,
  473. status: "going",
  474. content: "正在重新发送消息...",
  475. });
  476. setTimeout(() => {
  477. instance.updateMessage({
  478. id: message.id,
  479. status: "succeed",
  480. content: "发送成功",
  481. });
  482. }, 2000);
  483. }
  484. },
  485. changeMenuAvatarVisible() {
  486. this.hideMenuAvatar = !this.hideMenuAvatar;
  487. },
  488. changeMenuVisible() {
  489. this.hideMenu = !this.hideMenu;
  490. },
  491. changeMessageNameVisible() {
  492. this.hideMessageName = !this.hideMessageName;
  493. },
  494. changeMessageTimeVisible() {
  495. this.hideMessageTime = !this.hideMessageTime;
  496. },
  497. removeMessage() {
  498. const { IMUI } = this.$refs;
  499. const messages = IMUI.getCurrentMessages();
  500. const id = messages[messages.length - 1].id;
  501. if (messages.length > 0) {
  502. IMUI.removeMessage(id);
  503. }
  504. },
  505. updateMessage() {
  506. const { IMUI } = this.$refs;
  507. const messages = IMUI.getCurrentMessages();
  508. const message = messages[messages.length - 1];
  509. if (messages.length > 0) {
  510. const update = {
  511. id: message.id,
  512. status: "succeed",
  513. type: "file",
  514. fileName: "被修改成文件了.txt",
  515. fileSize: "4200000",
  516. };
  517. if (message.type == "event") {
  518. update.fromUser = this.user;
  519. }
  520. IMUI.updateMessage(update);
  521. IMUI.messageViewToBottom();
  522. }
  523. },
  524. appendCustomMessage() {
  525. const { IMUI } = this.$refs;
  526. const message = {
  527. id: generateRandId(),
  528. status: "succeed",
  529. type: "voice",
  530. sendTime: getTime(),
  531. content: "语音消息",
  532. params1: "1",
  533. params2: "2",
  534. toContactId: "contact-1",
  535. fromUser: this.user,
  536. };
  537. IMUI.appendMessage(message, true);
  538. },
  539. appendMessage() {
  540. const { IMUI } = this.$refs;
  541. const contact = IMUI.currentContact;
  542. const message = generateMessage("contact-3");
  543. message.fromUser = {
  544. ...message.fromUser,
  545. ...this.user,
  546. };
  547. IMUI.appendMessage(message, true);
  548. console.log("🚀 ~ file: App.vue ~ line 1508 ~ appendMessage ~ message", message)
  549. },
  550. appendRemoteMessage(message) { //从服务端返回的消息
  551. const { IMUI } = this.$refs;
  552. IMUI.appendMessage(message, true);
  553. },
  554. handleChangeMenu() {
  555. console.log("Event:change-menu");
  556. },
  557. handlePicturePreview(url) {
  558. this.dialogImageUrl = url;
  559. this.dialogVisible = true;
  560. },
  561. // 详情
  562. showDetail(extId) {
  563. this.detail.open = true
  564. setTimeout(() => {
  565. this.$refs.userDetail.getDetail(extId);
  566. }, 1);
  567. },
  568. // 搜索
  569. handleSearch() {
  570. const keyword = this.searchKeyword.trim().toLowerCase();
  571. if (!keyword) {
  572. // 为空时显示全部
  573. this.$refs.IMUI.initConversations(this.conversationData);
  574. return;
  575. }
  576. // 过滤会话列表
  577. const filtered = this.conversationData.filter(item => {
  578. // 搜索最后一条消息内容和联系人名
  579. return (
  580. (item.lastContent && item.lastContent.toLowerCase().includes(keyword)) ||
  581. (item.displayName && item.displayName.toLowerCase().includes(keyword))
  582. );
  583. });
  584. this.$refs.IMUI.initConversations(filtered);
  585. },
  586. },
  587. };
  588. </script>
  589. <style lang="scss" scoped>
  590. .imui-center{
  591. align-items: center;
  592. justify-content: center;
  593. }
  594. .lemon-wrapper{
  595. border:1px solid #ddd;
  596. //height: !important;
  597. }
  598. .lemon-drawer{
  599. border:1px solid #ddd;
  600. border-left:0;
  601. }
  602. .more {
  603. font-size: 12px;
  604. line-height: 24px;
  605. height: 24px;
  606. position: absolute;
  607. top: 14px;
  608. right: 14px;
  609. cursor: pointer;
  610. -webkit-user-select: none;
  611. -moz-user-select: none;
  612. -ms-user-select: none;
  613. user-select: none;
  614. color: #f1f1f1;
  615. display: inline-block;
  616. border-radius: 4px;
  617. background: #111;
  618. padding: 0 8px;
  619. }
  620. /deep/.el-dialog__headerbtn{
  621. top:10px;
  622. }
  623. /deep/.el-dialog:not(.is-fullscreen) {
  624. margin-top: 0 !important;
  625. }
  626. ::v-deep .im-tabs {
  627. .el-tabs__header {
  628. margin: 0 !important;
  629. .el-tabs__item {
  630. border-bottom: 0 !important;
  631. }
  632. }
  633. }
  634. </style>
  635. <style lang="stylus">
  636. .lemon-container__title{
  637. padding-bottom:10px;
  638. }
  639. .slot-group{
  640. width:200px;
  641. }
  642. .slot-group-title{
  643. padding:0 0 10px 0;
  644. }
  645. .slot-group-notice{
  646. padding: 10px 10px;
  647. }
  648. .slot-search{
  649. width:calc(100% - 20px) ;
  650. margin:5px 10px;
  651. padding:3px 0px;
  652. }
  653. .slot-group-member{
  654. display: flex;
  655. padding: 5px 0;
  656. flex:1;
  657. font-size: 14px;
  658. align-items: center;
  659. }
  660. .avatar{
  661. width: 30px;
  662. height: 30px;
  663. line-height: 30px;
  664. margin-right:5px;
  665. }
  666. .img{
  667. vertical-align: middle;
  668. border-style: none;
  669. width: 100%;
  670. height: 100%;
  671. line-height: 30px;
  672. border-radius: 50%;
  673. }
  674. </style>