index.vue 34 KB

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