index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  1. <script>
  2. import { useScopedSlot, fastDone, generateUUID } from "utils";
  3. import { isFunction, isString, isEmpty } from "utils/validate";
  4. import {
  5. DEFAULT_MENUS,
  6. DEFAULT_MENU_LASTMESSAGES,
  7. DEFAULT_MENU_CONTACTS
  8. } from "utils/constant";
  9. import lastContentRender from "../lastContentRender";
  10. import MemoryCache from "utils/cache/memory";
  11. const CacheContactContainer = new MemoryCache();
  12. const CacheMenuContainer = new MemoryCache();
  13. const CacheMessageLoaded = new MemoryCache();
  14. const messages = {};
  15. const emojiMap = {};
  16. let renderDrawerContent = () => {};
  17. export default {
  18. name: "LemonImui",
  19. provide() {
  20. return {
  21. IMUI: this
  22. };
  23. },
  24. props: {
  25. /**
  26. * 消息时间格式化规则
  27. */
  28. messageTimeFormat: Function,
  29. /**
  30. * 联系人最新消息时间格式化规则
  31. */
  32. contactTimeFormat: Function,
  33. /**
  34. * 初始化时是否隐藏抽屉
  35. */
  36. hideDrawer: {
  37. type: Boolean,
  38. default: true
  39. },
  40. /**
  41. * 初始化时是否隐藏导航按钮上的头像
  42. */
  43. hideMenuAvatar: Boolean,
  44. hideMenu: Boolean,
  45. user: {
  46. type: Object,
  47. default: () => {
  48. return {};
  49. }
  50. }
  51. },
  52. data() {
  53. return {
  54. drawerVisible: !this.hideDrawer,
  55. currentContactId: "",
  56. activeSidebar: DEFAULT_MENU_LASTMESSAGES,
  57. contacts: [],
  58. menus: []
  59. };
  60. },
  61. render() {
  62. return this._renderWrapper([
  63. this._renderMenu(),
  64. this._renderSidebarMessage(),
  65. this._renderSidebarContact(),
  66. this._renderContainer(),
  67. this._renderDrawer()
  68. ]);
  69. },
  70. created() {
  71. this.initMenus();
  72. },
  73. async mounted() {
  74. await this.$nextTick();
  75. },
  76. computed: {
  77. currentMessages() {
  78. return messages[this.currentContactId] || [];
  79. },
  80. currentContact() {
  81. return this.contacts.find(item => item.id == this.currentContactId) || {};
  82. },
  83. currentMenu() {
  84. return this.menus.find(item => item.name == this.activeSidebar) || {};
  85. },
  86. currentIsDefSidebar() {
  87. return DEFAULT_MENUS.includes(this.activeSidebar);
  88. },
  89. lastMessages() {
  90. const data = this.contacts.filter(item => !isEmpty(item.lastContent));
  91. data.sort((a1, a2) => {
  92. return a2.lastSendTime - a1.lastSendTime;
  93. });
  94. return data;
  95. }
  96. },
  97. watch: {
  98. activeSidebar() {}
  99. },
  100. methods: {
  101. _menuIsContacts() {
  102. return this.activeSidebar == DEFAULT_MENU_CONTACTS;
  103. },
  104. _menuIsMessages() {
  105. return this.activeSidebar == DEFAULT_MENU_LASTMESSAGES;
  106. },
  107. _createMessage(message) {
  108. return {
  109. ...{
  110. id: generateUUID(),
  111. type: "text",
  112. status: "going",
  113. sendTime: new Date().getTime(),
  114. toContactId: this.currentContactId,
  115. fromUser: {
  116. ...this.user
  117. }
  118. },
  119. ...message
  120. };
  121. },
  122. appendMessage(message, contactId = this.currentContactId) {
  123. this._addMessage(message, contactId, 1);
  124. this.messageViewToBottom();
  125. },
  126. _emitSend(message, next, file) {
  127. this.$emit(
  128. "send",
  129. message,
  130. (replaceMessage = { status: "succeed" }) => {
  131. next();
  132. message = Object.assign(message, replaceMessage);
  133. this.forceUpdateMessage(message.id);
  134. },
  135. file
  136. );
  137. },
  138. _handleSend(text) {
  139. const message = this._createMessage({ content: text });
  140. this.appendMessage(message);
  141. this._emitSend(message, () => {
  142. this.updateContact(message.toContactId, {
  143. lastContent: this.lastContentRender(message),
  144. lastSendTime: message.sendTime
  145. });
  146. });
  147. },
  148. _handleUpload(file) {
  149. const imageTypes = ["image/gif", "image/jpeg", "image/png"];
  150. let joinMessage;
  151. if (imageTypes.includes(file.type)) {
  152. joinMessage = {
  153. type: "image",
  154. content: URL.createObjectURL(file)
  155. };
  156. } else {
  157. joinMessage = {
  158. type: "file",
  159. fileSize: file.size,
  160. fileName: file.name,
  161. content: ""
  162. };
  163. }
  164. const message = this._createMessage(joinMessage);
  165. this.appendMessage(message);
  166. this._emitSend(
  167. message,
  168. () => {
  169. this.updateContact(message.toContactId, {
  170. lastContent: this.lastContentRender(message),
  171. lastSendTime: message.sendTime
  172. });
  173. },
  174. file
  175. );
  176. },
  177. _emitPullMessages(next) {
  178. this.$emit(
  179. "pull-messages",
  180. this.currentContact,
  181. (messages, isEnd = false) => {
  182. this._addMessage(messages, this.currentContactId, 0);
  183. CacheMessageLoaded.set(this.currentContactId, isEnd);
  184. if (isEnd == true) this.$refs.messages.loaded();
  185. next(isEnd);
  186. }
  187. );
  188. },
  189. clearCacheContainer(name) {
  190. CacheContactContainer.remove(name);
  191. CacheMenuContainer.remove(name);
  192. },
  193. _renderWrapper(children) {
  194. return (
  195. <div
  196. class={[
  197. "lemon-wrapper",
  198. this.drawerVisible && "lemon-wrapper--drawer-show"
  199. ]}
  200. >
  201. {children}
  202. </div>
  203. );
  204. },
  205. _renderMenu() {
  206. const menuItem = this._renderMenuItem();
  207. return (
  208. <div class="lemon-menu" v-show={!this.hideMenu}>
  209. {
  210. <lemon-avatar
  211. v-show={!this.hideMenuAvatar}
  212. on-click={e => {
  213. this.$emit("menu-avatar-click", e);
  214. }}
  215. class="lemon-menu__avatar"
  216. src={this.user.avatar}
  217. />
  218. }
  219. {menuItem.top}
  220. {this.$slots.menu}
  221. <div class="lemon-menu__bottom">
  222. {this.$slots["menu-bottom"]}
  223. {menuItem.bottom}
  224. </div>
  225. </div>
  226. );
  227. },
  228. _renderMenuAvatar() {
  229. return;
  230. },
  231. _renderMenuItem() {
  232. const top = [];
  233. const bottom = [];
  234. this.menus.forEach(item => {
  235. const { name, title, unread, render, click } = item;
  236. const node = (
  237. <div
  238. class={[
  239. "lemon-menu__item",
  240. { "lemon-menu__item--active": this.activeSidebar == name }
  241. ]}
  242. on-click={() => {
  243. fastDone(click, () => {
  244. if (name) this.changeMenu(name);
  245. });
  246. }}
  247. title={title}
  248. >
  249. <lemon-badge count={unread}>{render(item)}</lemon-badge>
  250. </div>
  251. );
  252. item.isBottom === true ? bottom.push(node) : top.push(node);
  253. });
  254. return {
  255. top,
  256. bottom
  257. };
  258. },
  259. _renderSidebarMessage() {
  260. return this._renderSidebar(
  261. [
  262. useScopedSlot(this.$scopedSlots["message-sidebar"]),
  263. this.lastMessages.map(contact => {
  264. return this._renderContact(
  265. {
  266. contact,
  267. timeFormat: this.contactTimeFormat
  268. },
  269. () => this.changeContact(contact.id)
  270. );
  271. })
  272. ],
  273. DEFAULT_MENU_LASTMESSAGES
  274. );
  275. },
  276. _renderContact(props, onClick) {
  277. const {
  278. click: customClick,
  279. renderContainer,
  280. id: contactId
  281. } = props.contact;
  282. const click = () => {
  283. fastDone(customClick, () => {
  284. onClick();
  285. this._customContainerReady(
  286. renderContainer,
  287. CacheContactContainer,
  288. contactId
  289. );
  290. });
  291. };
  292. return (
  293. <lemon-contact
  294. class={{
  295. "lemon-contact--active": this.currentContactId == props.contact.id
  296. }}
  297. props={props}
  298. on-click={click}
  299. />
  300. );
  301. },
  302. _renderSidebarContact() {
  303. let prevIndex;
  304. return this._renderSidebar(
  305. [
  306. useScopedSlot(this.$scopedSlots["contact-sidebar"]),
  307. this.contacts.map(contact => {
  308. if (!contact.index) return;
  309. contact.index = contact.index.replace(/\[[0-9]*\]/, "");
  310. const node = [
  311. contact.index !== prevIndex && (
  312. <p class="lemon-sidebar__label">{contact.index}</p>
  313. ),
  314. this._renderContact(
  315. {
  316. contact: contact,
  317. simple: true
  318. },
  319. () => this.changeContact(contact.id)
  320. )
  321. ];
  322. prevIndex = contact.index;
  323. return node;
  324. })
  325. ],
  326. DEFAULT_MENU_CONTACTS
  327. );
  328. },
  329. _renderSidebar(children, name) {
  330. return (
  331. <div class="lemon-sidebar" v-show={this.activeSidebar == name}>
  332. {children}
  333. </div>
  334. );
  335. },
  336. _renderDrawer() {
  337. return this._menuIsMessages() && this.currentContactId ? (
  338. <div class="lemon-drawer">
  339. {renderDrawerContent()}
  340. {useScopedSlot(this.$scopedSlots.drawer, "", this.currentContact)}
  341. </div>
  342. ) : (
  343. ""
  344. );
  345. },
  346. _isContactContainerCache(name) {
  347. return name.startsWith("contact#");
  348. },
  349. _renderContainer() {
  350. const nodes = [];
  351. const cls = "lemon-container";
  352. const curact = this.currentContact;
  353. let defIsShow = true;
  354. for (const name in CacheContactContainer.get()) {
  355. const show = curact.id == name && this.currentIsDefSidebar;
  356. defIsShow = !show;
  357. nodes.push(
  358. <div class={cls} v-show={show}>
  359. {CacheContactContainer.get(name)}
  360. </div>
  361. );
  362. }
  363. for (const name in CacheMenuContainer.get()) {
  364. nodes.push(
  365. <div
  366. class={cls}
  367. v-show={this.activeSidebar == name && !this.currentIsDefSidebar}
  368. >
  369. {CacheMenuContainer.get(name)}
  370. </div>
  371. );
  372. }
  373. nodes.push(
  374. <div
  375. class={cls}
  376. v-show={this._menuIsMessages() && defIsShow && curact.id}
  377. >
  378. <div class="lemon-container__title">
  379. <div class="lemon-container__displayname">
  380. {useScopedSlot(
  381. this.$scopedSlots["contact-title"],
  382. curact.displayName,
  383. curact
  384. )}
  385. </div>
  386. </div>
  387. <lemon-messages
  388. ref="messages"
  389. time-format={this.messageTimeFormat}
  390. reverse-user-id={this.user.id}
  391. on-reach-top={this._emitPullMessages}
  392. messages={this.currentMessages}
  393. />
  394. <lemon-editor
  395. ref="editor"
  396. onSend={this._handleSend}
  397. onUpload={this._handleUpload}
  398. />
  399. </div>
  400. );
  401. nodes.push(
  402. <div class={cls} v-show={!curact.id && this.currentIsDefSidebar}>
  403. {this.$slots.cover}
  404. </div>
  405. );
  406. nodes.push(
  407. <div
  408. class={cls}
  409. v-show={this._menuIsContacts() && defIsShow && curact.id}
  410. >
  411. {useScopedSlot(
  412. this.$scopedSlots["contact-info"],
  413. <div class="lemon-contact-info">
  414. <lemon-avatar src={curact.avatar} size={90} />
  415. <h4>{curact.displayName}</h4>
  416. <lemon-button
  417. on-click={() => {
  418. this.changeContact(curact.id, DEFAULT_MENU_LASTMESSAGES);
  419. }}
  420. >
  421. 发送消息
  422. </lemon-button>
  423. </div>,
  424. curact
  425. )}
  426. </div>
  427. );
  428. return nodes;
  429. },
  430. _addContact(data, t) {
  431. const type = {
  432. 0: "unshift",
  433. 1: "push"
  434. }[t];
  435. //this.contacts[type](cloneDeep(data));
  436. this.contacts[type](data);
  437. },
  438. _addMessage(data, contactId, t) {
  439. const type = {
  440. 0: "unshift",
  441. 1: "push"
  442. }[t];
  443. if (!Array.isArray(data)) data = [data];
  444. messages[contactId] = messages[contactId] || [];
  445. messages[contactId][type](...data);
  446. //console.log(messages[contactId]);
  447. this.forceUpdateMessage();
  448. },
  449. /**
  450. * 设置最新消息DOM
  451. * @param {String} messageType 消息类型
  452. * @param {Function} render 返回消息 vnode
  453. */
  454. setLastContentRender(messageType, render) {
  455. lastContentRender[messageType] = render;
  456. },
  457. lastContentRender(message) {
  458. return lastContentRender[message.type].call(this, message);
  459. },
  460. /**
  461. * 将字符串内的 EmojiItem.name 替换为 img
  462. * @param {String} str 被替换的字符串
  463. * @return {String} 替换后的字符串
  464. */
  465. replaceEmojiName(str) {
  466. return str.replace(/\[!(\w+)\]/gi, (str, match) => {
  467. const file = match;
  468. return emojiMap[file]
  469. ? `<img src="${emojiMap[file]}" />`
  470. : `[!${match}]`;
  471. });
  472. },
  473. /**
  474. * 将当前聊天窗口滚动到底部
  475. */
  476. messageViewToBottom() {
  477. this.$refs.messages.scrollToBottom();
  478. },
  479. /**
  480. * 改变聊天对象
  481. * @param contactId 联系人 id
  482. */
  483. changeContact(contactId, menuName) {
  484. if (this.currentContactId == contactId) {
  485. this.currentContactId = undefined;
  486. }
  487. if (menuName) {
  488. this.changeMenu(menuName);
  489. }
  490. this.currentContactId = contactId;
  491. this.$emit("change-contact", this.currentContact);
  492. if (isFunction(this.currentContact.renderContainer)) {
  493. return;
  494. }
  495. if (this._menuIsMessages()) {
  496. if (!CacheMessageLoaded.has(contactId)) {
  497. this.$refs.messages.resetLoadState();
  498. }
  499. if (!messages[contactId]) {
  500. this._emitPullMessages(isEnd => this.messageViewToBottom());
  501. } else {
  502. setTimeout(() => {
  503. this.messageViewToBottom();
  504. }, 0);
  505. }
  506. }
  507. },
  508. /**
  509. * 删除一条聊天消息
  510. * @param messageId 消息 id
  511. * @param contactId 联系人 id
  512. */
  513. removeMessage(messageId, contactId) {
  514. const index = this.findMessageIndexById(messageId, contactId);
  515. if (index !== -1) {
  516. messages[contactId].splice(index, 1);
  517. this.forceUpdateMessage();
  518. }
  519. },
  520. /**
  521. * 修改聊天一条聊天消息
  522. * @param {Message} data 根据 data.id 查找聊天消息并覆盖传入的值
  523. * @param contactId 联系人 id
  524. */
  525. updateMessage(messageId, contactId, data) {
  526. const index = this.findMessageIndexById(messageId, contactId);
  527. if (index !== -1) {
  528. messages[contactId][index] = Object.assign(
  529. messages[contactId][index],
  530. data
  531. );
  532. console.log("--------", messages[contactId][index]);
  533. this.forceUpdateMessage(messageId);
  534. }
  535. },
  536. /**
  537. * 手动更新对话消息
  538. * @param {String} messageId 消息ID,如果为空则更新当前聊天窗口的所有消息
  539. */
  540. forceUpdateMessage(messageId) {
  541. if (!messageId) {
  542. this.$refs.messages.$forceUpdate();
  543. } else {
  544. const components = this.$refs.messages.$refs.message;
  545. if (components) {
  546. const messageComponent = components.find(
  547. com => com.$attrs.message.id == messageId
  548. );
  549. if (messageComponent) messageComponent.$forceUpdate();
  550. }
  551. }
  552. },
  553. _customContainerReady(render, cacheDrive, key) {
  554. if (isFunction(render) && !cacheDrive.has(key)) {
  555. cacheDrive.set(key, render.call(this));
  556. }
  557. },
  558. /**
  559. * 切换左侧按钮
  560. * @param {String} name 按钮 name
  561. */
  562. changeMenu(name) {
  563. this.$emit("change-menu", name);
  564. this.activeSidebar = name;
  565. },
  566. /**
  567. * 初始化编辑框的 Emoji 表情列表,是 Lemon-editor.initEmoji 的代理方法
  568. * @param {Array<Emoji,EmojiItem>} data emoji 数据
  569. * Emoji = {label: 表情,children: [{name: wx,title: 微笑,src: url}]} 分组
  570. * EmojiItem = {name: wx,title: 微笑,src: url} 无分组
  571. */
  572. initEmoji(data) {
  573. this.$refs.editor.initEmoji(data);
  574. if (data[0].label) {
  575. data = data.flatMap(item => item.children);
  576. }
  577. data.forEach(({ name, src }) => (emojiMap[name] = src));
  578. },
  579. /**
  580. * 初始化左侧按钮
  581. * @param {Array<Menu>} data 按钮数据
  582. */
  583. initMenus(data) {
  584. const defaultMenus = [
  585. {
  586. name: DEFAULT_MENU_LASTMESSAGES,
  587. title: "聊天",
  588. unread: 0,
  589. click: null,
  590. render: menu => {
  591. return <i class="lemon-icon-message" />;
  592. },
  593. isBottom: false
  594. },
  595. {
  596. name: DEFAULT_MENU_CONTACTS,
  597. title: "通讯录",
  598. unread: 0,
  599. click: null,
  600. render: menu => {
  601. return <i class="lemon-icon-addressbook" />;
  602. },
  603. isBottom: false
  604. }
  605. ];
  606. let menus = [];
  607. if (Array.isArray(data)) {
  608. const indexMap = {
  609. lastMessages: 0,
  610. contacts: 1
  611. };
  612. const indexKeys = Object.keys(indexMap);
  613. menus = data.map(item => {
  614. if (indexKeys.includes(item.name)) {
  615. return {
  616. ...defaultMenus[indexMap[item.name]],
  617. ...item,
  618. ...{ renderContainer: null }
  619. };
  620. }
  621. if (item.renderContainer) {
  622. this._customContainerReady(
  623. item.renderContainer,
  624. CacheMenuContainer,
  625. item.name
  626. );
  627. }
  628. return item;
  629. });
  630. } else {
  631. menus = defaultMenus;
  632. }
  633. this.menus = menus;
  634. },
  635. /**
  636. * 初始化联系人数据
  637. * @param {Array<Contact>} data 联系人列表
  638. */
  639. initContacts(data) {
  640. this.contacts.push(...data);
  641. this.sortContacts();
  642. },
  643. /**
  644. * 使用 联系人的 index 值进行排序
  645. */
  646. sortContacts() {
  647. this.contacts.sort((a, b) => {
  648. if (!a.index) return;
  649. return a.index.localeCompare(b.index);
  650. });
  651. },
  652. /**
  653. * 修改联系人数据
  654. * @param {Contact} data 修改的数据,根据 data.id 查找联系人并覆盖传入的值
  655. */
  656. updateContact(contactId, data) {
  657. delete data.id;
  658. delete data.toContactId;
  659. const index = this.findContactIndexById(contactId);
  660. if (index !== -1) {
  661. const { unread } = data;
  662. if (isString(unread)) {
  663. if (unread.indexOf("+") === 0 || unread.indexOf("-") === 0) {
  664. data.unread =
  665. parseInt(unread) + parseInt(this.contacts[index].unread);
  666. }
  667. }
  668. this.$set(this.contacts, index, {
  669. ...this.contacts[index],
  670. ...data
  671. });
  672. }
  673. },
  674. /**
  675. * 根据 id 查找联系人的索引
  676. * @param contactId 联系人 id
  677. * @return {Number} 联系人索引,未找到返回 -1
  678. */
  679. findContactIndexById(contactId) {
  680. return this.contacts.findIndex(item => item.id == contactId);
  681. },
  682. findMessageIndexById(messageId, contactId) {
  683. const msg = messages[contactId];
  684. if (isEmpty(msg)) {
  685. return -1;
  686. }
  687. return msg.findIndex(item => item.id == messageId);
  688. },
  689. findMessageById(messageId, contactId) {
  690. const index = this.findMessageIndexById(messageId, contactId);
  691. if (index !== -1) return messages[contactId][index];
  692. },
  693. /**
  694. * 返回所有联系人
  695. * @return {Array<Contact>}
  696. */
  697. getContacts() {
  698. return this.contacts;
  699. },
  700. /**
  701. * 返回所有消息
  702. * @return {Object<Contact.id,Message>}
  703. */
  704. getMessages(contactId) {
  705. return (contactId ? messages[contactId] : messages) || [];
  706. },
  707. // appendContact(data) {
  708. // this._addContact(data, 0);
  709. // },
  710. // prependContact(data) {
  711. // this._addContact(data, 1);
  712. // },
  713. // addContactMessage(data) {
  714. // this._addContact(data, 0);
  715. // },
  716. // prependContactMessage(data) {
  717. // this._addContact(data, 1);
  718. // },
  719. // appendMessage(data) {},
  720. // prependMessage(data) {},
  721. // removeContact(contactId) {},
  722. // removeContactMessage(contactId) {},
  723. // removeContactAll(contactId) {},
  724. /**
  725. * 将自定义的HTML显示在主窗口内
  726. */
  727. openrenderContainer(vnode) {
  728. //renderContainerQueue[this.activeSidebar] = vnode;
  729. //this.$slots._renderContainer = vnode;
  730. },
  731. changeDrawer(render) {
  732. this.drawerVisible = !this.drawerVisible;
  733. if (this.drawerVisible == true) this.openDrawer(render);
  734. },
  735. openDrawer(render) {
  736. renderDrawerContent = render || new Function();
  737. this.drawerVisible = true;
  738. },
  739. closeDrawer() {
  740. this.drawerVisible = false;
  741. }
  742. }
  743. };
  744. </script>
  745. <style lang="stylus">
  746. wrapper-width = 850px
  747. drawer-width = 200px
  748. bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
  749. @import '~styles/utils/index'
  750. +b(lemon-wrapper)
  751. width wrapper-width
  752. height 580px
  753. display flex
  754. font-size 14px
  755. //mask-image radial-gradient(circle, white 100%, black 100%)
  756. background #efefef
  757. transition all .4s bezier
  758. position relative
  759. p
  760. margin 0
  761. img
  762. vertical-align middle
  763. border-style none
  764. +b(lemon-menu)
  765. flex-column()
  766. align-items center
  767. width 60px
  768. background #1d232a
  769. padding 15px 0
  770. position relative
  771. user-select none
  772. +e(bottom)
  773. flex-column()
  774. position absolute
  775. bottom 0
  776. +e(avatar)
  777. margin-bottom 20px
  778. cursor pointer
  779. +e(item)
  780. color #999
  781. cursor pointer
  782. padding 14px 10px
  783. max-width 100%
  784. +m(active)
  785. color #0fd547
  786. &:hover:not(.lemon-menu__item--active)
  787. color #eee
  788. word-break()
  789. > *
  790. font-size 24px
  791. .ant-badge-count
  792. display inline-block
  793. padding 0 4px
  794. height 18px
  795. line-height 16px
  796. min-width 18px
  797. .ant-badge-count
  798. .ant-badge-dot
  799. box-shadow 0 0 0 1px #1d232a
  800. +b(lemon-sidebar)
  801. width 250px
  802. background #efefef
  803. overflow-y auto
  804. scrollbar-light()
  805. +e(label)
  806. padding 6px 14px 6px 14px
  807. color #666
  808. font-size 12px
  809. margin 0
  810. +b(lemon-contact--active)
  811. background #d9d9d9
  812. +b(lemon-container)
  813. flex 1
  814. flex-column()
  815. background #f4f4f4
  816. word-break()
  817. position relative
  818. z-index 2
  819. +e(title)
  820. padding 15px 15px
  821. +e(displayname)
  822. font-size 16px
  823. +b(lemon-messages)
  824. flex 1
  825. height auto
  826. +b(lemon-drawer)
  827. position absolute
  828. top 0
  829. right 0
  830. overflow hidden
  831. background #f4f4f4
  832. transition width .4s bezier
  833. z-index 1
  834. width drawer-width
  835. height 100%
  836. box-sizing border-box
  837. //border-left 1px solid #e9e9e9
  838. +b(lemon-wrapper)
  839. +m(drawer-show)
  840. +b(lemon-drawer)
  841. right -200px
  842. +b(lemon-contact-info)
  843. flex-column()
  844. justify-content center
  845. align-items center
  846. height 100%
  847. h4
  848. font-size 16px
  849. font-weight normal
  850. margin 10px 0 20px 0
  851. user-select none
  852. </style>