qq.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. <template>
  2. <div style="width: 100%;height: 100%">
  3. <el-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="windowWidth"
  13. :height="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 #cover>
  32. <div>
  33. </div>
  34. </template>
  35. <template #message-title="contact">
  36. <div>
  37. </div>
  38. </template>
  39. <template #sidebar-contact-fixedtop="contact">
  40. <div>
  41. </div>
  42. </template>
  43. <template #sidebar-message-fixedtop="message">
  44. <div>
  45. </div>
  46. </template>
  47. <template #message-side="contact">
  48. <div>
  49. </div>
  50. </template>
  51. </lemon-imui>
  52. </div>
  53. <el-dialog :visible.sync="dialogVisible" append-to-body width="35%">
  54. <img style="width:100%;height:auto" :src="dialogImageUrl" alt="" />
  55. </el-dialog>
  56. <el-dialog :visible.sync="dialogVideoVisible"
  57. :close-on-click-modal="false"
  58. :destroy-on-close="true"
  59. :width="aplayer.pWidth"
  60. :height="aplayer.height"
  61. @close="dialogVideoVisible=false"
  62. ref="player">
  63. <videoPlayer :width="aplayer.width" :height="aplayer.height" :videoWidth="aplayer.videoWidth" :videoHeight="aplayer.videoHeight" :vid="aplayer.vid" :autoplay="true"
  64. :source="dialogVideoUrl" :cover="dialogVideoCover" ref="player"></videoPlayer>
  65. </el-dialog>
  66. <!-- 用户详情 -->
  67. <el-drawer
  68. append-to-body
  69. :with-header="false"
  70. size="75%"
  71. :title="detail.title" :visible.sync="detail.open">
  72. <userDetail ref="userDetail" />
  73. </el-drawer>
  74. </div>
  75. </template>
  76. <script>
  77. import {getConversations,getMessageList,getConversation,sendMsg,getQwUserList} from '@/api/qw/im';
  78. import Conversations from "@/components/LemonUI/database/conversations";
  79. import EmojiData from "@/components/LemonUI/database/emoji";
  80. import '@/components/LemonUI/index.css';
  81. import VideoPlayer from '@/components/VideoPlayer/VueAliplayer.vue'
  82. import UserDetail from "@/views/qw/qwChat/userDetail/index.vue";
  83. let pages = {};
  84. export default {
  85. name: "qqChat",
  86. components: {
  87. VideoPlayer,
  88. UserDetail
  89. },
  90. props: {
  91. showQw: Boolean
  92. },
  93. data() {
  94. return {
  95. contactName:'',
  96. appKey:null,
  97. qwUserId:null,
  98. qwUserList:[],
  99. theme: "default",
  100. IMUI:null,
  101. contactContextmenu: [],
  102. hideMenuAvatar: false,
  103. hideMenu: false,
  104. hideMessageName: false,
  105. hideMessageTime: true,
  106. qwUser:{},
  107. showQW:false,
  108. userData: {},
  109. contactData:null,
  110. conversationData:Conversations,
  111. dialogImageUrl: '',
  112. dialogVisible: false,
  113. dialogVideoVisible:false,
  114. dialogVideoUrl:'',
  115. dialogVideoCover:'',
  116. imageArr:[],
  117. pickUploadImgData:null,
  118. aplayer: {
  119. vid: "bf9b7e4a36d84aea8cee769765fbc28b",
  120. pWidth:"1040px",
  121. width:"1000px",
  122. height:"900px",
  123. videoWidth:"1000px",
  124. videoHeight:"900px"
  125. },
  126. player: null,
  127. roomMembers:[],
  128. roomAdmins:[],
  129. roomInfo:null,
  130. windowWidth: document.documentElement.clientWidth*0.6, //实时屏幕宽度
  131. windowHeight: document.documentElement.clientHeight*0.7, //实时屏幕高度
  132. queryParams: {
  133. pageNum: 1,
  134. pageSize: 10,
  135. conversationId: null,
  136. userId:null
  137. },
  138. detail: {
  139. title: '',
  140. open: false
  141. },
  142. messagePollingTimer: null, // 定时器引用
  143. pollingInterval: 5000, // 轮询间隔(毫秒)
  144. lastMessageIdMap: {}, // 记录每个会话最后一条消息ID
  145. };
  146. },
  147. created(){
  148. //获取企微列表
  149. getQwUserList().then(response => {
  150. this.qwUserList = response.data;
  151. if (this.qwUserList.length>0){
  152. this.qwUser = this.qwUserList[0];
  153. this.appKey = this.qwUser.appKey;
  154. this.setQwUserInfo();
  155. this.getConversation();
  156. }else {
  157. this.qwUser={};
  158. }
  159. });
  160. },
  161. watch: {
  162. showQw(nv, ov) {
  163. if (nv) {
  164. this.$nextTick(() => {
  165. this.$refs.IMUI.messageViewToBottom();
  166. });
  167. this.startMessagePolling()
  168. }
  169. else {
  170. this.stopMessagePolling();
  171. }
  172. }
  173. },
  174. mounted() {
  175. this.$watch('appKey', (newValue, oldValue) => {
  176. if (newValue) {
  177. console.log("appkey====",newValue);
  178. }
  179. });
  180. this.showQW=true;
  181. const IMUI = this.$refs.IMUI;
  182. const conversationData = Conversations.filter(item => item.conversationId <= 1);
  183. conversationData.sort((a1, a2) => {
  184. return a2.lastSendTime - a1.lastSendTime;
  185. });
  186. IMUI.initMenus([
  187. {
  188. name: "messages",
  189. }
  190. ]);
  191. IMUI.initEmoji(EmojiData);
  192. this.startMessagePolling(); // 启动定时轮询
  193. },
  194. beforeDestroy() {
  195. this.stopMessagePolling();
  196. },
  197. methods: {
  198. // 切换企微账号
  199. qwUserChange(tab, event){
  200. this.appKey = tab.name;
  201. let index= this.qwUserList.findIndex(item => item.appKey === tab.name);
  202. this.qwUser=this.qwUserList[index];
  203. this.getConversation(); //获取会话信息
  204. this.setQwUserInfo();
  205. },
  206. setQwUserInfo(){
  207. this.userData.id=this.qwUser.id;
  208. this.userData.displayName=this.qwUser.qwUserName;
  209. this.userData.avatar="https://cos.his.cdwjyyh.com/fs/20241231/22a765a96da247d1b83ea94fef438a41.png";
  210. },
  211. getConversation(){
  212. const IMUI = this.$refs.IMUI;
  213. getConversations(this.qwUser.id).then(response => {
  214. this.conversationData = response.data;
  215. // 初始化msgId
  216. if (Array.isArray(response.data)) {
  217. response.data.forEach(conversation => {
  218. this.lastMessageIdMap[conversation.conversationId] = conversation.msgId || 0;
  219. });
  220. }
  221. IMUI.initConversations(response.data);
  222. const fstConversation = this.conversationData[0];
  223. if(fstConversation) {
  224. IMUI.changeContact(fstConversation.conversationId);
  225. }
  226. });
  227. },
  228. messageTimeFormat(time) {
  229. return this.friendlyDate(time);
  230. },
  231. openDrawer(position) {
  232. const IMUI = this.$refs.IMUI;
  233. const params = {
  234. position,
  235. render: contact => {
  236. return (
  237. <div style="padding:15px">
  238. <h5>{contact.displayName}</h5>
  239. <span style="cursor:pointer;" on-click={IMUI.closeDrawer}>关闭侧边栏</span>
  240. </div>
  241. );
  242. },
  243. };
  244. IMUI.openDrawer(params);
  245. },
  246. handlePullMessages(contact, next,instance) {
  247. const { IMUI } = this.$refs;
  248. let isEnd = false;
  249. getMessageList(this.queryParams).then(response => {
  250. if(response.code==200){
  251. isEnd=response.data.isLastPage;
  252. next(response.data.list, isEnd);
  253. if(!isEnd){
  254. pages[contact.conversationId]++;
  255. this.queryParams.pageNum=pages[contact.conversationId];
  256. this.queryParams.userId = this.qwUser.id
  257. }
  258. }
  259. });
  260. },
  261. handleChangeConversation(conversation, instance) {
  262. if (!pages[conversation.conversationId]){
  263. pages[conversation.conversationId] =1;
  264. }
  265. this.queryParams.pageNum=pages[conversation.conversationId];
  266. this.queryParams.conversationId=conversation.conversationId;
  267. this.queryParams.userId = this.qwUser.id
  268. if(conversation.unread>0){
  269. conversation.unread=0;
  270. instance.updateContact(conversation);
  271. }
  272. },
  273. handleChangeContact(contact, instance) {
  274. console.log("ChangeContact:", contact, instance)
  275. },
  276. //收到消息后添加消息显示
  277. appendMessageAction(msgData){
  278. if(msgData.type=="text" || msgData.type=="image" || msgData.type=="voice"){ //文本 text image video voice
  279. const message = {
  280. id: msgData.id,
  281. status: msgData.status,
  282. type: msgData.type,
  283. sendTime: msgData.sendTime,
  284. content: msgData.content,
  285. params1: "1",
  286. params2: "2",
  287. toContactId: msgData.toContactId,
  288. fromUser: msgData.fromUser,
  289. };
  290. this.appendRemoteMessage(message);
  291. }
  292. else if(msgData.type=="file"){
  293. const message = {
  294. id: msgData.id,
  295. status: msgData.status,
  296. type: msgData.type,
  297. sendTime: msgData.sendTime,
  298. content: msgData.content,
  299. toContactId: msgData.toContactId,
  300. fromUser: msgData.fromUser,
  301. fileName:msgData.fileName,
  302. fileSize:msgData.fileSize
  303. };
  304. this.appendRemoteMessage(message);
  305. }
  306. else{
  307. const message = {
  308. id: msgData.id,
  309. status: msgData.status,
  310. type: msgData.type,
  311. sendTime: msgData.sendTime,
  312. content: msgData.content,
  313. toContactId: msgData.toContactId,
  314. fromUser: msgData.fromUser,
  315. };
  316. this.appendRemoteMessage(message);
  317. }
  318. },
  319. //发送消息
  320. handleSend(message, next, file) {
  321. const IMUI = this.$refs.IMUI;
  322. let params = {};
  323. if(message.type === "text"){ //text image voice video
  324. params = {"sessionId":message.toContactId,"appKey":this.qwUser.appKey,"content":message.content};
  325. sendMsg(params).then(response => {
  326. const {code, data} = response
  327. if(code === 200){
  328. this.appendRemoteMessage(data)
  329. let conversation = IMUI.findConversation(message.toContactId);
  330. conversation.lastSendTime = message.sendTime;
  331. conversation.lastContent = message.content;
  332. IMUI.topPopConversations(conversation);
  333. next();
  334. }
  335. });
  336. }
  337. },
  338. handleMenuAvatarClick() {
  339. console.log("Event:menu-avatar-click");
  340. },
  341. //聊天工具栏点击图片
  342. handleImageClick() {
  343. // this.$refs.material.openMaterial(this.qwUser.deviceId);
  344. console.log("Event:handleImageClick");
  345. },
  346. //选择图片框确定按钮回调
  347. handlePickImageDone(data){
  348. console.log("handlePickImageDone:"+JSON.stringify(data));
  349. this.pickUploadImgData=data;
  350. const IMUI = this.$refs.IMUI;
  351. IMUI._handleRemoteImage(data.url);
  352. },
  353. tooglePlayVideo(data){
  354. this.dialogVideoVisible=true;
  355. this.dialogVideoUrl=data.content;
  356. this.dialogVideoCover=data.url;
  357. const player = this.$refs.player.instance
  358. player && player.play()
  359. },
  360. handleMessageClick(e, key, message, instance) {
  361. if (key === 'avatar') {
  362. this.qwUser.id !== message.fromUser.id && this.showDetail(message.toContactId)
  363. return
  364. }
  365. if(message.type=="image"){
  366. var url=!!message.url?message.url:message.content;
  367. this.handlePicturePreview(url);
  368. }
  369. else if(message.type=="video"){
  370. this.tooglePlayVideo(message);
  371. }
  372. else if(message.type=="file"){
  373. }
  374. if (key == "status") {
  375. instance.updateMessage({
  376. id: message.id,
  377. status: "going",
  378. content: "正在重新发送消息...",
  379. });
  380. setTimeout(() => {
  381. instance.updateMessage({
  382. id: message.id,
  383. status: "succeed",
  384. content: "发送成功",
  385. });
  386. }, 2000);
  387. }
  388. },
  389. changeMenuAvatarVisible() {
  390. this.hideMenuAvatar = !this.hideMenuAvatar;
  391. },
  392. changeMenuVisible() {
  393. this.hideMenu = !this.hideMenu;
  394. },
  395. changeMessageNameVisible() {
  396. this.hideMessageName = !this.hideMessageName;
  397. },
  398. changeMessageTimeVisible() {
  399. this.hideMessageTime = !this.hideMessageTime;
  400. },
  401. removeMessage() {
  402. const { IMUI } = this.$refs;
  403. const messages = IMUI.getCurrentMessages();
  404. const id = messages[messages.length - 1].id;
  405. if (messages.length > 0) {
  406. IMUI.removeMessage(id);
  407. }
  408. },
  409. updateMessage() {
  410. const { IMUI } = this.$refs;
  411. const messages = IMUI.getCurrentMessages();
  412. const message = messages[messages.length - 1];
  413. if (messages.length > 0) {
  414. const update = {
  415. id: message.id,
  416. status: "succeed",
  417. type: "file",
  418. fileName: "被修改成文件了.txt",
  419. fileSize: "4200000",
  420. };
  421. if (message.type == "event") {
  422. update.fromUser = this.user;
  423. }
  424. IMUI.updateMessage(update);
  425. IMUI.messageViewToBottom();
  426. }
  427. },
  428. appendCustomMessage() {
  429. const { IMUI } = this.$refs;
  430. const message = {
  431. id: generateRandId(),
  432. status: "succeed",
  433. type: "voice",
  434. sendTime: getTime(),
  435. content: "语音消息",
  436. params1: "1",
  437. params2: "2",
  438. toContactId: "contact-1",
  439. fromUser: this.user,
  440. };
  441. IMUI.appendMessage(message, true);
  442. },
  443. appendMessage() {
  444. const { IMUI } = this.$refs;
  445. const contact = IMUI.currentContact;
  446. const message = generateMessage("contact-3");
  447. message.fromUser = {
  448. ...message.fromUser,
  449. ...this.user,
  450. };
  451. IMUI.appendMessage(message, true);
  452. console.log("🚀 ~ file: App.vue ~ line 1508 ~ appendMessage ~ message", message)
  453. },
  454. appendRemoteMessage(message) { //从服务端返回的消息
  455. const { IMUI } = this.$refs;
  456. IMUI.appendMessage(message, true);
  457. },
  458. handleChangeMenu() {
  459. console.log("Event:change-menu");
  460. },
  461. handlePicturePreview(url) {
  462. this.dialogImageUrl = url;
  463. this.dialogVisible = true;
  464. },
  465. // 详情
  466. showDetail(sessionId) {
  467. this.detail.open = true
  468. setTimeout(() => {
  469. this.$refs.userDetail.getDetail(sessionId);
  470. }, 1);
  471. },
  472. startMessagePolling() {
  473. this.stopMessagePolling(); // 避免重复启动
  474. this.messagePollingTimer = setInterval(() => {
  475. this.fetchAllConversationsLatestMessages();
  476. }, this.pollingInterval);
  477. },
  478. stopMessagePolling() {
  479. if (this.messagePollingTimer) {
  480. clearInterval(this.messagePollingTimer);
  481. this.messagePollingTimer = null;
  482. }
  483. },
  484. fetchAllConversationsLatestMessages() {
  485. if (!this.qwUser || !this.conversationData) return;
  486. this.conversationData.forEach(conversation => {
  487. const lastMsgId = this.lastMessageIdMap[conversation.conversationId] || 0;
  488. const params = {
  489. pageNum: 1,
  490. pageSize: 20,
  491. conversationId: conversation.conversationId,
  492. userId: this.qwUser.id,
  493. msgId: lastMsgId // 用消息ID拉取新消息
  494. };
  495. getMessageList(params).then(response => {
  496. if (response.code === 200 && response.data && response.data.list) {
  497. response.data.list.forEach(msg => {
  498. // 更新lastMessageIdMap
  499. if (!this.lastMessageIdMap[conversation.conversationId] || msg.id > this.lastMessageIdMap[conversation.conversationId]) {
  500. this.lastMessageIdMap[conversation.conversationId] = msg.id;
  501. }
  502. this.appendMessageAction(msg);
  503. });
  504. }
  505. });
  506. });
  507. },
  508. },
  509. };
  510. </script>
  511. <style lang="scss" scoped>
  512. .imui-center{
  513. align-items: center;
  514. justify-content: center;
  515. }
  516. .lemon-wrapper{
  517. border:1px solid #ddd;
  518. //height: !important;
  519. }
  520. .lemon-drawer{
  521. border:1px solid #ddd;
  522. border-left:0;
  523. }
  524. .more {
  525. font-size: 12px;
  526. line-height: 24px;
  527. height: 24px;
  528. position: absolute;
  529. top: 14px;
  530. right: 14px;
  531. cursor: pointer;
  532. -webkit-user-select: none;
  533. -moz-user-select: none;
  534. -ms-user-select: none;
  535. user-select: none;
  536. color: #f1f1f1;
  537. display: inline-block;
  538. border-radius: 4px;
  539. background: #111;
  540. padding: 0 8px;
  541. }
  542. /deep/.el-dialog__headerbtn{
  543. top:10px;
  544. }
  545. /deep/.el-dialog:not(.is-fullscreen) {
  546. margin-top: 0 !important;
  547. }
  548. </style>
  549. <style lang="stylus">
  550. .lemon-container__title{
  551. padding-bottom:10px;
  552. }
  553. .slot-group{
  554. width:200px;
  555. }
  556. .slot-group-title{
  557. padding:0 0 10px 0;
  558. }
  559. .slot-group-notice{
  560. padding: 10px 10px;
  561. }
  562. .slot-search{
  563. width:calc(100% - 20px) ;
  564. margin:5px 10px;
  565. padding:3px 0px;
  566. }
  567. .slot-group-member{
  568. display: flex;
  569. padding: 5px 0;
  570. flex:1;
  571. font-size: 14px;
  572. align-items: center;
  573. }
  574. .avatar{
  575. width: 30px;
  576. height: 30px;
  577. line-height: 30px;
  578. margin-right:5px;
  579. }
  580. .img{
  581. vertical-align: middle;
  582. border-style: none;
  583. width: 100%;
  584. height: 100%;
  585. line-height: 30px;
  586. border-radius: 50%;
  587. }
  588. </style>