index.vue 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282
  1. <script>
  2. import {
  3. useScopedSlot,
  4. funCall,
  5. generateUUID,
  6. clearHtmlExcludeImg,
  7. } from "utils";
  8. import { isFunction, isString, isEmpty } from "utils/validate";
  9. import contextmenu from "../directives/contextmenu";
  10. import {
  11. DEFAULT_MENUS,
  12. DEFAULT_MENU_LASTMESSAGES,
  13. DEFAULT_MENU_CONTACTS,
  14. } from "utils/constant";
  15. import lastContentRender from "../lastContentRender";
  16. import MemoryCache from "utils/cache/memory";
  17. let allMessages = {};
  18. const emojiMap = {};
  19. const toPx = val => {
  20. return isString(val) ? val : `${val}px`;
  21. };
  22. const toPoint = str => {
  23. return str.replace("%", "") / 100;
  24. };
  25. let renderDrawerContent = () => {};
  26. export default {
  27. name: "LemonImui",
  28. provide() {
  29. return {
  30. IMUI: this,
  31. };
  32. },
  33. props: {
  34. width: {
  35. type: [String, Number],
  36. default: 850,
  37. },
  38. height: {
  39. type: [String, Number],
  40. default: 580,
  41. },
  42. theme: {
  43. type: String,
  44. default: "default",
  45. },
  46. simple: {
  47. type: Boolean,
  48. default: false,
  49. },
  50. loadingText: [String, Function],
  51. loadendText: [String, Function],
  52. /**
  53. * 消息时间格式化规则
  54. */
  55. messageTimeFormat: Function,
  56. /**
  57. * 联系人最新消息时间格式化规则
  58. */
  59. contactTimeFormat: Function,
  60. /**
  61. * 初始化时是否隐藏抽屉
  62. */
  63. hideDrawer: {
  64. type: Boolean,
  65. default: true,
  66. },
  67. /**
  68. * 是否隐藏导航按钮上的头像
  69. */
  70. hideMenuAvatar: Boolean,
  71. hideMenu: Boolean,
  72. /**
  73. * 是否隐藏消息列表内的联系人名字
  74. */
  75. hideMessageName: Boolean,
  76. /**
  77. * 是否隐藏消息列表内的发送时间
  78. */
  79. hideMessageTime: Boolean,
  80. sendKey: Function,
  81. wrapKey: Function,
  82. sendText: String,
  83. contextmenu: Array,
  84. contactContextmenu: Array,
  85. avatarCricle: Boolean,
  86. user: {
  87. type: Object,
  88. default: () => {
  89. return {};
  90. },
  91. },
  92. latelyContacts: {
  93. type: Function,
  94. default(list){
  95. const data = list.filter(item => !isEmpty(item.lastContent));
  96. data.sort((a1, a2) => {
  97. return a2.lastSendTime - a1.lastSendTime;
  98. });
  99. return data;
  100. },
  101. },
  102. },
  103. data() {
  104. this.CacheContactContainer = new MemoryCache();
  105. this.CacheMenuContainer = new MemoryCache();
  106. this.CacheMessageLoaded = new MemoryCache();
  107. this.CacheDraft = new MemoryCache();
  108. return {
  109. drawerVisible: !this.hideDrawer,
  110. currentContactId: null, // 消息菜单下当前联系人
  111. currentContactIdSidebarContact: null, // 联系人菜单下当前联系人
  112. currentMessages: [],
  113. activeSidebar: DEFAULT_MENU_LASTMESSAGES,
  114. contacts: [],
  115. menus: [],
  116. editorTools: [
  117. { name: "emoji" },
  118. { name: "uploadFile" },
  119. { name: "uploadImage" },
  120. ],
  121. };
  122. },
  123. render() {
  124. return this._renderWrapper([
  125. this._renderMenu(),
  126. this._renderSidebarMessage(),
  127. this._renderSidebarContact(),
  128. this._renderContainer(),
  129. this._renderDrawer(),
  130. ]);
  131. },
  132. created() {
  133. this.initMenus();
  134. },
  135. async mounted() {
  136. await this.$nextTick();
  137. },
  138. computed: {
  139. currentContact() {
  140. return this.contacts.find(item => item.id == this.currentContactId) || {};
  141. },
  142. currentContactSidebarContact() {
  143. return this.contacts.find(item => item.id == this.currentContactIdSidebarContact) || {};
  144. },
  145. currentMenu() {
  146. return this.menus.find(item => item.name == this.activeSidebar) || {};
  147. },
  148. currentIsDefSidebar() {
  149. return DEFAULT_MENUS.includes(this.activeSidebar);
  150. },
  151. lastMessages() {
  152. return this.latelyContacts(this.contacts);
  153. },
  154. },
  155. watch: {
  156. activeSidebar() {},
  157. },
  158. methods: {
  159. _menuIsContacts() {
  160. return this.activeSidebar == DEFAULT_MENU_CONTACTS;
  161. },
  162. _menuIsMessages() {
  163. return this.activeSidebar == DEFAULT_MENU_LASTMESSAGES;
  164. },
  165. _createMessage(message) {
  166. return {
  167. ...{
  168. id: generateUUID(),
  169. type: "text",
  170. status: "going",
  171. sendTime: new Date().getTime(),
  172. toContactId: this.currentContactId,
  173. fromUser: {
  174. ...this.user,
  175. },
  176. },
  177. ...message,
  178. };
  179. },
  180. /**
  181. * 新增一条消息
  182. */
  183. appendMessage(message, scrollToBottom = false) {
  184. let unread = "+1";
  185. let messageList = allMessages[message.toContactId];
  186. // 如果是自己的消息需要push,发送的消息不再增加未读条数
  187. if (message.type == 'event' || this.user.id == message.fromUser.id) unread = "+0";
  188. if (messageList === undefined) {
  189. this.updateContact({
  190. id: message.toContactId,
  191. unread: unread,
  192. lastSendTime: message.sendTime,
  193. lastContent: this.lastContentRender(message),
  194. });
  195. } else {
  196. // 如果消息存在则不再添加
  197. let hasMsg = messageList.some(({id})=>id == message.id);
  198. if (hasMsg) return;
  199. this._addMessage(message, message.toContactId, 1);
  200. const updateContact = {
  201. id: message.toContactId,
  202. lastContent: this.lastContentRender(message),
  203. lastSendTime: message.sendTime,
  204. };
  205. if (message.toContactId == this.currentContactId) {
  206. if (scrollToBottom == true) {
  207. this.messageViewToBottom();
  208. }
  209. this.CacheDraft.remove(message.toContactId);
  210. } else {
  211. updateContact.unread = unread;
  212. }
  213. this.updateContact(updateContact);
  214. }
  215. },
  216. _emitSend(message, next, file) {
  217. this.$emit(
  218. "send",
  219. message,
  220. (replaceMessage = { status: "succeed" }) => {
  221. next();
  222. this.updateMessage(Object.assign(message, replaceMessage));
  223. },
  224. file,
  225. );
  226. },
  227. _handleSend(text) {
  228. const atUserList=this.$refs.editor.chatArea.getCallUserList();
  229. // 将数组中的id提取出来
  230. const atUserIds=atUserList.map(item=>item.id);
  231. const message = this._createMessage({ content: text,at:atUserIds});
  232. this.appendMessage(message, true);
  233. this._emitSend(message, () => {
  234. this.updateContact({
  235. id: message.toContactId,
  236. lastContent: this.lastContentRender(message),
  237. lastSendTime: message.sendTime,
  238. });
  239. this.CacheDraft.remove(message.toContactId);
  240. });
  241. },
  242. _handleUpload(file) {
  243. const imageTypes = ["image/gif", "image/jpeg", "image/png"];
  244. let joinMessage;
  245. if (imageTypes.includes(file.type)) {
  246. joinMessage = {
  247. type: "image",
  248. content: URL.createObjectURL(file),
  249. };
  250. } else {
  251. joinMessage = {
  252. type: "file",
  253. fileSize: file.size,
  254. fileName: file.name,
  255. content: "",
  256. };
  257. }
  258. const message = this._createMessage(joinMessage);
  259. this.appendMessage(message, true);
  260. this._emitSend(
  261. message,
  262. () => {
  263. this.updateContact({
  264. id: message.toContactId,
  265. lastContent: this.lastContentRender(message),
  266. lastSendTime: message.sendTime,
  267. });
  268. },
  269. file,
  270. );
  271. },
  272. _emitPullMessages(next) {
  273. this._changeContactLock = true;
  274. this.$emit(
  275. "pull-messages",
  276. this.currentContact,
  277. (messages = [], isEnd = false) => {
  278. this._addMessage(messages, this.currentContactId, 0);
  279. this.CacheMessageLoaded.set(this.currentContactId, isEnd);
  280. if (isEnd == true) this.$refs.messages.loaded();
  281. this.updateCurrentMessages();
  282. this._changeContactLock = false;
  283. next(isEnd);
  284. },
  285. this,
  286. );
  287. },
  288. clearCacheContainer(name) {
  289. this.CacheContactContainer.remove(name);
  290. this.CacheMenuContainer.remove(name);
  291. },
  292. _renderWrapper(children) {
  293. return (
  294. <div
  295. style={{
  296. width: toPx(this.width),
  297. height: toPx(this.height),
  298. }}
  299. ref="wrapper"
  300. class={[
  301. "lemon-wrapper",
  302. `lemon-wrapper--theme-${this.theme}`,
  303. { "lemon-wrapper--simple": this.simple },
  304. this.drawerVisible && "lemon-wrapper--drawer-show",
  305. ]}
  306. >
  307. {children}
  308. </div>
  309. );
  310. },
  311. _renderMenu() {
  312. const menuItem = this._renderMenuItem();
  313. return (
  314. <div class="lemon-menu" v-show={!this.hideMenu}>
  315. {
  316. <lemon-avatar
  317. v-show={!this.hideMenuAvatar}
  318. on-click={e => {
  319. this.$emit("menu-avatar-click", e);
  320. }}
  321. class="lemon-menu__avatar"
  322. src={this.user.avatar}
  323. />
  324. }
  325. {menuItem.top}
  326. {this.$slots.menu}
  327. <div class="lemon-menu__bottom">
  328. {this.$slots["menu-bottom"]}
  329. {menuItem.bottom}
  330. </div>
  331. </div>
  332. );
  333. },
  334. _renderMenuAvatar() {
  335. return;
  336. },
  337. _renderMenuItem() {
  338. const top = [];
  339. const bottom = [];
  340. this.menus.forEach(item => {
  341. const { name, title, unread, render, click } = item;
  342. const node = (
  343. <div
  344. class={[
  345. "lemon-menu__item",
  346. { "lemon-menu__item--active": this.activeSidebar == name },
  347. ]}
  348. on-click={() => {
  349. funCall(click, () => {
  350. if (name) this.changeMenu(name);
  351. });
  352. }}
  353. title={title}
  354. >
  355. <lemon-badge count={unread}>{render(item)}</lemon-badge>
  356. </div>
  357. );
  358. item.isBottom === true ? bottom.push(node) : top.push(node);
  359. });
  360. return {
  361. top,
  362. bottom,
  363. };
  364. },
  365. _renderSidebarMessage() {
  366. return this._renderSidebar(
  367. [
  368. useScopedSlot(this.$scopedSlots["sidebar-message-top"], null, this),
  369. this.lastMessages.map(contact => {
  370. return this._renderContact(
  371. {
  372. contact,
  373. timeFormat: this.contactTimeFormat,
  374. },
  375. () => this.changeContact(contact.id),
  376. this.$scopedSlots["sidebar-message"],
  377. );
  378. }),
  379. ],
  380. DEFAULT_MENU_LASTMESSAGES,
  381. useScopedSlot(
  382. this.$scopedSlots["sidebar-message-fixedtop"],
  383. null,
  384. this,
  385. ),
  386. );
  387. },
  388. _renderContact(props, onClick, slot) {
  389. const {
  390. click: customClick,
  391. renderContainer,
  392. id: contactId,
  393. } = props.contact;
  394. const click = () => {
  395. funCall(customClick, () => {
  396. onClick();
  397. this._customContainerReady(
  398. renderContainer,
  399. this.CacheContactContainer,
  400. contactId,
  401. );
  402. });
  403. };
  404. return (
  405. <lemon-contact
  406. class={{
  407. "lemon-contact--active": this.activeSidebar == DEFAULT_MENU_CONTACTS ? this.currentContactIdSidebarContact == props.contact.id : this.currentContactId == props.contact.id
  408. }}
  409. v-lemon-contextmenu_contact={this.contactContextmenu}
  410. props={props}
  411. on-click={click}
  412. scopedSlots={{ default: slot }}
  413. />
  414. );
  415. },
  416. _renderSidebarContact() {
  417. let prevIndex;
  418. return this._renderSidebar(
  419. [
  420. useScopedSlot(this.$scopedSlots["sidebar-contact-top"], null, this),
  421. this.contacts.map(contact => {
  422. if (!contact.index) return;
  423. contact.index = contact.index.replace(/\[[0-9]*\]/, "");
  424. const node = [
  425. contact.index !== prevIndex && (
  426. <p class="lemon-sidebar__label">{contact.index}</p>
  427. ),
  428. this._renderContact(
  429. {
  430. contact: contact,
  431. simple: true,
  432. },
  433. () => {
  434. this.changeContact(contact.id);
  435. },
  436. this.$scopedSlots["sidebar-contact"],
  437. ),
  438. ];
  439. prevIndex = contact.index;
  440. return node;
  441. }),
  442. ],
  443. DEFAULT_MENU_CONTACTS,
  444. useScopedSlot(
  445. this.$scopedSlots["sidebar-contact-fixedtop"],
  446. null,
  447. this,
  448. ),
  449. );
  450. },
  451. _renderSidebar(children, name, fixedtop) {
  452. return (
  453. <div
  454. class="lemon-sidebar"
  455. v-show={this.activeSidebar == name}
  456. on-scroll={this._handleSidebarScroll}
  457. >
  458. <div class="lemon-sidebar__fixed-top">{fixedtop}</div>
  459. <div class="lemon-sidebar__scroll">{children}</div>
  460. </div>
  461. );
  462. },
  463. _renderDrawer() {
  464. return this._menuIsMessages() && this.currentContactId ? (
  465. <div class="lemon-drawer" ref="drawer">
  466. {renderDrawerContent(this.currentContact)}
  467. {useScopedSlot(this.$scopedSlots.drawer, "", this.currentContact)}
  468. </div>
  469. ) : (
  470. ""
  471. );
  472. },
  473. _isContactContainerCache(name) {
  474. return name.startsWith("contact#");
  475. },
  476. _renderContainer() {
  477. const nodes = [];
  478. const cls = "lemon-container";
  479. const curact = this.activeSidebar == DEFAULT_MENU_CONTACTS ? this.currentContactSidebarContact : this.currentContact;
  480. let defIsShow = true;
  481. for (const name in this.CacheContactContainer.get()) {
  482. const show = curact.id == name && this.currentIsDefSidebar;
  483. if(show)defIsShow = !show;
  484. nodes.push(
  485. <div class={cls} v-show={show}>
  486. {this.CacheContactContainer.get(name)}
  487. </div>,
  488. );
  489. }
  490. for (const name in this.CacheMenuContainer.get()) {
  491. nodes.push(
  492. <div
  493. class={cls}
  494. v-show={this.activeSidebar == name && !this.currentIsDefSidebar}
  495. >
  496. {this.CacheMenuContainer.get(name)}
  497. </div>,
  498. );
  499. }
  500. nodes.push(
  501. <div
  502. class={cls}
  503. v-show={this._menuIsMessages() && defIsShow && curact.id}
  504. >
  505. <div class="lemon-container__title">
  506. {useScopedSlot(
  507. this.$scopedSlots["message-title"],
  508. <div class="lemon-container__displayname">
  509. {curact.displayName}
  510. </div>,
  511. curact,
  512. )}
  513. </div>
  514. <div class="lemon-vessel">
  515. <div class="lemon-vessel__left">
  516. <lemon-messages
  517. ref="messages"
  518. loading-text={this.loadingText}
  519. loadend-text={this.loadendText}
  520. hide-time={this.hideMessageTime}
  521. hide-name={this.hideMessageName}
  522. time-format={this.messageTimeFormat}
  523. reverse-user-id={this.user.id}
  524. on-reach-top={this._emitPullMessages}
  525. messages={this.currentMessages}
  526. />
  527. <lemon-editor
  528. ref="editor"
  529. tools={this.editorTools}
  530. sendText={this.sendText}
  531. sendKey={this.sendKey}
  532. wrapKey={this.wrapKey}
  533. onSend={this._handleSend}
  534. onUpload={this._handleUpload}
  535. />
  536. </div>
  537. <div class="lemon-vessel__right">
  538. {useScopedSlot(this.$scopedSlots["message-side"], null, curact)}
  539. </div>
  540. </div>
  541. </div>,
  542. );
  543. nodes.push(
  544. <div class={cls} v-show={!curact.id && this.currentIsDefSidebar}>
  545. {this.$slots.cover}
  546. </div>,
  547. );
  548. nodes.push(
  549. <div
  550. class={cls}
  551. v-show={this._menuIsContacts() && defIsShow && curact.id}
  552. >
  553. {useScopedSlot(
  554. this.$scopedSlots["contact-info"],
  555. <div class="lemon-contact-info">
  556. <lemon-avatar src={curact.avatar} size={90} />
  557. <h4>{curact.displayName}</h4>
  558. <lemon-button
  559. on-click={() => {
  560. if (isEmpty(curact.lastContent)) {
  561. this.updateContact({
  562. id: curact.id,
  563. lastContent: " ",
  564. });
  565. }
  566. this.changeContact(curact.id, DEFAULT_MENU_LASTMESSAGES);
  567. }}
  568. >
  569. 发送消息
  570. </lemon-button>
  571. </div>,
  572. curact,
  573. )}
  574. </div>,
  575. );
  576. return nodes;
  577. },
  578. _handleSidebarScroll() {
  579. contextmenu.hide();
  580. },
  581. _addContact(data, t) {
  582. const type = {
  583. 0: "unshift",
  584. 1: "push",
  585. }[t];
  586. this.contacts[type](data);
  587. },
  588. _addMessage(data, contactId, t) {
  589. const type = {
  590. 0: "unshift",
  591. 1: "push",
  592. }[t];
  593. if (!Array.isArray(data)) data = [data];
  594. allMessages[contactId] = allMessages[contactId] || [];
  595. allMessages[contactId][type](...data);
  596. },
  597. /**
  598. * 设置最新消息DOM
  599. * @param {String} messageType 消息类型
  600. * @param {Function} render 返回消息 vnode
  601. */
  602. setLastContentRender(messageType, render) {
  603. lastContentRender[messageType] = render;
  604. },
  605. lastContentRender(message) {
  606. if (!isFunction(lastContentRender[message.type])) {
  607. console.error(
  608. `not found '${
  609. message.type
  610. }' of the latest message renderer,try to use ‘setLastContentRender()’`,
  611. );
  612. return "";
  613. }
  614. return lastContentRender[message.type].call(this, message);
  615. },
  616. /**
  617. * 将字符串内的 EmojiItem.name 替换为 img
  618. * @param {String} str 被替换的字符串
  619. * @return {String} 替换后的字符串
  620. */
  621. emojiNameToImage(str) {
  622. return str.replace(/\[!(\w+)\]/gi, (str, match) => {
  623. const file = match;
  624. return emojiMap[file]
  625. ? `<img emoji-name="${match}" src="${emojiMap[file]}" />`
  626. : `[!${match}]`;
  627. });
  628. },
  629. emojiImageToName(str) {
  630. return str.replace(/<img emoji-name=\"([^\"]*?)\" [^>]*>/gi, "[!$1]");
  631. },
  632. updateCurrentMessages() {
  633. if (!allMessages[this.currentContactId])
  634. allMessages[this.currentContactId] = [];
  635. this.currentMessages = allMessages[this.currentContactId];
  636. },
  637. /**
  638. * 将当前聊天窗口滚动到底部
  639. */
  640. messageViewToBottom() {
  641. this.$refs.messages.scrollToBottom();
  642. },
  643. /**
  644. * 设置联系人的草稿信息
  645. */
  646. setDraft(cid, editorValue) {
  647. if (isEmpty(cid) || isEmpty(editorValue)) return false;
  648. const contact = this.findContact(cid);
  649. let lastContent = contact.lastContent;
  650. if (isEmpty(contact)) return false;
  651. if (this.CacheDraft.has(cid)) {
  652. lastContent = this.CacheDraft.get(cid).lastContent;
  653. }
  654. this.CacheDraft.set(cid, {
  655. editorValue,
  656. lastContent,
  657. });
  658. this.updateContact({
  659. id: cid,
  660. lastContent: `<span style="color:red;">[草稿]</span><span>${this.lastContentRender(
  661. { type: "text", content: editorValue },
  662. )}</span>`,
  663. });
  664. },
  665. /**
  666. * 清空联系人草稿信息
  667. */
  668. clearDraft(contactId) {
  669. const draft = this.CacheDraft.get(contactId);
  670. if (draft) {
  671. const currentContent = this.findContact(contactId).lastContent;
  672. if (
  673. currentContent.indexOf('<span style="color:red;">[草稿]</span>') === 0
  674. ) {
  675. this.updateContact({
  676. id: contactId,
  677. lastContent: draft.lastContent,
  678. });
  679. }
  680. this.CacheDraft.remove(contactId);
  681. }
  682. },
  683. /**
  684. * 改变聊天对象
  685. * @param contactId 联系人 id
  686. */
  687. async changeContact(contactId, menuName) {
  688. if (menuName) {
  689. this.changeMenu(menuName);
  690. } else {
  691. if (this._changeContactLock ||
  692. (this.activeSidebar == DEFAULT_MENU_LASTMESSAGES && this.currentContactId == contactId) ||
  693. (this.activeSidebar == DEFAULT_MENU_CONTACTS && this.currentContactIdSidebarContact == contactId)
  694. )
  695. return false;
  696. }
  697. //保存上个聊天目标的草稿
  698. if (this.currentContactId) {
  699. const editorValue = clearHtmlExcludeImg(this.getEditorValue()).trim();
  700. if (editorValue) {
  701. this.setDraft(this.currentContactId, editorValue);
  702. this.setEditorValue();
  703. } else {
  704. this.clearDraft(this.currentContactId);
  705. }
  706. }
  707. if (this.activeSidebar == DEFAULT_MENU_CONTACTS) {
  708. this.currentContactIdSidebarContact = contactId
  709. } else {
  710. this.currentContactId = contactId;
  711. }
  712. if (!this.currentContactId) return false;
  713. this.$emit("change-contact", this.currentContact, this);
  714. if (
  715. isFunction(this.currentContact.renderContainer) ||
  716. this.activeSidebar == DEFAULT_MENU_CONTACTS
  717. ) {
  718. return;
  719. }
  720. //填充草稿内容
  721. const draft = this.CacheDraft.get(contactId);
  722. if (draft) this.setEditorValue(draft.editorValue);
  723. if (this.CacheMessageLoaded.has(contactId)) {
  724. this.$refs.messages.loaded();
  725. } else {
  726. this.$refs.messages.resetLoadState();
  727. }
  728. if (!allMessages[contactId]) {
  729. this.updateCurrentMessages();
  730. this._emitPullMessages(isEnd => {
  731. this.messageViewToBottom();
  732. });
  733. } else {
  734. setTimeout(() => {
  735. this.updateCurrentMessages();
  736. this.messageViewToBottom();
  737. }, 0);
  738. }
  739. },
  740. /**
  741. * 删除一条聊天消息
  742. * @param messageId 消息 id
  743. * @param contactId 联系人 id
  744. */
  745. removeMessage(messageId) {
  746. const message = this.findMessage(messageId);
  747. if (!message) return false;
  748. const index = allMessages[message.toContactId].findIndex(
  749. ({ id }) => id == messageId,
  750. );
  751. allMessages[message.toContactId].splice(index, 1);
  752. return true;
  753. },
  754. /**
  755. * 修改聊天一条聊天消息
  756. * @param {Message} data 根据 data.id 查找聊天消息并覆盖传入的值
  757. * @param contactId 联系人 id
  758. */
  759. updateMessage(message) {
  760. if (!message.id) return false;
  761. let historyMessage = this.findMessage(message.id);
  762. if (!historyMessage) return false;
  763. historyMessage = Object.assign(historyMessage, message, {
  764. toContactId: historyMessage.toContactId,
  765. });
  766. return true;
  767. },
  768. /**
  769. * 手动更新对话消息
  770. * @param {String} messageId 消息ID,如果为空则更新当前聊天窗口的所有消息
  771. */
  772. forceUpdateMessage(messageId) {
  773. if (!messageId) {
  774. this.$refs.messages.$forceUpdate();
  775. } else {
  776. const components = this.$refs.messages.$refs.message;
  777. if (components) {
  778. const messageComponent = components.find(
  779. com => com.$attrs.message.id == messageId,
  780. );
  781. if (messageComponent) messageComponent.$forceUpdate();
  782. }
  783. }
  784. },
  785. _customContainerReady(render, cacheDrive, key) {
  786. if (isFunction(render) && !cacheDrive.has(key)) {
  787. cacheDrive.set(key, render.call(this));
  788. }
  789. },
  790. /**
  791. * 切换左侧按钮
  792. * @param {String} name 按钮 name
  793. */
  794. changeMenu(name) {
  795. this.$emit("change-menu", name);
  796. this.activeSidebar = name;
  797. },
  798. /**
  799. * 初始化编辑框的 Emoji 表情列表,是 Lemon-editor.initEmoji 的代理方法
  800. * @param {Array<Emoji,EmojiItem>} data emoji 数据
  801. * Emoji = {label: 表情,children: [{name: wx,title: 微笑,src: url}]} 分组
  802. * EmojiItem = {name: wx,title: 微笑,src: url} 无分组
  803. */
  804. initEmoji(data) {
  805. let flatData = [];
  806. this.$refs.editor.initEmoji(data);
  807. if (data[0].label) {
  808. data.forEach(item => {
  809. flatData.push(...item.children);
  810. });
  811. } else {
  812. flatData = data;
  813. }
  814. flatData.forEach(({ name, src }) => (emojiMap[name] = src));
  815. },
  816. initEditorTools(data) {
  817. //this.editorTools = data;
  818. this.editorTools = data;
  819. //this.$refs.editor.initTools(data);
  820. },
  821. /**
  822. * 初始化左侧按钮
  823. * @param {Array<Menu>} data 按钮数据
  824. */
  825. initMenus(data) {
  826. const defaultMenus = [
  827. {
  828. name: DEFAULT_MENU_LASTMESSAGES,
  829. title: "聊天",
  830. unread: 0,
  831. click: null,
  832. render: menu => {
  833. return <i class="lemon-icon-message" />;
  834. },
  835. isBottom: false,
  836. },
  837. {
  838. name: DEFAULT_MENU_CONTACTS,
  839. title: "通讯录",
  840. unread: 0,
  841. click: null,
  842. render: menu => {
  843. return <i class="lemon-icon-addressbook" />;
  844. },
  845. isBottom: false,
  846. },
  847. ];
  848. let menus = [];
  849. if (Array.isArray(data)) {
  850. const indexMap = {
  851. messages: 0,
  852. contacts: 1,
  853. };
  854. const indexKeys = Object.keys(indexMap);
  855. menus = data.map(item => {
  856. if (indexKeys.includes(item.name)) {
  857. return {
  858. ...defaultMenus[indexMap[item.name]],
  859. ...item,
  860. ...{ renderContainer: null },
  861. };
  862. }
  863. if (item.renderContainer) {
  864. this._customContainerReady(
  865. item.renderContainer,
  866. this.CacheMenuContainer,
  867. item.name,
  868. );
  869. }
  870. return item;
  871. });
  872. } else {
  873. menus = defaultMenus;
  874. }
  875. this.menus = menus;
  876. },
  877. /**
  878. * 初始化联系人数据
  879. * @param {Array<Contact>} data 联系人列表
  880. */
  881. initContacts(data) {
  882. this.contacts = data;
  883. this.sortContacts();
  884. },
  885. /**
  886. * 使用 联系人的 index 值进行排序
  887. */
  888. sortContacts() {
  889. this.contacts.sort((a, b) => {
  890. if (!a.index) return;
  891. return a.index.localeCompare(b.index);
  892. });
  893. },
  894. appendContact(contact) {
  895. if (isEmpty(contact.id) || isEmpty(contact.displayName)) {
  896. console.error("id | displayName cant be empty");
  897. return false;
  898. }
  899. if (this.hasContact(contact.id)) return true;
  900. this.contacts.push(
  901. Object.assign(
  902. {
  903. id: "",
  904. displayName: "",
  905. avatar: "",
  906. index: "",
  907. unread: 0,
  908. lastSendTime: "",
  909. lastContent: "",
  910. },
  911. contact,
  912. ),
  913. );
  914. return true;
  915. },
  916. removeContact(id) {
  917. const index = this.findContactIndexById(id);
  918. if (index === -1) return false;
  919. this.contacts.splice(index, 1);
  920. this.CacheDraft.remove(id);
  921. this.CacheMessageLoaded.remove(id);
  922. return true;
  923. },
  924. /**
  925. * 修改联系人数据
  926. * @param {Contact} data 修改的数据,根据 Contact.id 查找联系人并覆盖传入的值
  927. */
  928. updateContact(data) {
  929. const contactId = data.id;
  930. delete data.id;
  931. const index = this.findContactIndexById(contactId);
  932. if (index !== -1) {
  933. const { unread } = data;
  934. if (isString(unread)) {
  935. if (unread.indexOf("+") === 0 || unread.indexOf("-") === 0) {
  936. data.unread =
  937. parseInt(unread) + parseInt(this.contacts[index].unread);
  938. }
  939. }
  940. this.$set(this.contacts, index, {
  941. ...this.contacts[index],
  942. ...data,
  943. });
  944. }
  945. },
  946. /**
  947. * 根据 id 查找联系人的索引
  948. * @param contactId 联系人 id
  949. * @return {Number} 联系人索引,未找到返回 -1
  950. */
  951. findContactIndexById(contactId) {
  952. return this.contacts.findIndex(item => item.id == contactId);
  953. },
  954. /**
  955. * 根据 id 查找判断是否存在联系人
  956. * @param contactId 联系人 id
  957. * @return {Boolean}
  958. */
  959. hasContact(contactId) {
  960. return this.findContactIndexById(contactId) !== -1;
  961. },
  962. findMessage(messageId) {
  963. for (const key in allMessages) {
  964. const message = allMessages[key].find(({ id }) => id == messageId);
  965. if (message) return message;
  966. }
  967. },
  968. findContact(contactId) {
  969. return this.getContacts().find(({ id }) => id == contactId);
  970. },
  971. /**
  972. * 返回所有联系人
  973. * @return {Array<Contact>}
  974. */
  975. getContacts() {
  976. return this.contacts;
  977. },
  978. //返回当前聊天窗口联系人信息
  979. getCurrentContact() {
  980. return this.currentContact;
  981. },
  982. getCurrentMessages() {
  983. return this.currentMessages;
  984. },
  985. setEditorValue(val = "") {
  986. if (!isString(val)) return false;
  987. this.$refs.editor.setValue(this.emojiNameToImage(val));
  988. },
  989. getEditorValue() {
  990. return this.$refs.editor.getFormatValue();
  991. },
  992. /**
  993. * 清空某个联系人的消息,切换到该联系人时会重新触发pull-messages事件
  994. */
  995. clearMessages(contactId) {
  996. if (contactId) {
  997. delete allMessages[contactId];
  998. this.CacheMessageLoaded.remove(contactId);
  999. this.CacheDraft.remove(contactId);
  1000. } else {
  1001. allMessages = {};
  1002. this.CacheMessageLoaded.remove();
  1003. this.CacheDraft.remove();
  1004. }
  1005. return true;
  1006. },
  1007. /**
  1008. * 返回所有消息
  1009. * @return {Object<Contact.id,Message>}
  1010. */
  1011. getMessages(contactId) {
  1012. return (contactId ? allMessages[contactId] : allMessages) || [];
  1013. },
  1014. changeDrawer(params) {
  1015. this.drawerVisible = !this.drawerVisible;
  1016. if (this.drawerVisible == true) this.openDrawer(params);
  1017. },
  1018. // openDrawer(data) {
  1019. // renderDrawerContent = data || new Function();
  1020. // this.drawerVisible = true;
  1021. // },
  1022. openDrawer(params) {
  1023. renderDrawerContent = isFunction(params)
  1024. ? params
  1025. : params.render || new Function();
  1026. const wrapperWidth = this.$refs.wrapper.clientWidth;
  1027. const wrapperHeight = this.$refs.wrapper.clientHeight;
  1028. let width = params.width || 200;
  1029. let height = params.height || wrapperHeight;
  1030. let offsetX = params.offsetX || 0;
  1031. let offsetY = params.offsetY || 0;
  1032. const position = params.position || "right";
  1033. if (isString(width)) width = wrapperWidth * toPoint(width);
  1034. if (isString(height)) height = wrapperHeight * toPoint(height);
  1035. if (isString(offsetX)) offsetX = wrapperWidth * toPoint(offsetX);
  1036. if (isString(offsetY)) offsetY = wrapperHeight * toPoint(offsetY);
  1037. this.$refs.drawer.style.width = `${width}px`;
  1038. this.$refs.drawer.style.height = `${height}px`;
  1039. let left = 0;
  1040. let top = 0;
  1041. let shadow = "";
  1042. if (position == "right") {
  1043. left = wrapperWidth;
  1044. } else if (position == "rightInside") {
  1045. left = wrapperWidth - width;
  1046. shadow = `-15px 0 16px -14px rgba(0,0,0,0.08)`;
  1047. } else if (position == "center") {
  1048. left = wrapperWidth / 2 - width / 2;
  1049. top = wrapperHeight / 2 - height / 2;
  1050. shadow = `0 0 20px rgba(0,0,0,0.08)`;
  1051. }
  1052. left += offsetX;
  1053. top += offsetY + -1;
  1054. this.$refs.drawer.style.top = `${top}px`;
  1055. this.$refs.drawer.style.left = `${left}px`;
  1056. this.$refs.drawer.style.boxShadow = shadow;
  1057. this.drawerVisible = true;
  1058. },
  1059. closeDrawer() {
  1060. this.drawerVisible = false;
  1061. },
  1062. setAtUserList(data,callEvery) {
  1063. this.$refs.editor.chatArea.updateConfig({
  1064. userList: data,
  1065. needCallEvery: callEvery
  1066. });
  1067. },
  1068. setUserTag(data) {
  1069. this.$refs.editor.chatArea.setUserTag(data);
  1070. this.$refs.editor._checkSubmitDisabled();
  1071. },
  1072. },
  1073. };
  1074. </script>
  1075. <style lang="stylus">
  1076. bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
  1077. @import '~styles/utils/index'
  1078. +b(lemon-wrapper)
  1079. display flex
  1080. font-size 14px
  1081. font-family "Microsoft YaHei"
  1082. //mask-image radial-gradient(circle, white 100%, black 100%)
  1083. background #efefef
  1084. transition all .4s bezier
  1085. position relative
  1086. p
  1087. margin 0
  1088. img
  1089. vertical-align middle
  1090. border-style none
  1091. +b(lemon-menu)
  1092. flex-column()
  1093. align-items center
  1094. width 60px
  1095. background #1d232a
  1096. padding 15px 0
  1097. position relative
  1098. user-select none
  1099. +e(bottom)
  1100. flex-column()
  1101. position absolute
  1102. bottom 0
  1103. +e(avatar)
  1104. margin-bottom 20px
  1105. cursor pointer
  1106. +e(item)
  1107. color #999
  1108. cursor pointer
  1109. padding 14px 10px
  1110. max-width 100%
  1111. +m(active)
  1112. color #0fd547
  1113. &:hover:not(.lemon-menu__item--active)
  1114. color #eee
  1115. word-break()
  1116. > *
  1117. font-size 24px
  1118. .ant-badge-count
  1119. display inline-block
  1120. padding 0 4px
  1121. height 18px
  1122. line-height 16px
  1123. min-width 18px
  1124. .ant-badge-count
  1125. .ant-badge-dot
  1126. box-shadow 0 0 0 1px #1d232a
  1127. +b(lemon-sidebar)
  1128. width 250px
  1129. background #efefef
  1130. display flex
  1131. flex-direction column
  1132. +e(scroll)
  1133. overflow-y auto
  1134. scrollbar-light()
  1135. +e(label)
  1136. padding 6px 14px 6px 14px
  1137. color #666
  1138. font-size 12px
  1139. margin 0
  1140. text-align left
  1141. +b(lemon-contact--active)
  1142. background #d9d9d9
  1143. +b(lemon-container)
  1144. flex 1
  1145. flex-column()
  1146. background #f4f4f4
  1147. word-break()
  1148. position relative
  1149. z-index 10
  1150. +e(title)
  1151. padding 15px 15px
  1152. +e(displayname)
  1153. font-size 16px
  1154. +b(lemon-vessel)
  1155. display flex
  1156. flex 1
  1157. min-height 100px
  1158. +e(left)
  1159. display flex
  1160. flex-direction column
  1161. flex 1
  1162. +e(right)
  1163. flex none
  1164. +b(lemon-messages)
  1165. flex 1
  1166. height auto
  1167. +b(lemon-drawer)
  1168. position absolute
  1169. top 0
  1170. overflow hidden
  1171. background #f6f6f6
  1172. z-index 11
  1173. display none
  1174. +b(lemon-wrapper)
  1175. +m(drawer-show)
  1176. +b(lemon-drawer)
  1177. display block
  1178. +b(lemon-contact-info)
  1179. flex-column()
  1180. justify-content center
  1181. align-items center
  1182. height 100%
  1183. h4
  1184. font-size 16px
  1185. font-weight normal
  1186. margin 10px 0 20px 0
  1187. user-select none
  1188. .lemon-wrapper--theme-blue
  1189. .lemon-message__content
  1190. background #f3f3f3
  1191. &::before
  1192. border-right-color #f3f3f3
  1193. .lemon-message--reverse .lemon-message__content
  1194. background #e6eeff
  1195. &::before
  1196. border-left-color #e6eeff
  1197. .lemon-container
  1198. background #fff
  1199. .lemon-sidebar
  1200. background #f9f9f9
  1201. .lemon-contact
  1202. background #f9f9f9
  1203. &:hover:not(.lemon-contact--active)
  1204. background #f1f1f1
  1205. &--active
  1206. background #e9e9e9
  1207. .lemon-menu
  1208. background #096bff
  1209. .lemon-menu__item
  1210. color rgba(255,255,255,0.4)
  1211. &:hover:not(.lemon-menu__item--active)
  1212. color rgba(255,255,255,0.6)
  1213. &--active
  1214. color #fff
  1215. text-shadow 0 0 10px rgba(2,48,118,0.4)
  1216. .lemon-wrapper--simple
  1217. .lemon-menu
  1218. .lemon-sidebar
  1219. display none
  1220. .lemon-wrapper--simple
  1221. .lemon-menu
  1222. .lemon-sidebar
  1223. display none
  1224. +b(lemon-contextmenu)
  1225. border-radius 4px
  1226. font-size 14px
  1227. font-variant tabular-nums
  1228. line-height 1.5
  1229. color rgba(0, 0, 0, 0.65)
  1230. z-index 9999
  1231. background-color #fff
  1232. border-radius 6px
  1233. box-shadow 0 2px 8px rgba(0, 0, 0, 0.06)
  1234. position absolute
  1235. transform-origin 50% 150%
  1236. box-sizing border-box
  1237. user-select none
  1238. overflow hidden
  1239. min-width 120px
  1240. +e(item)
  1241. font-size 14px
  1242. line-height 16px
  1243. padding 10px 15px
  1244. cursor pointer
  1245. display flex
  1246. align-items center
  1247. color #333
  1248. > span
  1249. display inline-block
  1250. flex none
  1251. //max-width 100px
  1252. ellipsis()
  1253. &:hover
  1254. background #f3f3f3
  1255. color #000
  1256. &:active
  1257. background #e9e9e9
  1258. +e(icon)
  1259. font-size 16px
  1260. margin-right 4px
  1261. </style>