wansfa 1 年之前
父節點
當前提交
3f6911b23c
共有 43 個文件被更改,包括 3565 次插入22 次删除
  1. 2 1
      package.json
  2. 81 0
      src/components/LemonUI/components/avatar.vue
  3. 74 0
      src/components/LemonUI/components/badge.vue
  4. 70 0
      src/components/LemonUI/components/button.vue
  5. 145 0
      src/components/LemonUI/components/contact.vue
  6. 487 0
      src/components/LemonUI/components/editor.vue
  7. 1251 0
      src/components/LemonUI/components/index.vue
  8. 30 0
      src/components/LemonUI/components/lemon-message-voice.vue
  9. 240 0
      src/components/LemonUI/components/message/basic.vue
  10. 38 0
      src/components/LemonUI/components/message/event.vue
  11. 59 0
      src/components/LemonUI/components/message/file.vue
  12. 30 0
      src/components/LemonUI/components/message/image.vue
  13. 36 0
      src/components/LemonUI/components/message/text.vue
  14. 187 0
      src/components/LemonUI/components/messages.vue
  15. 146 0
      src/components/LemonUI/components/popover.vue
  16. 77 0
      src/components/LemonUI/components/tabs.vue
  17. 0 0
      src/components/LemonUI/database/contacts.js
  18. 0 0
      src/components/LemonUI/database/emoji.js
  19. 3 2
      src/components/LemonUI/database/messages.js
  20. 0 0
      src/components/LemonUI/database/user.js
  21. 83 0
      src/components/LemonUI/directives/contextmenu.js
  22. 0 0
      src/components/LemonUI/index.css
  23. 49 0
      src/components/LemonUI/index.js
  24. 15 0
      src/components/LemonUI/lastContentRender.js
  25. 13 0
      src/components/LemonUI/styles/common/animate.styl
  26. 45 0
      src/components/LemonUI/styles/common/icons.styl
  27. 3 0
      src/components/LemonUI/styles/common/index.styl
  28. 23 0
      src/components/LemonUI/styles/common/normalize.styl
  29. 二進制
      src/components/LemonUI/styles/fonts/icon.woff
  30. 67 0
      src/components/LemonUI/styles/utils/bem.styl
  31. 47 0
      src/components/LemonUI/styles/utils/functional.styl
  32. 5 0
      src/components/LemonUI/styles/utils/index.styl
  33. 26 0
      src/components/LemonUI/styles/utils/var.styl
  34. 24 0
      src/components/LemonUI/utils/cache/memory.js
  35. 16 0
      src/components/LemonUI/utils/constant.js
  36. 134 0
      src/components/LemonUI/utils/index.js
  37. 37 0
      src/components/LemonUI/utils/validate.js
  38. 2 1
      src/main.js
  39. 1 1
      src/views/company/tcmScheduleReport/add.vue
  40. 4 4
      src/views/company/tcmScheduleReport/consume.vue
  41. 8 7
      src/views/qw/qwChat/index.vue
  42. 5 5
      src/views/qw/qwChat/qq.vue
  43. 2 1
      vue.config.js

+ 2 - 1
package.json

@@ -49,7 +49,6 @@
     "js-beautify": "1.10.2",
     "js-cookie": "2.2.0",
     "jsencrypt": "3.0.0-rc.1",
-    "lemon-imui": "^1.7.7",
     "lodash.clonedeep": "^4.5.0",
     "lodash.merge": "^4.6.2",
     "moment": "^2.29.4",
@@ -86,6 +85,8 @@
     "autoprefixer": "9.5.1",
     "babel-eslint": "10.1.0",
     "babel-jest": "23.6.0",
+    "stylus": "^0.54.7",
+    "stylus-loader": "^3.0.2",
     "babel-plugin-dynamic-import-node": "2.3.3",
     "chalk": "2.4.2",
     "chokidar": "2.1.5",

+ 81 - 0
src/components/LemonUI/components/avatar.vue

@@ -0,0 +1,81 @@
+<script>
+export default {
+  name: "LemonAvatar",
+  inject: ["IMUI"],
+  props: {
+    src: String,
+    icon: {
+      type: String,
+      default: "lemon-icon-people",
+    },
+    circle: {
+      type: Boolean,
+      default() {
+        return this.IMUI ? this.IMUI.avatarCricle : false;
+      },
+    },
+    size: {
+      type: Number,
+      default: 32,
+    },
+  },
+  data() {
+    return {
+      imageFinishLoad: true,
+    };
+  },
+  render() {
+    return (
+      <span
+        style={this.style}
+        class={["lemon-avatar", { "lemon-avatar--circle": this.circle }]}
+        on-click={e => this.$emit("click", e)}
+      >
+        {(this.imageFinishLoad || !this.src) && <i class={this.icon} />}
+        <img src={this.src} onLoad={this._handleLoad} />
+      </span>
+    );
+  },
+  computed: {
+    style() {
+      const size = `${this.size}px`;
+      return {
+        width: size,
+        height: size,
+        lineHeight: size,
+        fontSize: `${this.size / 2}px`,
+      };
+    },
+  },
+  methods: {
+    _handleLoad() {
+      this.imageFinishLoad = false;
+    },
+  },
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-avatar)
+  font-variant tabular-nums
+  line-height 1.5
+  box-sizing border-box
+  margin 0
+  padding 0
+  list-style none
+  display inline-block
+  text-align center
+  background #ccc
+  color rgba(255,255,255,0.7)
+  white-space nowrap
+  position relative
+  overflow hidden
+  vertical-align middle
+  border-radius 4px
+  +m(circle)
+    border-radius 50%
+  img
+    width 100%
+    height 100%
+    display block
+</style>

+ 74 - 0
src/components/LemonUI/components/badge.vue

@@ -0,0 +1,74 @@
+<script>
+export default {
+  name: "LemonBadge",
+  props: {
+    count: [Number, Boolean],
+    overflowCount: {
+      type: Number,
+      default: 99
+    }
+  },
+  render() {
+    return (
+      <span class="lemon-badge">
+        {this.$slots.default}
+        {this.count !== 0 && this.count !== undefined && (
+          <span
+            class={[
+              "lemon-badge__label",
+              this.isDot && "lemon-badge__label--dot"
+            ]}
+          >
+            {this.label}
+          </span>
+        )}
+      </span>
+    );
+  },
+  computed: {
+    isDot() {
+      return this.count === true;
+    },
+    label() {
+      if (this.isDot) return "";
+      return this.count > this.overflowCount
+        ? `${this.overflowCount}+`
+        : this.count;
+    }
+  },
+  methods: {}
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-badge)
+  position relative
+  display inline-block
+  +e(label)
+    border-radius 10px
+    background #f5222d
+    color #fff
+    text-align center
+    font-size 12px
+    font-weight normal
+    white-space nowrap
+    box-shadow 0 0 0 1px #fff
+    z-index 10
+    position absolute
+    transform  translateX(50%)
+    transform-origin  100%
+    display inline-block
+    padding 0 4px
+    height 18px
+    line-height 17px
+    min-width 10px
+    top -4px
+    right 6px
+    +m(dot)
+      width 10px
+      height 10px
+      min-width auto
+      padding 0
+      top -3px
+      right 2px
+</style>

+ 70 - 0
src/components/LemonUI/components/button.vue

@@ -0,0 +1,70 @@
+<script>
+export default {
+  name: "LemonButton",
+  props: {
+    color: {
+      type: String,
+      default: "default"
+    },
+    disabled: Boolean
+  },
+  render() {
+    return (
+      <button
+        class={["lemon-button", `lemon-button--color-${this.color}`]}
+        disabled={this.disabled}
+        type="button"
+        on-click={this._handleClick}
+      >
+        {this.$slots.default}
+      </button>
+    );
+  },
+  methods: {
+    _handleClick(e) {
+      this.$emit("click", e);
+    }
+  }
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-button)
+  outline none
+  line-height 1.499
+  display inline-block
+  font-weight 400
+  text-align center
+  touch-action manipulation
+  cursor pointer
+  background-image none
+  border 1px solid #ddd
+  box-sizing border-box
+  white-space nowrap
+  padding 0 15px
+  font-size 14px
+  border-radius 4px
+  height 32px
+  user-select none
+  transition all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1)
+  color rgba(0, 0, 0, 0.65)
+  background-color #fff
+  box-shadow 0 2px 0 rgba(0, 0, 0, 0.015)
+  text-shadow 0 -1px 0 rgba(0, 0, 0, 0.12)
+  +m(color-default)
+    &:hover:not([disabled])
+      border-color #666
+      color #333
+    &:active
+      background-color #ddd
+    &[disabled]
+      cursor not-allowed
+      color #aaa
+      background #eee
+  +m(color-grey)
+    background #e1e1e1
+    border-color #e1e1e1
+    color #666
+    &:hover:not([disabled])
+      border-color #bbb
+</style>

+ 145 - 0
src/components/LemonUI/components/contact.vue

@@ -0,0 +1,145 @@
+<script>
+import { isString, isToday } from "../utils/validate";
+import { timeFormat, useScopedSlot } from "../utils";
+export default {
+  name: "LemonContact",
+  components: {},
+  inject: {
+    IMUI: {
+      from: "IMUI",
+      default() {
+        return this;
+      },
+    },
+  },
+  data() {
+    return {};
+  },
+  props: {
+    contact: Object,
+    simple: Boolean,
+    timeFormat: {
+      type: Function,
+      default(val) {
+        return timeFormat(val, isToday(val) ? "h:i" : "y/m/d");
+      },
+    },
+  },
+  render() {
+    return (
+      <div
+        class={["lemon-contact", { "lemon-contact--name-center": this.simple }]}
+        title={this.contact.displayName}
+        on-click={e => this._handleClick(e, this.contact)}
+      >
+        {useScopedSlot(
+          this.$scopedSlots.default,
+          this._renderInner(),
+          this.contact,
+        )}
+      </div>
+    );
+  },
+  created() {},
+  mounted() {},
+  computed: {},
+  watch: {},
+  methods: {
+    _renderInner() {
+      const { contact } = this;
+      return [
+        <lemon-badge
+          count={!this.simple ? contact.unread : 0}
+          class="lemon-contact__avatar"
+        >
+          <lemon-avatar size={40} src={contact.avatar} />
+        </lemon-badge>,
+        <div class="lemon-contact__inner">
+          <p class="lemon-contact__label">
+            <span class="lemon-contact__name">{contact.displayName}</span>
+            {!this.simple && (
+              <span class="lemon-contact__time">
+                {this.timeFormat(contact.lastSendTime)}
+              </span>
+            )}
+          </p>
+          {!this.simple && (
+            <p class="lemon-contact__content">
+              {isString(contact.lastContent) ? (
+                <span domProps={{ innerHTML: contact.lastContent }} />
+              ) : (
+                contact.lastContent
+              )}
+            </p>
+          )}
+        </div>,
+      ];
+    },
+    _handleClick(e, data) {
+      this.$emit("click", data);
+    },
+  },
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-contact)
+  padding 10px 14px
+  cursor pointer
+  user-select none
+  box-sizing border-box
+  overflow hidden
+  background #efefef
+  text-align left
+  p
+    margin 0
+  +m(active)
+    background #bebdbd
+  &:hover:not(.lemon-contact--active)
+    background #e3e3e3
+    .el-badge__content
+      border-color #ddd
+  +e(avatar)
+    float left
+    margin-right 10px
+    img
+      display block
+    .ant-badge-count
+      display inline-block
+      padding 0 4px
+      height 18px
+      line-height 18px
+      min-width 18px
+      top -4px
+      right 7px
+  +e(label)
+    display flex
+  +e(time)
+    font-size 12px
+    line-height 18px
+    padding-left 6px
+    color #999
+    white-space nowrap
+  +e(name)
+    display block
+    width 100%
+    ellipsis()
+  +e(content)
+    font-size 12px
+    color #999
+    height 18px
+    line-height 18px
+    margin-top 1px !important
+    ellipsis()
+    img
+      height 14px
+      display inline-block
+      vertical-align middle
+      margin 0 1px
+      position relative
+      top -1px
+  +m(name-center)
+    +e(label)
+      padding-bottom 0
+      line-height 38px
+</style>

+ 487 - 0
src/components/LemonUI/components/editor.vue

@@ -0,0 +1,487 @@
+<script>
+import { useScopedSlot, messageToHtml, clearHtmlExcludeImg } from "../utils";
+const command = (command, val) => {
+  document.execCommand(command, false, val);
+};
+const selection = window.getSelection();
+let range;
+let emojiData = [];
+let isInitTool = false;
+export default {
+  name: "LemonEditor",
+  inject: {
+    IMUI: {
+      from: "IMUI",
+      default() {
+        return this;
+      },
+    },
+  },
+  components: {},
+  props: {
+    tools: {
+      type: Array,
+      default: () => [],
+    },
+    sendText: {
+      type: String,
+      default: "发 送",
+    },
+    wrapKey: {
+      type: Function,
+      default: function(e) {
+        return e.keyCode == 13 && e.ctrlKey == false && e.shiftKey == false;
+      },
+    },
+    sendKey: {
+      type: Function,
+      default(e) {
+        return e.keyCode == 13 && e.ctrlKey === true;
+      },
+    },
+  },
+  data() {
+    this.clipboardBlob = null;
+    return {
+      //剪切板图片URL
+      clipboardUrl: "",
+      submitDisabled: true,
+      //proxyTools: [],
+      accept: "",
+    };
+  },
+  created() {
+    this.IMUI.$on("change-contact", () => {
+      this.closeClipboardImage();
+    });
+  },
+  render() {
+    const toolLeft = [];
+    const toolRight = [];
+    this.proxyTools.forEach(({ name, title, render, click, isRight }) => {
+      click = click || new Function();
+      const classes = [
+        "lemon-editor__tool-item",
+        { "lemon-editor__tool-item--right": isRight },
+      ];
+      let node;
+      if (name == "emoji") {
+        node =
+          emojiData.length == 0 ? (
+            ""
+          ) : (
+            <lemon-popover class="lemon-editor__emoji">
+              <template slot="content">{this._renderEmojiTabs()}</template>
+              <div class={classes} title={title}>
+                {render()}
+              </div>
+            </lemon-popover>
+          );
+      } else {
+        node = (
+          <div class={classes} on-click={click} title={title}>
+            {render()}
+          </div>
+        );
+      }
+      if (isRight) {
+        toolRight.push(node);
+      } else {
+        toolLeft.push(node);
+      }
+    });
+
+    return (
+      <div class="lemon-editor">
+        {this.clipboardUrl && (
+          <div class="lemon-editor__clipboard-image">
+            <img src={this.clipboardUrl} />
+            <div>
+              <lemon-button
+                style={{ marginRight: "10px" }}
+                on-click={this.closeClipboardImage}
+                color="grey"
+              >
+                取消
+              </lemon-button>
+              <lemon-button on-click={this.sendClipboardImage}>
+                发送图片
+              </lemon-button>
+            </div>
+          </div>
+        )}
+        <input
+          style="display:none"
+          type="file"
+          multiple="multiple"
+          ref="fileInput"
+          accept={this.accept}
+          onChange={this._handleChangeFile}
+        />
+        <div class="lemon-editor__tool">
+          <div class="lemon-editor__tool-left">{toolLeft}</div>
+          <div class="lemon-editor__tool-right">{toolRight}</div>
+        </div>
+        <div class="lemon-editor__inner">
+          <div
+            class="lemon-editor__input"
+            ref="textarea"
+            contenteditable="true"
+            on-keyup={this._handleKeyup}
+            on-keydown={this._handleKeydown}
+            on-paste={this._handlePaste}
+            on-click={this._handleClick}
+            spellcheck="false"
+          />
+        </div>
+        <div class="lemon-editor__footer">
+          <div class="lemon-editor__tip">
+            {useScopedSlot(
+              this.IMUI.$scopedSlots["editor-footer"],
+              "使用 ctrl + enter 快捷发送消息",
+            )}
+          </div>
+          <div class="lemon-editor__submit">
+            <lemon-button
+              disabled={this.submitDisabled}
+              on-click={this._handleSend}
+            >
+              {this.sendText}
+            </lemon-button>
+          </div>
+        </div>
+      </div>
+    );
+  },
+  computed: {
+    proxyTools() {
+      if (!this.tools) return [];
+      const defaultTools = [
+        {
+          name: "emoji",
+          title: "表情",
+          click: null,
+          render: menu => {
+            return <i class="lemon-icon-emoji" />;
+          },
+        },
+        {
+          name: "uploadFile",
+          title: "文件上传",
+          click: () => this.selectFile("*"),
+          render: menu => {
+            return <i class="lemon-icon-folder" />;
+          },
+        },
+        {
+          name: "uploadImage",
+          title: "图片上传",
+          click: () => this.selectFile("image/*"),
+          render: menu => {
+            return <i class="lemon-icon-image" />;
+          },
+        },
+      ];
+      let tools = [];
+      if (Array.isArray(this.tools)) {
+        const indexMap = {
+          emoji: 0,
+          uploadFile: 1,
+          uploadImage: 2,
+        };
+        const indexKeys = Object.keys(indexMap);
+        tools = this.tools.map(item => {
+          if (indexKeys.includes(item.name)) {
+            return {
+              ...defaultTools[indexMap[item.name]],
+              ...item,
+            };
+          }
+          return item;
+        });
+      } else {
+        tools = defaultTools;
+      }
+      return tools;
+    },
+  },
+  methods: {
+    closeClipboardImage() {
+      this.clipboardUrl = "";
+      this.clipboardBlob = null;
+    },
+    sendClipboardImage() {
+      if (!this.clipboardBlob) return;
+      this.$emit("upload", this.clipboardBlob);
+      this.closeClipboardImage();
+    },
+    saveRangeToLast() {
+      if (!range) {
+        range = document.createRange();
+      }
+      range.selectNodeContents(textarea.value);
+      range.collapse(false);
+      selection.removeAllRanges();
+      selection.addRange(range);
+    },
+    inertContent(val, toLast = false) {
+      if (toLast) saveRangeToLast();
+      this.focusRange();
+      command("insertHTML", val);
+      this.saveRange();
+    },
+    saveRange() {
+      range = selection.getRangeAt(0);
+    },
+    focusRange() {
+      this.$refs.textarea.focus();
+      if (range) {
+        selection.removeAllRanges();
+        selection.addRange(range);
+      }
+    },
+    _handleClick() {
+      this.saveRange();
+    },
+    _renderEmojiTabs() {
+      const renderImageGrid = items => {
+        return items.map(item => (
+          <img
+            src={item.src}
+            title={item.title}
+            class="lemon-editor__emoji-item"
+            on-click={() => this._handleSelectEmoji(item)}
+          />
+        ));
+      };
+      if (emojiData[0].label) {
+        const nodes = emojiData.map((item, index) => {
+          return (
+            <div slot="tab-pane" index={index} tab={item.label}>
+              {renderImageGrid(item.children)}
+            </div>
+          );
+        });
+        return <lemon-tabs style="width: 412px">{nodes}</lemon-tabs>;
+      } else {
+        return (
+          <div class="lemon-tabs-content" style="width:406px">
+            {renderImageGrid(emojiData)}
+          </div>
+        );
+      }
+    },
+    _handleSelectEmoji(item) {
+      this.inertContent(
+        `<img emoji-name="${item.name}" src="${item.src}"></img>`,
+      );
+      this._checkSubmitDisabled();
+    },
+    async selectFile(accept) {
+      this.accept = accept;
+      await this.$nextTick();
+      this.$refs.fileInput.click();
+    },
+    _handlePaste(e) {
+      e.preventDefault();
+      const clipboardData = e.clipboardData || window.clipboardData;
+      const text = clipboardData.getData("Text");
+      if (text) {
+        this.submitDisabled = false;
+        if (window.clipboardData) {
+          this.$refs.textarea.innerHTML = text;
+        } else {
+          command("insertText", text);
+        }
+      } else {
+        const { blob, blobUrl } = this._getClipboardBlob(clipboardData);
+        this.clipboardBlob = blob;
+        this.clipboardUrl = blobUrl;
+      }
+    },
+    _getClipboardBlob(clipboard) {
+      let blob, blobUrl;
+      for (var i = 0; i < clipboard.items.length; ++i) {
+        if (
+          clipboard.items[i].kind == "file" &&
+          clipboard.items[i].type.indexOf("image/") !== -1
+        ) {
+          blob = clipboard.items[i].getAsFile();
+          blobUrl = (window.URL || window.webkitURL).createObjectURL(blob);
+        }
+      }
+      return { blob, blobUrl };
+    },
+    _handleKeyup(e) {
+      this.saveRange();
+      this._checkSubmitDisabled();
+    },
+    _handleKeydown(e) {
+      if (e.keyCode == 13 || (e.keyCode == 13 && e.shiftKey)) {
+        e.preventDefault();
+      }
+      if (this.wrapKey(e)) {
+        e.preventDefault();
+        command("insertLineBreak");
+      }
+      if (this.submitDisabled == false && this.sendKey(e)) {
+        this._handleSend();
+      }
+    },
+    getFormatValue() {
+      // return toEmojiName(
+      //   this.$refs.textarea.innerHTML
+      //     .replace(/<br>|<\/br>/, "")
+      //     .replace(/<div>|<p>/g, "\r\n")
+      //     .replace(/<\/div>|<\/p>/g, "")
+      // );
+      return this.IMUI.emojiImageToName(this.$refs.textarea.innerHTML);
+    },
+    _checkSubmitDisabled() {
+      this.submitDisabled = !clearHtmlExcludeImg(
+        this.$refs.textarea.innerHTML.trim(),
+      );
+    },
+    _handleSend(e) {
+      const text = this.getFormatValue();
+      this.$emit("send", text);
+      this.clear();
+      this._checkSubmitDisabled();
+    },
+    _handleChangeFile(e) {
+      const { fileInput } = this.$refs;
+      Array.from(fileInput.files).forEach(file => {
+        this.$emit("upload", file);
+      });
+      fileInput.value = "";
+    },
+    clear() {
+      this.$refs.textarea.innerHTML = "";
+    },
+    initEmoji(data) {
+      emojiData = data;
+      this.$forceUpdate();
+    },
+    setValue(val) {
+      this.$refs.textarea.innerHTML = this.IMUI.emojiNameToImage(val);
+      this._checkSubmitDisabled();
+    },
+  },
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
+gap = 10px;
++b(lemon-editor)
+  height 200px
+  position relative
+  flex-column()
+  +e(tool)
+    display flex
+    height 40px
+    align-items center
+    justify-content space-between
+    padding 0 5px
+  +e(tool-left){
+    display flex
+  }
+  +e(tool-right){
+    display flex
+  }
+  +e(tool-item)
+    cursor pointer
+    padding 4px gap
+    height 28px
+    line-height 24px;
+    color #999
+    transition all ease .3s
+    font-size 12px
+    [class^='lemon-icon-']
+      line-height 26px
+      font-size 22px
+    &:hover
+      color #333
+    +m(right){
+      margin-left:auto;
+    }
+  +e(inner)
+    flex 1
+    overflow-x hidden
+    overflow-y auto
+    scrollbar-light()
+  +e(clipboard-image)
+    position absolute
+    top 0
+    left 0
+    width 100%
+    height 100%
+    flex-column()
+    justify-content center
+    align-items center
+    background #f4f4f4
+    z-index 1
+    img
+      max-height 66%
+      max-width 80%
+      background #e9e9e9
+      //box-shadow 0 0 20px rgba(0,0,0,0.15)
+      user-select none
+      cursor pointer
+      border-radius 4px
+      margin-bottom 10px
+      border 3px dashed #ddd !important
+      box-sizing border-box
+    .clipboard-popover-title
+      font-size 14px
+      color #333
+  +e(input)
+    height 100%
+    box-sizing border-box
+    border none
+    outline none
+    padding 0 gap
+    scrollbar-light()
+    p,div
+      margin 0
+    img
+      height 20px
+      padding 0 2px
+      pointer-events none
+      position relative
+      top -1px
+      vertical-align middle
+  +e(footer)
+    display flex
+    height 52px
+    justify-content flex-end
+    padding 0 gap
+    align-items center
+  +e(tip)
+    margin-right 10px
+    font-size 12px
+    color #999
+    user-select none
+  +e(emoji)
+    user-select none
+    .lemon-popover
+      background #f6f6f6
+    .lemon-popover__content
+      padding 0
+    .lemon-popover__arrow
+      background #f6f6f6
+    .lemon-tabs-content
+      box-sizing border-box
+      padding 8px
+      height 200px
+      overflow-x hidden
+      overflow-y auto
+      scrollbar-light()
+      margin-bottom 8px
+  +e(emoji-item)
+    cursor pointer
+    width 22px
+    padding 4px
+    border-radius 4px
+    &:hover
+      background #e9e9e9
+</style>

+ 1251 - 0
src/components/LemonUI/components/index.vue

@@ -0,0 +1,1251 @@
+<script>
+import {
+  useScopedSlot,
+  funCall,
+  generateUUID,
+  clearHtmlExcludeImg,
+} from "../utils";
+import { isFunction, isString, isEmpty } from "../utils/validate";
+import contextmenu from "../directives/contextmenu";
+import {
+  DEFAULT_MENUS,
+  DEFAULT_MENU_LASTMESSAGES,
+  DEFAULT_MENU_CONTACTS,
+} from "../utils/constant";
+import lastContentRender from "../lastContentRender";
+
+import MemoryCache from "../utils/cache/memory";
+
+let allMessages = {};
+const emojiMap = {};
+const toPx = val => {
+  return isString(val) ? val : `${val}px`;
+};
+const toPoint = str => {
+  return str.replace("%", "") / 100;
+};
+
+let renderDrawerContent = () => {};
+
+export default {
+  name: "LemonImui",
+  provide() {
+    return {
+      IMUI: this,
+    };
+  },
+  props: {
+    width: {
+      type: [String, Number],
+      default: 850,
+    },
+    height: {
+      type: [String, Number],
+      default: 580,
+    },
+    theme: {
+      type: String,
+      default: "default",
+    },
+    simple: {
+      type: Boolean,
+      default: false,
+    },
+    loadingText: [String, Function],
+    loadendText: [String, Function],
+    /**
+     * 消息时间格式化规则
+     */
+    messageTimeFormat: Function,
+    /**
+     * 联系人最新消息时间格式化规则
+     */
+    contactTimeFormat: Function,
+    /**
+     * 初始化时是否隐藏抽屉
+     */
+    hideDrawer: {
+      type: Boolean,
+      default: true,
+    },
+    /**
+     * 是否隐藏导航按钮上的头像
+     */
+    hideMenuAvatar: Boolean,
+    hideMenu: Boolean,
+    /**
+     * 是否隐藏消息列表内的联系人名字
+     */
+    hideMessageName: Boolean,
+    /**
+     * 是否隐藏消息列表内的发送时间
+     */
+    hideMessageTime: Boolean,
+    sendKey: Function,
+    wrapKey: Function,
+    sendText: String,
+    contextmenu: Array,
+    contactContextmenu: Array,
+    avatarCricle: Boolean,
+    user: {
+      type: Object,
+      default: () => {
+        return {};
+      },
+    },
+  },
+  data() {
+    this.CacheContactContainer = new MemoryCache();
+    this.CacheMenuContainer = new MemoryCache();
+    this.CacheMessageLoaded = new MemoryCache();
+    this.CacheDraft = new MemoryCache();
+    return {
+      drawerVisible: !this.hideDrawer,
+      currentContactId: null,
+      currentMessages: [],
+      activeSidebar: DEFAULT_MENU_LASTMESSAGES,
+      contacts: [],
+      menus: [],
+      editorTools: [
+        { name: "emoji" },
+        { name: "uploadFile" },
+        { name: "uploadImage" },
+      ],
+    };
+  },
+
+  render() {
+    return this._renderWrapper([
+      this._renderMenu(),
+      this._renderSidebarMessage(),
+      this._renderSidebarContact(),
+      this._renderContainer(),
+      this._renderDrawer(),
+    ]);
+  },
+  created() {
+    this.initMenus();
+  },
+  async mounted() {
+    await this.$nextTick();
+  },
+  computed: {
+    currentContact() {
+      return this.contacts.find(item => item.id == this.currentContactId) || {};
+    },
+    currentMenu() {
+      return this.menus.find(item => item.name == this.activeSidebar) || {};
+    },
+    currentIsDefSidebar() {
+      return DEFAULT_MENUS.includes(this.activeSidebar);
+    },
+    lastMessages() {
+      const data = this.contacts.filter(item => !isEmpty(item.lastContent));
+      data.sort((a1, a2) => {
+        return a2.lastSendTime - a1.lastSendTime;
+      });
+      return data;
+    },
+  },
+  watch: {
+    activeSidebar() {},
+  },
+  methods: {
+    _menuIsContacts() {
+      return this.activeSidebar == DEFAULT_MENU_CONTACTS;
+    },
+    _menuIsMessages() {
+      return this.activeSidebar == DEFAULT_MENU_LASTMESSAGES;
+    },
+    _createMessage(message) {
+      return {
+        ...{
+          id: generateUUID(),
+          type: "text",
+          status: "going",
+          sendTime: new Date().getTime(),
+          toContactId: this.currentContactId,
+          fromUser: {
+            ...this.user,
+          },
+        },
+        ...message,
+      };
+    },
+    /**
+     * 新增一条消息
+     */
+    appendMessage(message, scrollToBottom = false) {
+      let unread = "+1";
+      let messageList = allMessages[message.toContactId];
+      // 如果是自己的消息需要push,发送的消息不再增加未读条数
+      if (message.type == 'event' || this.user.id == message.fromUser.id) unread = "+0";
+      if (messageList === undefined) {
+        this.updateContact({
+          id: message.toContactId,
+          unread: unread,
+          lastSendTime: message.sendTime,
+          lastContent: this.lastContentRender(message),
+        });
+      } else {
+        // 如果消息存在则不再添加
+        let hasMsg = messageList.some(({id})=>id == message.id);
+        if (hasMsg) return;
+        this._addMessage(message, message.toContactId, 1);
+        const updateContact = {
+          id: message.toContactId,
+          lastContent: this.lastContentRender(message),
+          lastSendTime: message.sendTime,
+        };
+        if (message.toContactId == this.currentContactId) {
+          if (scrollToBottom == true) {
+            this.messageViewToBottom();
+          }
+          this.CacheDraft.remove(message.toContactId);
+        } else {
+          updateContact.unread = unread;
+        }
+        this.updateContact(updateContact);
+      }
+    },
+    _emitSend(message, next, file) {
+      this.$emit(
+        "send",
+        message,
+        (replaceMessage = { status: "succeed" }) => {
+          next();
+          this.updateMessage(Object.assign(message, replaceMessage));
+        },
+        file,
+      );
+    },
+    _handleSend(text) {
+      const message = this._createMessage({ content: text });
+      this.appendMessage(message, true);
+      this._emitSend(message, () => {
+        this.updateContact({
+          id: message.toContactId,
+          lastContent: this.lastContentRender(message),
+          lastSendTime: message.sendTime,
+        });
+        this.CacheDraft.remove(message.toContactId);
+      });
+    },
+    _handleUpload(file) {
+      const imageTypes = ["image/gif", "image/jpeg", "image/png"];
+      let joinMessage;
+      if (imageTypes.includes(file.type)) {
+        joinMessage = {
+          type: "image",
+          content: URL.createObjectURL(file),
+        };
+      } else {
+        joinMessage = {
+          type: "file",
+          fileSize: file.size,
+          fileName: file.name,
+          content: "",
+        };
+      }
+      const message = this._createMessage(joinMessage);
+      this.appendMessage(message, true);
+      this._emitSend(
+        message,
+        () => {
+          this.updateContact({
+            id: message.toContactId,
+            lastContent: this.lastContentRender(message),
+            lastSendTime: message.sendTime,
+          });
+        },
+        file,
+      );
+    },
+    _emitPullMessages(next) {
+      this._changeContactLock = true;
+      this.$emit(
+        "pull-messages",
+        this.currentContact,
+        (messages = [], isEnd = false) => {
+          this._addMessage(messages, this.currentContactId, 0);
+          this.CacheMessageLoaded.set(this.currentContactId, isEnd);
+          if (isEnd == true) this.$refs.messages.loaded();
+          this.updateCurrentMessages();
+          this._changeContactLock = false;
+          next(isEnd);
+        },
+        this,
+      );
+    },
+    clearCacheContainer(name) {
+      this.CacheContactContainer.remove(name);
+      this.CacheMenuContainer.remove(name);
+    },
+    _renderWrapper(children) {
+      return (
+        <div
+          style={{
+            width: toPx(this.width),
+            height: toPx(this.height),
+          }}
+          ref="wrapper"
+          class={[
+            "lemon-wrapper",
+            `lemon-wrapper--theme-${this.theme}`,
+            { "lemon-wrapper--simple": this.simple },
+            this.drawerVisible && "lemon-wrapper--drawer-show",
+          ]}
+        >
+          {children}
+        </div>
+      );
+    },
+    _renderMenu() {
+      const menuItem = this._renderMenuItem();
+      return (
+        <div class="lemon-menu" v-show={!this.hideMenu}>
+          {
+            <lemon-avatar
+              v-show={!this.hideMenuAvatar}
+              on-click={e => {
+                this.$emit("menu-avatar-click", e);
+              }}
+              class="lemon-menu__avatar"
+              src={this.user.avatar}
+            />
+          }
+          {menuItem.top}
+          {this.$slots.menu}
+          <div class="lemon-menu__bottom">
+            {this.$slots["menu-bottom"]}
+            {menuItem.bottom}
+          </div>
+        </div>
+      );
+    },
+    _renderMenuAvatar() {
+      return;
+    },
+    _renderMenuItem() {
+      const top = [];
+      const bottom = [];
+      this.menus.forEach(item => {
+        const { name, title, unread, render, click } = item;
+        const node = (
+          <div
+            class={[
+              "lemon-menu__item",
+              { "lemon-menu__item--active": this.activeSidebar == name },
+            ]}
+            on-click={() => {
+              funCall(click, () => {
+                if (name) this.changeMenu(name);
+              });
+            }}
+            title={title}
+          >
+            <lemon-badge count={unread}>{render(item)}</lemon-badge>
+          </div>
+        );
+        item.isBottom === true ? bottom.push(node) : top.push(node);
+      });
+      return {
+        top,
+        bottom,
+      };
+    },
+    _renderSidebarMessage() {
+      return this._renderSidebar(
+        [
+          useScopedSlot(this.$scopedSlots["sidebar-message-top"], null, this),
+          this.lastMessages.map(contact => {
+            return this._renderContact(
+              {
+                contact,
+                timeFormat: this.contactTimeFormat,
+              },
+              () => this.changeContact(contact.id),
+              this.$scopedSlots["sidebar-message"],
+            );
+          }),
+        ],
+        DEFAULT_MENU_LASTMESSAGES,
+        useScopedSlot(
+          this.$scopedSlots["sidebar-message-fixedtop"],
+          null,
+          this,
+        ),
+      );
+    },
+    _renderContact(props, onClick, slot) {
+      const {
+        click: customClick,
+        renderContainer,
+        id: contactId,
+      } = props.contact;
+      const click = () => {
+        funCall(customClick, () => {
+          onClick();
+          this._customContainerReady(
+            renderContainer,
+            this.CacheContactContainer,
+            contactId,
+          );
+        });
+      };
+
+      return (
+        <lemon-contact
+          class={{
+            "lemon-contact--active": this.currentContactId == props.contact.id,
+          }}
+          v-lemon-contextmenu_contact={this.contactContextmenu}
+          props={props}
+          on-click={click}
+          scopedSlots={{ default: slot }}
+        />
+      );
+    },
+    _renderSidebarContact() {
+      let prevIndex;
+      return this._renderSidebar(
+        [
+          useScopedSlot(this.$scopedSlots["sidebar-contact-top"], null, this),
+          this.contacts.map(contact => {
+            if (!contact.index) return;
+            contact.index = contact.index.replace(/\[[0-9]*\]/, "");
+            const node = [
+              contact.index !== prevIndex && (
+                <p class="lemon-sidebar__label">{contact.index}</p>
+              ),
+              this._renderContact(
+                {
+                  contact: contact,
+                  simple: true,
+                },
+                () => {
+                  this.changeContact(contact.id);
+                },
+                this.$scopedSlots["sidebar-contact"],
+              ),
+            ];
+            prevIndex = contact.index;
+            return node;
+          }),
+        ],
+        DEFAULT_MENU_CONTACTS,
+        useScopedSlot(
+          this.$scopedSlots["sidebar-contact-fixedtop"],
+          null,
+          this,
+        ),
+      );
+    },
+    _renderSidebar(children, name, fixedtop) {
+      return (
+        <div
+          class="lemon-sidebar"
+          v-show={this.activeSidebar == name}
+          on-scroll={this._handleSidebarScroll}
+        >
+          <div class="lemon-sidebar__fixed-top">{fixedtop}</div>
+          <div class="lemon-sidebar__scroll">{children}</div>
+        </div>
+      );
+    },
+    _renderDrawer() {
+      return this._menuIsMessages() && this.currentContactId ? (
+        <div class="lemon-drawer" ref="drawer">
+          {renderDrawerContent(this.currentContact)}
+          {useScopedSlot(this.$scopedSlots.drawer, "", this.currentContact)}
+        </div>
+      ) : (
+        ""
+      );
+    },
+    _isContactContainerCache(name) {
+      return name.startsWith("contact#");
+    },
+    _renderContainer() {
+      const nodes = [];
+      const cls = "lemon-container";
+      const curact = this.currentContact;
+      let defIsShow = true;
+      for (const name in this.CacheContactContainer.get()) {
+        const show = curact.id == name && this.currentIsDefSidebar;
+        if(show)defIsShow = !show;
+        nodes.push(
+          <div class={cls} v-show={show}>
+            {this.CacheContactContainer.get(name)}
+          </div>,
+        );
+      }
+      for (const name in this.CacheMenuContainer.get()) {
+        nodes.push(
+          <div
+            class={cls}
+            v-show={this.activeSidebar == name && !this.currentIsDefSidebar}
+          >
+            {this.CacheMenuContainer.get(name)}
+          </div>,
+        );
+      }
+
+      nodes.push(
+        <div
+          class={cls}
+          v-show={this._menuIsMessages() && defIsShow && curact.id}
+        >
+          <div class="lemon-container__title">
+            {useScopedSlot(
+              this.$scopedSlots["message-title"],
+              <div class="lemon-container__displayname">
+                {curact.displayName}
+              </div>,
+              curact,
+            )}
+          </div>
+          <div class="lemon-vessel">
+            <div class="lemon-vessel__left">
+              <lemon-messages
+                ref="messages"
+                loading-text={this.loadingText}
+                loadend-text={this.loadendText}
+                hide-time={this.hideMessageTime}
+                hide-name={this.hideMessageName}
+                time-format={this.messageTimeFormat}
+                reverse-user-id={this.user.id}
+                on-reach-top={this._emitPullMessages}
+                messages={this.currentMessages}
+              />
+              <lemon-editor
+                ref="editor"
+                tools={this.editorTools}
+                sendText={this.sendText}
+                sendKey={this.sendKey}
+                wrapKey={this.wrapKey}
+                onSend={this._handleSend}
+                onUpload={this._handleUpload}
+              />
+            </div>
+            <div class="lemon-vessel__right">
+              {useScopedSlot(this.$scopedSlots["message-side"], null, curact)}
+            </div>
+          </div>
+        </div>,
+      );
+      nodes.push(
+        <div class={cls} v-show={!curact.id && this.currentIsDefSidebar}>
+          {this.$slots.cover}
+        </div>,
+      );
+      nodes.push(
+        <div
+          class={cls}
+          v-show={this._menuIsContacts() && defIsShow && curact.id}
+        >
+          {useScopedSlot(
+            this.$scopedSlots["contact-info"],
+            <div class="lemon-contact-info">
+              <lemon-avatar src={curact.avatar} size={90} />
+              <h4>{curact.displayName}</h4>
+              <lemon-button
+                on-click={() => {
+                  if (isEmpty(curact.lastContent)) {
+                    this.updateContact({
+                      id: curact.id,
+                      lastContent: " ",
+                    });
+                  }
+                  this.changeContact(curact.id, DEFAULT_MENU_LASTMESSAGES);
+                }}
+              >
+                发送消息
+              </lemon-button>
+            </div>,
+            curact,
+          )}
+        </div>,
+      );
+      return nodes;
+    },
+    _handleSidebarScroll() {
+      contextmenu.hide();
+    },
+    _addContact(data, t) {
+      const type = {
+        0: "unshift",
+        1: "push",
+      }[t];
+      this.contacts[type](data);
+    },
+    _addMessage(data, contactId, t) {
+      const type = {
+        0: "unshift",
+        1: "push",
+      }[t];
+      if (!Array.isArray(data)) data = [data];
+      allMessages[contactId] = allMessages[contactId] || [];
+      allMessages[contactId][type](...data);
+    },
+    /**
+     * 设置最新消息DOM
+     * @param {String} messageType 消息类型
+     * @param {Function} render 返回消息 vnode
+     */
+    setLastContentRender(messageType, render) {
+      lastContentRender[messageType] = render;
+    },
+    lastContentRender(message) {
+      if (!isFunction(lastContentRender[message.type])) {
+        console.error(
+          `not found '${
+            message.type
+          }' of the latest message renderer,try to use ‘setLastContentRender()’`,
+        );
+        return "";
+      }
+      return lastContentRender[message.type].call(this, message);
+    },
+    /**
+     * 将字符串内的 EmojiItem.name 替换为 img
+     * @param {String} str 被替换的字符串
+     * @return {String} 替换后的字符串
+     */
+    emojiNameToImage(str) {
+      return str.replace(/\[!(\w+)\]/gi, (str, match) => {
+        const file = match;
+        return emojiMap[file]
+          ? `<img emoji-name="${match}" src="${emojiMap[file]}" />`
+          : `[!${match}]`;
+      });
+    },
+    emojiImageToName(str) {
+      return str.replace(/<img emoji-name=\"([^\"]*?)\" [^>]*>/gi, "[!$1]");
+    },
+    updateCurrentMessages() {
+      if (!allMessages[this.currentContactId])
+        allMessages[this.currentContactId] = [];
+      this.currentMessages = allMessages[this.currentContactId];
+    },
+    /**
+     * 将当前聊天窗口滚动到底部
+     */
+    messageViewToBottom() {
+      this.$refs.messages.scrollToBottom();
+    },
+    /**
+     * 设置联系人的草稿信息
+     */
+    setDraft(cid, editorValue) {
+      if (isEmpty(cid) || isEmpty(editorValue)) return false;
+      const contact = this.findContact(cid);
+      let lastContent = contact.lastContent;
+      if (isEmpty(contact)) return false;
+      if (this.CacheDraft.has(cid)) {
+        lastContent = this.CacheDraft.get(cid).lastContent;
+      }
+      this.CacheDraft.set(cid, {
+        editorValue,
+        lastContent,
+      });
+      this.updateContact({
+        id: cid,
+        lastContent: `<span style="color:red;">[草稿]</span><span>${this.lastContentRender(
+          { type: "text", content: editorValue },
+        )}</span>`,
+      });
+    },
+    /**
+     * 清空联系人草稿信息
+     */
+    clearDraft(contactId) {
+      const draft = this.CacheDraft.get(contactId);
+      if (draft) {
+        const currentContent = this.findContact(contactId).lastContent;
+        if (
+          currentContent.indexOf('<span style="color:red;">[草稿]</span>') === 0
+        ) {
+          this.updateContact({
+            id: contactId,
+            lastContent: draft.lastContent,
+          });
+        }
+        this.CacheDraft.remove(contactId);
+      }
+    },
+    /**
+     * 改变聊天对象
+     * @param contactId 联系人 id
+     */
+    async changeContact(contactId, menuName) {
+      if (menuName) {
+        this.changeMenu(menuName);
+      } else {
+        if (this._changeContactLock || this.currentContactId == contactId)
+          return false;
+      }
+
+      //保存上个聊天目标的草稿
+      if (this.currentContactId) {
+        const editorValue = clearHtmlExcludeImg(this.getEditorValue()).trim();
+        if (editorValue) {
+          this.setDraft(this.currentContactId, editorValue);
+          this.setEditorValue();
+        } else {
+          this.clearDraft(this.currentContactId);
+        }
+      }
+
+      this.currentContactId = contactId;
+      if (!this.currentContactId) return false;
+
+      this.$emit("change-contact", this.currentContact, this);
+      if (
+        isFunction(this.currentContact.renderContainer) ||
+        this.activeSidebar == DEFAULT_MENU_CONTACTS
+      ) {
+        return;
+      }
+      //填充草稿内容
+      const draft = this.CacheDraft.get(contactId);
+      if (draft) this.setEditorValue(draft.editorValue);
+
+      if (this.CacheMessageLoaded.has(contactId)) {
+        this.$refs.messages.loaded();
+      } else {
+        this.$refs.messages.resetLoadState();
+      }
+
+      if (!allMessages[contactId]) {
+        this.updateCurrentMessages();
+        this._emitPullMessages(isEnd => {
+          this.messageViewToBottom();
+        });
+      } else {
+        setTimeout(() => {
+          this.updateCurrentMessages();
+          this.messageViewToBottom();
+        }, 0);
+      }
+    },
+    /**
+     * 删除一条聊天消息
+     * @param messageId 消息 id
+     * @param contactId 联系人 id
+     */
+    removeMessage(messageId) {
+      const message = this.findMessage(messageId);
+      if (!message) return false;
+      const index = allMessages[message.toContactId].findIndex(
+        ({ id }) => id == messageId,
+      );
+      allMessages[message.toContactId].splice(index, 1);
+      return true;
+    },
+    /**
+     * 修改聊天一条聊天消息
+     * @param {Message} data 根据 data.id 查找聊天消息并覆盖传入的值
+     * @param contactId 联系人 id
+     */
+    updateMessage(message) {
+      if (!message.id) return false;
+      let historyMessage = this.findMessage(message.id);
+      if (!historyMessage) return false;
+      historyMessage = Object.assign(historyMessage, message, {
+        toContactId: historyMessage.toContactId,
+      });
+      return true;
+    },
+    /**
+     * 手动更新对话消息
+     * @param {String} messageId 消息ID,如果为空则更新当前聊天窗口的所有消息
+     */
+    forceUpdateMessage(messageId) {
+      if (!messageId) {
+        this.$refs.messages.$forceUpdate();
+      } else {
+        const components = this.$refs.messages.$refs.message;
+        if (components) {
+          const messageComponent = components.find(
+            com => com.$attrs.message.id == messageId,
+          );
+          if (messageComponent) messageComponent.$forceUpdate();
+        }
+      }
+    },
+    _customContainerReady(render, cacheDrive, key) {
+      if (isFunction(render) && !cacheDrive.has(key)) {
+        cacheDrive.set(key, render.call(this));
+      }
+    },
+    /**
+     * 切换左侧按钮
+     * @param {String} name 按钮 name
+     */
+    changeMenu(name) {
+      this.$emit("change-menu", name);
+      this.activeSidebar = name;
+    },
+    /**
+     * 初始化编辑框的 Emoji 表情列表,是 Lemon-editor.initEmoji 的代理方法
+     * @param {Array<Emoji,EmojiItem>} data emoji 数据
+     * Emoji = {label: 表情,children: [{name: wx,title: 微笑,src: url}]} 分组
+     * EmojiItem = {name: wx,title: 微笑,src: url} 无分组
+     */
+    initEmoji(data) {
+      let flatData = [];
+      this.$refs.editor.initEmoji(data);
+      if (data[0].label) {
+        data.forEach(item => {
+          flatData.push(...item.children);
+        });
+      } else {
+        flatData = data;
+      }
+      flatData.forEach(({ name, src }) => (emojiMap[name] = src));
+    },
+    initEditorTools(data) {
+      //this.editorTools = data;
+      this.editorTools = data;
+      //this.$refs.editor.initTools(data);
+    },
+    /**
+     * 初始化左侧按钮
+     * @param {Array<Menu>} data 按钮数据
+     */
+    initMenus(data) {
+      const defaultMenus = [
+        {
+          name: DEFAULT_MENU_LASTMESSAGES,
+          title: "聊天",
+          unread: 0,
+          click: null,
+          render: menu => {
+            return <i class="lemon-icon-message" />;
+          },
+          isBottom: false,
+        },
+        {
+          name: DEFAULT_MENU_CONTACTS,
+          title: "通讯录",
+          unread: 0,
+          click: null,
+          render: menu => {
+            return <i class="lemon-icon-addressbook" />;
+          },
+          isBottom: false,
+        },
+      ];
+      let menus = [];
+      if (Array.isArray(data)) {
+        const indexMap = {
+          messages: 0,
+          contacts: 1,
+        };
+        const indexKeys = Object.keys(indexMap);
+        menus = data.map(item => {
+          if (indexKeys.includes(item.name)) {
+            return {
+              ...defaultMenus[indexMap[item.name]],
+              ...item,
+              ...{ renderContainer: null },
+            };
+          }
+
+          if (item.renderContainer) {
+            this._customContainerReady(
+              item.renderContainer,
+              this.CacheMenuContainer,
+              item.name,
+            );
+          }
+
+          return item;
+        });
+      } else {
+        menus = defaultMenus;
+      }
+
+      this.menus = menus;
+    },
+    /**
+     * 初始化联系人数据
+     * @param {Array<Contact>} data 联系人列表
+     */
+    initContacts(data) {
+      this.contacts = data;
+      this.sortContacts();
+    },
+    /**
+     * 使用 联系人的 index 值进行排序
+     */
+    sortContacts() {
+      this.contacts.sort((a, b) => {
+        if (!a.index) return;
+        return a.index.localeCompare(b.index);
+      });
+    },
+    appendContact(contact) {
+      if (isEmpty(contact.id) || isEmpty(contact.displayName)) {
+        console.error("id | displayName cant be empty");
+        return false;
+      }
+      if (this.hasContact(contact.id)) return true;
+      this.contacts.push(
+        Object.assign(
+          {
+            id: "",
+            displayName: "",
+            avatar: "",
+            index: "",
+            unread: 0,
+            lastSendTime: "",
+            lastContent: "",
+          },
+          contact,
+        ),
+      );
+      return true;
+    },
+    removeContact(id) {
+      const index = this.findContactIndexById(id);
+      if (index === -1) return false;
+      this.contacts.splice(index, 1);
+      this.CacheDraft.remove(id);
+      this.CacheMessageLoaded.remove(id);
+      return true;
+    },
+    /**
+     * 修改联系人数据
+     * @param {Contact} data 修改的数据,根据 Contact.id 查找联系人并覆盖传入的值
+     */
+    updateContact(data) {
+      const contactId = data.id;
+      delete data.id;
+
+      const index = this.findContactIndexById(contactId);
+      if (index !== -1) {
+        const { unread } = data;
+        if (isString(unread)) {
+          if (unread.indexOf("+") === 0 || unread.indexOf("-") === 0) {
+            data.unread =
+              parseInt(unread) + parseInt(this.contacts[index].unread);
+          }
+        }
+        this.$set(this.contacts, index, {
+          ...this.contacts[index],
+          ...data,
+        });
+      }
+    },
+    /**
+     * 根据 id 查找联系人的索引
+     * @param contactId 联系人 id
+     * @return {Number} 联系人索引,未找到返回 -1
+     */
+    findContactIndexById(contactId) {
+      return this.contacts.findIndex(item => item.id == contactId);
+    },
+    /**
+     * 根据 id 查找判断是否存在联系人
+     * @param contactId 联系人 id
+     * @return {Boolean}
+     */
+    hasContact(contactId) {
+      return this.findContactIndexById(contactId) !== -1;
+    },
+    findMessage(messageId) {
+      for (const key in allMessages) {
+        const message = allMessages[key].find(({ id }) => id == messageId);
+        if (message) return message;
+      }
+    },
+    findContact(contactId) {
+      return this.getContacts().find(({ id }) => id == contactId);
+    },
+    /**
+     * 返回所有联系人
+     * @return {Array<Contact>}
+     */
+    getContacts() {
+      return this.contacts;
+    },
+    //返回当前聊天窗口联系人信息
+    getCurrentContact() {
+      return this.currentContact;
+    },
+    getCurrentMessages() {
+      return this.currentMessages;
+    },
+    setEditorValue(val = "") {
+      if (!isString(val)) return false;
+      this.$refs.editor.setValue(this.emojiNameToImage(val));
+    },
+    getEditorValue() {
+      return this.$refs.editor.getFormatValue();
+    },
+    /**
+     * 清空某个联系人的消息,切换到该联系人时会重新触发pull-messages事件
+     */
+    clearMessages(contactId) {
+      if (contactId) {
+        delete allMessages[contactId];
+        this.CacheMessageLoaded.remove(contactId);
+        this.CacheDraft.remove(contactId);
+      } else {
+        allMessages = {};
+        this.CacheMessageLoaded.remove();
+        this.CacheDraft.remove();
+      }
+      return true;
+    },
+    /**
+     * 返回所有消息
+     * @return {Object<Contact.id,Message>}
+     */
+    getMessages(contactId) {
+      return (contactId ? allMessages[contactId] : allMessages) || [];
+    },
+    changeDrawer(params) {
+      this.drawerVisible = !this.drawerVisible;
+      if (this.drawerVisible == true) this.openDrawer(params);
+    },
+    // openDrawer(data) {
+    //   renderDrawerContent = data || new Function();
+    //   this.drawerVisible = true;
+    // },
+    openDrawer(params) {
+      renderDrawerContent = isFunction(params)
+        ? params
+        : params.render || new Function();
+      const wrapperWidth = this.$refs.wrapper.clientWidth;
+      const wrapperHeight = this.$refs.wrapper.clientHeight;
+      let width = params.width || 200;
+      let height = params.height || wrapperHeight;
+      let offsetX = params.offsetX || 0;
+      let offsetY = params.offsetY || 0;
+      const position = params.position || "right";
+      if (isString(width)) width = wrapperWidth * toPoint(width);
+      if (isString(height)) height = wrapperHeight * toPoint(height);
+      if (isString(offsetX)) offsetX = wrapperWidth * toPoint(offsetX);
+      if (isString(offsetY)) offsetY = wrapperHeight * toPoint(offsetY);
+
+      this.$refs.drawer.style.width = `${width}px`;
+      this.$refs.drawer.style.height = `${height}px`;
+
+      let left = 0;
+      let top = 0;
+      let shadow = "";
+      if (position == "right") {
+        left = wrapperWidth;
+      } else if (position == "rightInside") {
+        left = wrapperWidth - width;
+        shadow = `-15px 0 16px -14px rgba(0,0,0,0.08)`;
+      } else if (position == "center") {
+        left = wrapperWidth / 2 - width / 2;
+        top = wrapperHeight / 2 - height / 2;
+        shadow = `0 0 20px rgba(0,0,0,0.08)`;
+      }
+      left += offsetX;
+      top += offsetY + -1;
+      this.$refs.drawer.style.top = `${top}px`;
+      this.$refs.drawer.style.left = `${left}px`;
+      this.$refs.drawer.style.boxShadow = shadow;
+
+      this.drawerVisible = true;
+    },
+    closeDrawer() {
+      this.drawerVisible = false;
+    },
+  },
+};
+</script>
+<style lang="stylus">
+bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
+@import '../styles/utils/index'
+
++b(lemon-wrapper)
+  display flex
+  font-size 14px
+  font-family "Microsoft YaHei"
+  //mask-image radial-gradient(circle, white 100%, black 100%)
+  background #efefef
+  transition all .4s bezier
+  position relative
+  p
+    margin 0
+  img
+    vertical-align middle
+    border-style none
++b(lemon-menu)
+  flex-column()
+  align-items center
+  width 60px
+  background #1d232a
+  padding 15px 0
+  position relative
+  user-select none
+  +e(bottom)
+    flex-column()
+    position absolute
+    bottom 0
+  +e(avatar)
+    margin-bottom 20px
+    cursor pointer
+  +e(item)
+    color #999
+    cursor pointer
+    padding 14px 10px
+    max-width 100%
+    +m(active)
+      color #0fd547
+    &:hover:not(.lemon-menu__item--active)
+      color #eee
+    word-break()
+    > *
+      font-size 24px
+    .ant-badge-count
+      display inline-block
+      padding 0 4px
+      height 18px
+      line-height 16px
+      min-width 18px
+    .ant-badge-count
+    .ant-badge-dot
+      box-shadow 0 0 0 1px #1d232a
++b(lemon-sidebar)
+  width 250px
+  background #efefef
+  display flex
+  flex-direction column
+  +e(scroll)
+    overflow-y auto
+    scrollbar-light()
+  +e(label)
+    padding 6px 14px 6px 14px
+    color #666
+    font-size 12px
+    margin 0
+    text-align left
+  +b(lemon-contact--active)
+    background #d9d9d9
++b(lemon-container)
+  flex 1
+  flex-column()
+  background #f4f4f4
+  word-break()
+  position relative
+  z-index 10
+  +e(title)
+    padding 15px 15px
+  +e(displayname)
+    font-size 16px
++b(lemon-vessel)
+  display flex
+  flex 1
+  min-height 100px
+  +e(left)
+    display flex
+    flex-direction column
+    flex 1
+  +e(right)
+    flex none
++b(lemon-messages)
+  flex 1
+  height auto
++b(lemon-drawer)
+  position absolute
+  top 0
+  overflow hidden
+  background #f6f6f6
+  z-index 11
+  display none
++b(lemon-wrapper)
+  +m(drawer-show)
+    +b(lemon-drawer)
+      display block
++b(lemon-contact-info)
+  flex-column()
+  justify-content center
+  align-items center
+  height 100%
+  h4
+    font-size 16px
+    font-weight normal
+    margin 10px 0 20px 0
+    user-select none
+.lemon-wrapper--theme-blue
+  .lemon-message__content
+    background #f3f3f3
+    &::before
+      border-right-color #f3f3f3
+  .lemon-message--reverse .lemon-message__content
+    background #e6eeff
+    &::before
+      border-left-color #e6eeff
+  .lemon-container
+    background #fff
+  .lemon-sidebar
+    background #f9f9f9
+    .lemon-contact
+      background #f9f9f9
+      &:hover:not(.lemon-contact--active)
+        background #f1f1f1
+      &--active
+        background #e9e9e9
+  .lemon-menu
+    background #096bff
+  .lemon-menu__item
+    color rgba(255,255,255,0.4)
+    &:hover:not(.lemon-menu__item--active)
+      color rgba(255,255,255,0.6)
+    &--active
+      color #fff
+      text-shadow 0 0 10px rgba(2,48,118,0.4)
+.lemon-wrapper--simple
+  .lemon-menu
+  .lemon-sidebar
+    display none
+.lemon-wrapper--simple
+  .lemon-menu
+  .lemon-sidebar
+    display none
++b(lemon-contextmenu)
+  border-radius 4px
+  font-size 14px
+  font-variant tabular-nums
+  line-height 1.5
+  color rgba(0, 0, 0, 0.65)
+  z-index 9999
+  background-color #fff
+  border-radius 6px
+  box-shadow 0 2px 8px rgba(0, 0, 0, 0.06)
+  position absolute
+  transform-origin 50% 150%
+  box-sizing border-box
+  user-select none
+  overflow hidden
+  min-width 120px
+  +e(item)
+    font-size 14px
+    line-height 16px
+    padding 10px 15px
+    cursor pointer
+    display flex
+    align-items center
+    color #333
+    > span
+      display inline-block
+      flex none
+      //max-width 100px
+      ellipsis()
+    &:hover
+      background #f3f3f3
+      color #000
+    &:active
+      background #e9e9e9
+  +e(icon)
+    font-size 16px
+    margin-right 4px
+</style>

+ 30 - 0
src/components/LemonUI/components/lemon-message-voice.vue

@@ -0,0 +1,30 @@
+<script>
+export default {
+  name: "lemonMessageVoice",
+  inheritAttrs: false,
+  inject: ["IMUI"],
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-voice"
+        props={{ ...this.$attrs }}
+        scopedSlots={{
+          content: props => {
+            return <span>{props.content}&nbsp;🔈</span>;
+          }
+        }}
+      />
+    );
+  }
+};
+</script>
+<style lang="stylus">
+.lemon-message.lemon-message-voice
+  user-select none
+  .lemon-message__content
+    border 2px solid #000
+    font-size 12px
+    cursor pointer
+    &::before
+      display none
+</style>

+ 240 - 0
src/components/LemonUI/components/message/basic.vue

@@ -0,0 +1,240 @@
+<script>
+import { useScopedSlot } from "../../utils";
+export default {
+  name: "lemonMessageBasic",
+  inject: {
+    IMUI: {
+      from: "IMUI",
+      default() {
+        return this;
+      },
+    },
+  },
+  props: {
+    contextmenu: Array,
+    message: {
+      type: Object,
+      default: () => {
+        return {};
+      },
+    },
+    timeFormat: {
+      type: Function,
+      default: () => "",
+    },
+    reverse: Boolean,
+    hideName: Boolean,
+    hideTime: Boolean,
+  },
+  data() {
+    return {};
+  },
+  render() {
+    const { fromUser, status, sendTime } = this.message;
+    const hideTitle = this.hideName == true && this.hideTime == true;
+    return (
+      <div
+        class={[
+          "lemon-message",
+          `lemon-message--status-${status}`,
+          {
+            "lemon-message--reverse": this.reverse,
+            "lemon-message--hide-title": hideTitle,
+          },
+        ]}
+      >
+        <div class="lemon-message__avatar">
+          <lemon-avatar
+            size={36}
+            shape="square"
+            src={fromUser.avatar}
+            on-click={e => {
+              this._emitClick(e, "avatar");
+            }}
+          />
+        </div>
+        <div class="lemon-message__inner">
+          <div class="lemon-message__title">
+            {this.hideName == false && (
+              <span
+                on-click={e => {
+                  this._emitClick(e, "displayName");
+                }}
+              >
+                {fromUser.displayName}
+              </span>
+            )}
+            {this.hideTime == false && (
+              <span
+                class="lemon-message__time"
+                on-click={e => {
+                  this._emitClick(e, "sendTime");
+                }}
+              >
+                {this.timeFormat(sendTime)}
+              </span>
+            )}
+          </div>
+          <div class="lemon-message__content-flex">
+            <div
+              v-lemon-contextmenu_message={this.IMUI.contextmenu}
+              class="lemon-message__content"
+              on-click={e => {
+                this._emitClick(e, "content");
+              }}
+            >
+              {useScopedSlot(this.$scopedSlots["content"], null, this.message)}
+            </div>
+            <div class="lemon-message__content-after">
+              {useScopedSlot(
+                this.IMUI.$scopedSlots["message-after"],
+                null,
+                this.message,
+              )}
+            </div>
+            <div
+              class="lemon-message__status"
+              on-click={e => {
+                this._emitClick(e, "status");
+              }}
+            >
+              <i class="lemon-icon-loading lemonani-spin" />
+              <i
+                class="lemon-icon-prompt"
+                title="重发消息"
+                style={{
+                  color: "#ff2525",
+                  cursor: "pointer",
+                }}
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  },
+  created() {},
+  mounted() {},
+  computed: {},
+  watch: {},
+  methods: {
+    _emitClick(e, key) {
+      this.IMUI.$emit("message-click", e, key, this.message, this.IMUI);
+    },
+  },
+};
+</script>
+<style lang="stylus">
+@import '../../styles/utils/index'
+arrow()
+  content ' '
+  position absolute
+  top 6px
+  width 0
+  height 0
+  border 4px solid transparent
++b(lemon-message)
+  display flex
+  padding 10px 0
+  +e(time)
+    color #b9b9b9
+    padding 0 5px
+  +e(inner)
+    position relative
+  +e(avatar)
+    padding-right 10px
+    user-select none
+    .lemon-avatar
+      cursor pointer
+  +e(title)
+    display flex
+    font-size 12px
+    line-height 16px
+    height 16px
+    padding-bottom 4px
+    user-select none
+    color #666
+  +e(content-flex)
+    display flex
+  +e(content)
+    font-size 14px
+    line-height 20px
+    padding 8px 10px
+    background #fff
+    border-radius 4px
+    position relative
+    margin 0
+    img
+    video
+      background #e9e9e9
+      height 100px
+    &:before
+      arrow()
+      left -4px
+      border-left none
+      border-right-color #fff
+  +e(content-after)
+    display block
+    width 48px
+    height 36px
+    padding-left 6px
+    flex none
+    font-size 12px
+    color #aaa
+    overflow hidden
+    visibility hidden
+  +e(status)
+    position absolute
+    top 23px
+    right 20px
+    color #aaa
+    font-size 20px
+    .lemon-icon-loading
+    .lemon-icon-prompt
+      display none
+  +m(status-going)
+    .lemon-icon-loading
+      display inline-block
+  +m(status-failed)
+    .lemon-icon-prompt
+      display inline-block
+  +m(status-succeed)
+    +e(content-after)
+      visibility visible
+  +m(reverse)
+    flex-direction row-reverse
+    +e(content-flex)
+      flex-direction row-reverse
+    +e(content-after)
+      padding-right 6px
+      padding-left 0
+      text-align right
+    +e(title)
+      flex-direction row-reverse
+    +e(status)
+      left 26px
+      right auto
+    +e(content)
+      background #35d863
+      &:before
+        arrow()
+        left auto
+        right -4px
+        border-right none
+        border-left-color #35d863
+    +e(title)
+      text-align right
+    +e(avatar)
+      padding-right 0
+      padding-left 10px
+  +m(hide-title)
+    +e(avatar)
+      padding-top 10px
+    +e(status)
+      top 14px
+    +e(content)
+      position relative
+      top -10px
+      &:before
+        top 14px
+</style>

+ 38 - 0
src/components/LemonUI/components/message/event.vue

@@ -0,0 +1,38 @@
+<script>
+export default {
+  name: "lemonMessageEvent",
+  inheritAttrs: false,
+  inject: ["IMUI"],
+  render() {
+    const { content } = this.$attrs.message;
+    return (
+      <div class="lemon-message lemon-message-event">
+        <span
+          class="lemon-message-event__content"
+          on-click={e => this._emitClick(e, "content")}
+        >
+          {content}
+        </span>
+      </div>
+    );
+  },
+  methods: {
+    _emitClick(e, key) {
+      this.IMUI.$emit("message-click", e, key, this.$attrs.message, this.IMUI);
+    }
+  }
+};
+</script>
+<style lang="stylus">
+@import '../../styles/utils/index'
++b(lemon-message-event)
+  +e(content)
+    user-select none
+    display inline-block
+    background #e9e9e9
+    color #aaa
+    font-size 12px
+    margin 0 auto
+    padding 5px 10px
+    border-radius 4px
+</style>

+ 59 - 0
src/components/LemonUI/components/message/file.vue

@@ -0,0 +1,59 @@
+<script>
+import { formatByte } from "../../utils";
+export default {
+  name: "lemonMessageFile",
+  inheritAttrs: false,
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-file"
+        props={{ ...this.$attrs }}
+        scopedSlots={{
+          content: props => [
+            <div class="lemon-message-file__inner">
+              <p class="lemon-message-file__name">{props.fileName}</p>
+              <p class="lemon-message-file__byte">
+                {formatByte(props.fileSize)}
+              </p>
+            </div>,
+            <div class="lemon-message-file__sfx">
+              <i class="lemon-icon-attah" />
+            </div>
+          ]
+        }}
+      />
+    );
+  }
+};
+</script>
+<style lang="stylus">
+@import '../../styles/utils/index'
++b(lemon-message-file)
+  +b(lemon-message)
+    +e(content)
+      display flex
+      cursor pointer
+      width 200px
+      background #fff
+      padding 12px 18px
+      overflow hidden
+      p
+        margin 0
+  +e(tip)
+    display none
+  +e(inner)
+    flex 1
+  +e(name)
+    font-size 14px
+  +e(byte)
+    font-size 12px
+    color #aaa
+  +e(sfx)
+    display flex
+    align-items center
+    justify-content center
+    font-weight bold
+    user-select none
+    font-size 34px
+    color #ccc
+</style>

+ 30 - 0
src/components/LemonUI/components/message/image.vue

@@ -0,0 +1,30 @@
+<script>
+export default {
+  name: "lemonMessageImage",
+  inheritAttrs: false,
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-image"
+        props={{ ...this.$attrs }}
+        scopedSlots={{
+          content: props => <img src={props.content} />
+        }}
+      />
+    );
+  }
+};
+</script>
+<style lang="stylus">
+@import '../../styles/utils/index'
++b(lemon-message-image)
+  +b(lemon-message)
+    +e(content)
+      padding 0
+      cursor pointer
+      overflow hidden
+      img
+        max-width 100%
+        min-width 100px
+        display block
+</style>

+ 36 - 0
src/components/LemonUI/components/message/text.vue

@@ -0,0 +1,36 @@
+<script>
+export default {
+  name: "lemonMessageText",
+  inheritAttrs: false,
+  inject: ["IMUI"],
+  render() {
+    return (
+      <lemon-message-basic
+        class="lemon-message-text"
+        props={{ ...this.$attrs }}
+        scopedSlots={{ 
+          content: props => {
+            const content = this.IMUI.emojiNameToImage(props.content);
+            return <span domProps={{ innerHTML: content }} />;
+          }
+        }}
+      />
+    );
+  }
+};
+</script>
+<style lang="stylus">
+@import '../../styles/utils/index'
++b(lemon-message-text)
+  +b(lemon-message)
+    +e(content)
+      img
+        width 18px
+        height 18px
+        display inline-block
+        background transparent
+        position relative
+        top -1px
+        padding 0 2px
+        vertical-align middle
+</style>

+ 187 - 0
src/components/LemonUI/components/messages.vue

@@ -0,0 +1,187 @@
+<script>
+import { hoursTimeFormat } from "../utils";
+import { isString } from "../utils/validate";
+import contextmenu from "../directives/contextmenu";
+export default {
+  name: "LemonMessages",
+  components: {},
+  props: {
+    //是否隐藏消息发送人昵称
+    hideName: Boolean,
+    //是否隐藏显示消息时间
+    hideTime: Boolean,
+    reverseUserId: [String, Number],
+    timeRange: {
+      type: Number,
+      default: 1,
+    },
+    timeFormat: {
+      type: Function,
+      default(val) {
+        return hoursTimeFormat(val);
+      },
+    },
+    loadingText: {
+      type: [String, Function],
+    },
+    loadendText: {
+      type: [String, Function],
+      default: "暂无更多消息",
+    },
+    messages: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    this._lockScroll = false;
+    return {
+      _loading: false,
+      _loadend: false,
+    };
+  },
+  render() {
+    return (
+      <div class="lemon-messages" ref="wrap" on-scroll={this._handleScroll}>
+        <div
+          class={[
+            "lemon-messages__load",
+            `lemon-messages__load--${this._loadend ? "end" : "ing"}`,
+          ]}
+        >
+          <span class="lemon-messages__loadend">
+            {isString(this.loadendText) ? this.loadendText : this.loadendText()}
+          </span>
+          <span class="lemon-messages__loading">
+            {this.loadingText ? (
+              isString(this.loadingText) ? (
+                this.loadingText
+              ) : (
+                this.loadingText()
+              )
+            ) : (
+              <i class="lemon-icon-loading lemonani-spin" />
+            )}
+          </span>
+        </div>
+        {this.messages.map((message, index) => {
+          const node = [];
+          const tagName = `lemon-message-${message.type}`;
+          const prev = this.messages[index - 1];
+          if (
+            prev &&
+            this.msecRange &&
+            message.sendTime - prev.sendTime > this.msecRange
+          ) {
+            node.push(
+              <lemon-message-event
+                attrs={{
+                  message: {
+                    id: "__time__",
+                    type: "event",
+                    content: hoursTimeFormat(message.sendTime),
+                  },
+                }}
+              />,
+            );
+          }
+
+          let attrs;
+          if (message.type == "event") {
+            attrs = { message: message };
+          } else {
+            attrs = {
+              timeFormat: this.timeFormat,
+              message: message,
+              reverse: this.reverseUserId == message.fromUser.id,
+              hideTime: this.hideTime,
+              hideName: this.hideName,
+            };
+          }
+          node.push(<tagName ref="message" refInFor={true} attrs={attrs} />);
+          return node;
+        })}
+      </div>
+    );
+  },
+  computed: {
+    msecRange() {
+      return this.timeRange * 1000 * 60;
+    },
+  },
+  watch: {},
+  methods: {
+    loaded() {
+      this._loadend = true;
+      this.$forceUpdate();
+    },
+    resetLoadState() {
+      this._lockScroll = true;
+      this._loading = false;
+      this._loadend = false;
+      setTimeout(() => {
+        this._lockScroll = false;
+      }, 200);
+    },
+    async _handleScroll(e) {
+      if (this._lockScroll) return;
+      const { target } = e;
+      contextmenu.hide();
+      if (
+        target.scrollTop == 0 &&
+        this._loading == false &&
+        this._loadend == false
+      ) {
+        this._loading = true;
+        await this.$nextTick();
+        const hst = target.scrollHeight;
+
+        this.$emit("reach-top", async isEnd => {
+          await this.$nextTick();
+          target.scrollTop = target.scrollHeight - hst;
+          this._loading = false;
+          this._loadend = !!isEnd;
+        });
+      }
+    },
+    async scrollToBottom() {
+      await this.$nextTick();
+      const { wrap } = this.$refs;
+      if (wrap) {
+        wrap.scrollTop = wrap.scrollHeight;
+      }
+    },
+  },
+  created() {},
+  mounted() {},
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-messages)
+  height 400px
+  overflow-x hidden
+  overflow-y auto
+  scrollbar-light()
+  padding 10px 15px
+  +e(time)
+    text-align center
+    font-size 12px
+  +e(load)
+    user-select none
+    font-size 12px
+    text-align center
+    color #999
+    line-height 30px
+    .lemon-messages__loading
+    .lemon-messages__loadend
+      display none
+    +m(ing)
+      .lemon-icon-loading
+        font-size 22px
+      .lemon-messages__loading
+        display block
+    +m(end)
+      .lemon-messages__loadend
+        display block
+</style>

+ 146 - 0
src/components/LemonUI/components/popover.vue

@@ -0,0 +1,146 @@
+<script>
+const popoverCloseQueue = [];
+import contextmenu from "../directives/contextmenu";
+const triggerEvents = {
+  hover(el) {},
+  focus(el) {
+    el.addEventListener("focus", e => {
+      this.changeVisible();
+    });
+    el.addEventListener("blur", e => {
+      this.changeVisible();
+    });
+  },
+  click(el) {
+    el.addEventListener("click", e => {
+      e.stopPropagation();
+      contextmenu.hide();
+      this.changeVisible();
+    });
+  },
+  contextmenu(el) {
+    el.addEventListener("contextmenu", e => {
+      e.preventDefault();
+      this.changeVisible();
+    });
+  }
+};
+export default {
+  name: "LemonPopover",
+  props: {
+    trigger: {
+      type: String,
+      default: "click",
+      validator(val) {
+        return Object.keys(triggerEvents).includes(val);
+      }
+    }
+  },
+  data() {
+    return {
+      popoverStyle: {},
+      visible: false
+    };
+  },
+  created() {
+    document.addEventListener("click", this._documentClickEvent);
+    popoverCloseQueue.push(this.close);
+  },
+  mounted() {
+    triggerEvents[this.trigger].call(this, this.$slots.default[0].elm);
+  },
+  render() {
+    return (
+      <span style="position:relative">
+        <transition name="lemon-slide-top">
+          {this.visible && (
+            <div
+              class="lemon-popover"
+              ref="popover"
+              style={this.popoverStyle}
+              on-click={e => e.stopPropagation()}
+            >
+              <div class="lemon-popover__content">{this.$slots.content}</div>
+              <div class="lemon-popover__arrow" />
+            </div>
+          )}
+        </transition>
+        {this.$slots.default}
+      </span>
+    );
+  },
+  destroyed() {
+    document.removeEventListener("click", this._documentClickEvent);
+  },
+  computed: {},
+  watch: {
+    async visible(val) {
+      if (val) {
+        await this.$nextTick();
+        const defaultEl = this.$slots.default[0].elm;
+        const contentEl = this.$refs.popover;
+
+        this.popoverStyle = {
+          top: `-${contentEl.offsetHeight + 10}px`,
+          left: `${defaultEl.offsetWidth / 2 - contentEl.offsetWidth / 2}px`
+        };
+      }
+    }
+  },
+  methods: {
+    _documentClickEvent(e) {
+      e.stopPropagation();
+      if (this.visible) this.close();
+    },
+    changeVisible() {
+      this.visible ? this.close() : this.open();
+    },
+    open() {
+      this.closeAll();
+      this.visible = true;
+    },
+    closeAll() {
+      popoverCloseQueue.forEach(callback => callback());
+    },
+    close() {
+      this.visible = false;
+    }
+  }
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
++b(lemon-popover)
+  border 1px solid #eee
+  border-radius 4px
+  font-size 14px
+  font-variant tabular-nums
+  line-height 1.5
+  color rgba(0, 0, 0, 0.65)
+  z-index 10
+  background-color #fff
+  border-radius 4px
+  box-shadow 0 2px 8px rgba(0, 0, 0, 0.08)
+  position absolute
+  transform-origin 50% 150%
+  +e(content)
+    padding 15px
+    box-sizing border-box
+    position relative
+    z-index 1
+  +e(arrow)
+    left 50%
+    transform translateX(-50%) rotate(45deg)
+    position absolute
+    z-index 0
+    bottom -4px
+    box-shadow 3px 3px 7px rgba(0, 0, 0, 0.07)
+    width 8px
+    height 8px
+    background #fff
+.lemon-slide-top-leave-active ,.lemon-slide-top-enter-active
+  transition all .2s cubic-bezier(0.645, 0.045, 0.355, 1)
+.lemon-slide-top-enter, .lemon-slide-top-leave-to
+  transform translateY(-10px) scale(.8)
+  opacity 0
+</style>

+ 77 - 0
src/components/LemonUI/components/tabs.vue

@@ -0,0 +1,77 @@
+<script>
+export default {
+  name: "LemonTabs",
+  props: {
+    activeIndex: String
+  },
+  data() {
+    return {
+      active: this.activeIndex
+    };
+  },
+  mounted() {
+    if (!this.active) {
+      this.active = this.$slots["tab-pane"][0].data.attrs.index;
+    }
+  },
+  render() {
+    const pane = [];
+    const nav = [];
+    this.$slots["tab-pane"].map(vnode => {
+      const { tab, index } = vnode.data.attrs;
+      pane.push(
+        <div class="lemon-tabs-content__pane" v-show={this.active == index}>
+          {vnode}
+        </div>
+      );
+      nav.push(
+        <div
+          class={[
+            "lemon-tabs-nav__item",
+            this.active == index && "lemon-tabs-nav__item--active"
+          ]}
+          on-click={() => this._handleNavClick(index)}
+        >
+          {tab}
+        </div>
+      );
+    });
+    return (
+      <div class="lemon-tabs">
+        <div class="lemon-tabs-content">{pane}</div>
+        <div class="lemon-tabs-nav">{nav}</div>
+      </div>
+    );
+  },
+  methods: {
+    _handleNavClick(index) {
+      this.active = index;
+    }
+  }
+};
+</script>
+<style lang="stylus">
+@import '../styles/utils/index'
+pane-color = #f6f6f6
++b(lemon-tabs)
+  background pane-color
++b(lemon-tabs-content)
+  width 100%
+  height 100%
+  padding 15px
+  +e(pane)
+    //scrollbar-light()
+    //overflow-y auto
+    height 100%
+    width 100%
++b(lemon-tabs-nav)
+  display flex
+  background #eee
+  +e(item)
+    line-height 38px
+    padding 0 15px
+    cursor pointer
+    transition all .3s cubic-bezier(0.645, 0.045, 0.355, 1)
+    +m(active)
+      background pane-color
+</style>

+ 0 - 0
src/views/qw/database/contacts.js → src/components/LemonUI/database/contacts.js


+ 0 - 0
src/views/qw/database/emoji.js → src/components/LemonUI/database/emoji.js


+ 3 - 2
src/views/qw/database/messages.js → src/components/LemonUI/database/messages.js

@@ -1,5 +1,6 @@
-import ContactsData from "lemon-imui/examples/database/contacts";
-import UserData from "lemon-imui/examples/database/user";
+
+import ContactsData from "./contacts";
+import UserData from "./user";
 const generateRandId = () => {
   return Math.random()
     .toString(36)

+ 0 - 0
src/views/qw/database/user.js → src/components/LemonUI/database/user.js


+ 83 - 0
src/components/LemonUI/directives/contextmenu.js

@@ -0,0 +1,83 @@
+// import Vue from "vue";
+import { isFunction, isEmpty } from "../utils/validate";
+import LemonPopover from "../components/popover.vue";
+let popover;
+
+const hidePopover = () => {
+  if (popover) popover.style.display = "none";
+};
+const showPopover = () => {
+  if (popover) popover.style.display = "block";
+};
+document.addEventListener("click", e => {
+  hidePopover();
+});
+export default {
+  hide: hidePopover,
+  bind(el, binding, vnode) {
+    el.addEventListener(
+      binding.modifiers.click ? "click" : "contextmenu",
+      e => {
+        if (isEmpty(binding.value) || !Array.isArray(binding.value)) return;
+        if (binding.modifiers.click) e.stopPropagation();
+        e.preventDefault();
+        LemonPopover.methods.closeAll();
+        let component;
+        let visibleItems = [];
+        if (binding.modifiers.message) component = vnode.context;
+        else if (binding.modifiers.contact) component = vnode.child;
+        if (!popover) {
+          popover = document.createElement("div");
+          popover.className = "lemon-contextmenu";
+          document.body.appendChild(popover);
+        }
+        popover.innerHTML = binding.value
+          .map(item => {
+            let visible;
+            if (isFunction(item.visible)) {
+              visible = item.visible(component);
+            } else {
+              visible = item.visible === undefined ? true : item.visible;
+            }
+
+            if (visible) {
+              visibleItems.push(item);
+              const icon = item.icon
+                ? `<i class="lemon-contextmenu__icon ${item.icon}"></i>`
+                : "";
+              return `<div style="color:${item.color}" title="${
+                item.text
+              }" class="lemon-contextmenu__item">${icon}<span>${
+                item.text
+              }</span></div>`;
+            }
+            return "";
+          })
+          .join("");
+        popover.style.top = `${e.pageY}px`;
+        popover.style.left = `${e.pageX}px`;
+
+        popover.childNodes.forEach((node, index) => {
+          const { click, render } = visibleItems[index];
+          node.addEventListener("click", e => {
+            e.stopPropagation();
+            if (isFunction(click)) click(e, component, hidePopover);
+          });
+
+          // if (isFunction(render)) {
+          //   const ins = Vue.extend({
+          //     render: h => {
+          //       return render(h, component, hidePopover);
+          //     },
+          //   });
+          //   const renderComponent = new ins().$mount();
+          //   node.querySelector("span").innerHTML =
+          //     renderComponent.$el.outerHTML;
+          // }
+        });
+
+        showPopover();
+      },
+    );
+  },
+};

文件差異過大導致無法顯示
+ 0 - 0
src/components/LemonUI/index.css


+ 49 - 0
src/components/LemonUI/index.js

@@ -0,0 +1,49 @@
+import Contextmenu from "./directives/contextmenu";
+import LemonTabs from "./components/tabs";
+import LemonPopover from "./components/popover";
+import LemonButton from "./components/button";
+import LemonBadge from "./components/badge";
+import LemonAvatar from "./components/avatar";
+import LemonContact from "./components/contact";
+import LemonEditor from "./components/editor";
+import LemonMessages from "./components/messages";
+import LemonMessageBasic from "./components/message/basic";
+import LemonMessageText from "./components/message/text";
+import lemonMessageImage from "./components/message/image";
+import lemonMessageFile from "./components/message/file";
+import lemonMessageEvent from "./components/message/event";
+
+import LemonIMUI from "./components/index";
+import "./styles/common/index.styl";
+const version = "1.4.2";
+const components = [
+  LemonIMUI,
+  LemonContact,
+  LemonMessages,
+  LemonEditor,
+  LemonAvatar,
+  LemonBadge,
+  LemonButton,
+  LemonPopover,
+  LemonTabs,
+  LemonMessageBasic,
+  LemonMessageText,
+  lemonMessageImage,
+  lemonMessageFile,
+  lemonMessageEvent
+];
+const install = (Vue) => {
+  Vue.directive("LemonContextmenu", Contextmenu);
+  components.forEach(component => {
+    Vue.component(component.name, component);
+  });
+};
+
+if (typeof window !== "undefined" && window.Vue) {
+  install(window.Vue);
+}
+
+export default {
+  version,
+  install
+};

+ 15 - 0
src/components/LemonUI/lastContentRender.js

@@ -0,0 +1,15 @@
+import {clearHtml } from './utils';
+export default {
+  file(message) {
+    return "[文件]";
+  },
+  image(message) {
+    return "[图片]";
+  },
+  text(message) {
+    return this.emojiNameToImage(clearHtml(message.content));
+  },
+  event(message){
+    return '[通知]';
+  },
+};

+ 13 - 0
src/components/LemonUI/styles/common/animate.styl

@@ -0,0 +1,13 @@
+
+.lemonani-spin
+  display inline-block
+  animation lemonani-spin 1s infinite
+@keyframes lemonani-spin{
+  0%{
+    transform rotate(0deg)
+  }
+  100%{
+    transform rotate(360deg)
+  }
+}
+

+ 45 - 0
src/components/LemonUI/styles/common/icons.styl

@@ -0,0 +1,45 @@
+// @font-face {
+//   font-family: 'lemon-icons'; 
+//   src: url('//at.alicdn.com/t/font_1312162_neqltsj20an.eot');
+//   src: url('//at.alicdn.com/t/font_1312162_neqltsj20an.eot?#iefix') format('embedded-opentype'),
+//   url('//at.alicdn.com/t/font_1312162_neqltsj20an.woff2') format('woff2'),
+//   url('//at.alicdn.com/t/font_1312162_neqltsj20an.woff') format('woff'),
+//   url('//at.alicdn.com/t/font_1312162_neqltsj20an.ttf') format('truetype'),
+//   url('//at.alicdn.com/t/font_1312162_neqltsj20an.svg#iconfont') format('svg');
+// }
+@font-face {
+  font-family: 'lemon-icons'; 
+  src:url('../fonts/icon.woff') format('woff');
+}
+[class^='lemon-icon-'],
+[class*=' lemon-icon-']
+  font-family lemon-icons !important
+  speak none
+  font-style normal
+  font-weight 400
+  font-variant normal
+  text-transform none
+  line-height 1
+  vertical-align baseline
+  display inline-block
+
+.lemon-icon-loading:before
+  content '\e633'
+.lemon-icon-prompt:before
+  content '\e71b'
+.lemon-icon-message:before
+  content '\e84a'
+.lemon-icon-emoji:before
+  content '\e6f6'
+.lemon-icon-attah:before
+  content '\e7e1'
+.lemon-icon-image:before
+  content '\e7de'
+.lemon-icon-folder:before
+  content '\e7d1'
+.lemon-icon-people:before
+  content '\e715'
+.lemon-icon-group:before
+  content '\e6ff'
+.lemon-icon-addressbook:before
+  content '\e6e2'

+ 3 - 0
src/components/LemonUI/styles/common/index.styl

@@ -0,0 +1,3 @@
+//@import './normalize';
+@import './animate';
+@import './icons';

+ 23 - 0
src/components/LemonUI/styles/common/normalize.styl

@@ -0,0 +1,23 @@
+html 
+  -webkit-tap-highlight-color transparent
+body 
+  margin 0
+a 
+  text-decoration none
+a
+input
+button
+textarea 
+  &:focus 
+    outline none
+ol
+ul 
+  margin 0
+  padding 0
+  list-style none
+input
+button
+textarea 
+  font inherit
+  color inherit
+

二進制
src/components/LemonUI/styles/fonts/icon.woff


+ 67 - 0
src/components/LemonUI/styles/utils/bem.styl

@@ -0,0 +1,67 @@
+// -----------------------------------------------------------------------------
+// bem-sugar.styl --- Bem mixins for stylus language
+//
+// Copyright (c) 2017 Ilya Obuhov
+//
+// Author: Ilya Obuhov <iobuhov.mail@gmail.com>
+// URL: https://github.com/iobuhov/stylus-bem-sugar
+
+
+e-prefix    ?= '__'
+m-prefix    ?= '--'
+m-delimiter ?= '_'
+group-store = ()
+
+str()
+  join('', arguments)
+
+b(name)
+  .{name}
+    {block}
+
+group()
+  caller = called-from[0]
+  level = length(called-from) + 1
+  elements = group-store[level]
+  selector = ()
+  parent = null
+  {join(',', elements)}
+    {block}
+  group-store[level] = null
+
+m(mod, val=null)
+  val    = val && m-delimiter + val
+  mod    = m-prefix + mod
+  mod    = val ? mod + val : mod
+  caller = called-from[0]
+  if caller in ('group')
+    level = length(called-from)
+    mod = str('&', mod)
+    if group-store[level] == null
+      group-store[level] = mod
+    else
+      push(group-store[level], mod)
+  &{mod}
+    {block}
+
+e(element)
+  element = e-prefix + element
+  caller  = called-from[0]
+  gcaller = called-from[1]
+  if caller in ('group')
+    level = length(called-from)
+    if gcaller in ('e' 'm')
+      element = str('& ^[0]', element)
+    else
+      element = str('^[0]', element)
+    if group-store[level] == null
+      group-store[level] = element
+    else
+      push(group-store[level], element)
+  else
+    if caller in ('e' 'm')
+      & ^[0]{element}
+        {block}
+    else
+      &{element}
+        {block}

+ 47 - 0
src/components/LemonUI/styles/utils/functional.styl

@@ -0,0 +1,47 @@
+flex-column()
+  display flex
+  flex-direction column
+  
+scrollbar-theme($color=#1f252d, $background=#6d6d6d)
+  &::-webkit-scrollbar
+    width 5px
+    height 5px
+  
+  &::-webkit-scrollbar-track-piece
+    background-color $background
+  
+  &::-webkit-scrollbar-thumb:vertical
+    height 5px
+    background-color $color
+  
+  &::-webkit-scrollbar-thumb:horizontal 
+    width 5px
+    background-color $background
+
+scrollbar-dark() 
+  scrollbar-theme()
+
+scrollbar-light() 
+  scrollbar-theme(#aaa, transparent)
+
+
+vertical-center()
+  &::after
+    display inline-block
+    content ''
+    height 100%
+    vertical-align middle
+
+position-center($type fixed) 
+  position $type
+  top 50%
+  left 50%
+  transform translate(-50%, -50%)
+ellipsis()
+  text-overflow ellipsis
+  overflow hidden
+  white-space nowrap
+word-break()
+  word-break break-all
+  word-wrap break-word
+  white-space pre-wrap

+ 5 - 0
src/components/LemonUI/styles/utils/index.styl

@@ -0,0 +1,5 @@
+@import './functional';
+@import './bem';
+@import './var';
+
+

+ 26 - 0
src/components/LemonUI/styles/utils/var.styl

@@ -0,0 +1,26 @@
+//color-primary #2977fa
+color-primary = #1bc213
+color-light = #fff
+/* 头像 */
+avatar-size = 45px
+avatar-radius = 50%
+
+/** 标题 */
+title-background = color-primary
+title-color = color-light
+title-height = 44px
+/* 气泡 */
+bubble-background = color-primary
+bubble-color = color-light
+bubble-radius = 12px
+
+bubble-self-background = #e7ebef
+bubble-self-color = #606d84
+
+/* 输入框 */
+editor-textarea-height = 40px
+editor-textarea-radius = 5px
+
+editor-submit-disable-color = #bcbcbc
+editor-submit-disable-background = #ebebeb
+editor-submit-radius = editor-textarea-radius

+ 24 - 0
src/components/LemonUI/utils/cache/memory.js

@@ -0,0 +1,24 @@
+export default class MemoryCache {
+  constructor() {
+    this.table = {};
+  }
+  get(key) {
+    return key ? this.table[key] : this.table;
+  }
+  set(key, val) {
+    this.table[key] = val;
+  }
+  // setOnly(key, val) {
+  //   if (!this.has(key)) this.set(key, val);
+  // }
+  remove(key) {
+    if (key) {
+      delete this.table[key];
+    } else {
+      this.table = {};
+    }
+  }
+  has(key) {
+    return !!this.table[key];
+  }
+}

+ 16 - 0
src/components/LemonUI/utils/constant.js

@@ -0,0 +1,16 @@
+export const EMIT_AVATAR_CLICK = "avatar-click";
+
+export const DEFAULT_MENU_LASTMESSAGES = "messages";
+export const DEFAULT_MENU_CONTACTS = "contacts";
+export const DEFAULT_MENUS = [DEFAULT_MENU_LASTMESSAGES, DEFAULT_MENU_CONTACTS];
+/**
+ * 聊天消息类型
+ */
+export const MESSAGE_TYPE = ["voice", "file", "video", "image", "text"];
+
+/**
+ * 聊天消息状态
+ */
+export const MESSAGE_STATUS = ["going", "succeed", "failed"];
+
+export const CONTACT_TYPE = ["many", "single"];

+ 134 - 0
src/components/LemonUI/utils/index.js

@@ -0,0 +1,134 @@
+import { isPlainObject, isFunction } from "../utils/validate";
+export function messageToHtml() {}
+export function messageToText() {}
+/**
+ * 使用某个组件上的作用域插槽
+ * @param {VueComponent} inject
+ * @param {String} slotName
+ * @param {Node} defaultElement
+ * @param {Object} props
+ */
+export function useScopedSlot(slot, def, props) {
+  return slot ? slot(props) : def;
+}
+export function padZero(val) {
+  return val < 10 ? `0${val}` : val;
+}
+export function hoursTimeFormat(t) {
+  const date = new Date(t);
+  const nowDate = new Date();
+  const Y = t => {
+    return t.getFullYear();
+  };
+  const MD = t => {
+    return `${t.getMonth() + 1}-${t.getDate()}`;
+  };
+  const dateY = Y(date);
+  const nowDateY = Y(nowDate);
+
+  let format;
+  if (dateY !== nowDateY) {
+    format = "y年m月d日 h:i";
+  } else if (`${dateY}-${MD(date)}` === `${nowDateY}-${MD(nowDate)}`) {
+    format = "h:i";
+  } else {
+    format = "m月d日 h:i";
+  }
+  return timeFormat(t, format);
+}
+export function timeFormat(t, format) {
+  if (!format) format = "y-m-d h:i:s";
+  if (t) t = new Date(t);
+  else t = new Date();
+  const formatArr = [
+    t.getFullYear().toString(),
+    padZero((t.getMonth() + 1).toString()),
+    padZero(t.getDate().toString()),
+    padZero(t.getHours().toString()),
+    padZero(t.getMinutes().toString()),
+    padZero(t.getSeconds().toString()),
+  ];
+  const reg = "ymdhis";
+  for (let i = 0; i < formatArr.length; i++) {
+    format = format.replace(reg.charAt(i), formatArr[i]);
+  }
+  return format;
+}
+
+export function funCall(event, callback) {
+  if (isFunction(event)) {
+    event(() => {
+      callback();
+    });
+  } else {
+    callback();
+  }
+}
+/**
+ * 获取数组相交的值组成新数组
+ * @param {Array} a
+ * @param {Array} b
+ */
+export function arrayIntersect(a, b) {
+  return a.filter(x => b.includes(x));
+}
+//清除字符串内的所有HTML标签
+export function clearHtml(str) {
+  return str.replace(/<.*?>/gi, "");
+}
+//清除字符串内的所有HTML标签,除了IMG
+export function clearHtmlExcludeImg(str) {
+  return str.replace(/<(?!img).*?>/gi, "");
+}
+export function error(text) {
+  throw new Error(text);
+}
+export function cloneDeep(obj) {
+  const newobj = { ...obj };
+  for (const key in newobj) {
+    const val = newobj[key];
+    if (isPlainObject(val)) {
+      newobj[key] = cloneDeep(val);
+    }
+  }
+  return newobj;
+}
+
+export function mergeDeep(o1, o2) {
+  for (const key in o2) {
+    if (isPlainObject(o1[key])) {
+      o1[key] = mergeDeep(o1[key], o2[key]);
+    } else {
+      o1[key] = o2[key];
+    }
+  }
+  return o1;
+}
+
+export function formatByte(value) {
+  if (null == value || value == "") {
+    return "0 Bytes";
+  }
+  var unitArr = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
+  var index = 0;
+  var srcsize = parseFloat(value);
+  index = Math.floor(Math.log(srcsize) / Math.log(1024));
+  var size = srcsize / Math.pow(1024, index);
+  size = parseFloat(size.toFixed(2));
+  return size + unitArr[index];
+}
+
+export function generateUUID() {
+  var d = new Date().getTime();
+  if (window.performance && typeof window.performance.now === "function") {
+    d += performance.now(); //use high-precision timer if available
+  }
+  var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(
+    c,
+  ) {
+    var r = (d + Math.random() * 16) % 16 | 0;
+    d = Math.floor(d / 16);
+    return (c == "x" ? r : (r & 0x3) | 0x8).toString(16);
+  });
+  return uuid;
+}

+ 37 - 0
src/components/LemonUI/utils/validate.js

@@ -0,0 +1,37 @@
+export function isPlainObject(obj) {
+  return Object.prototype.toString.call(obj) === "[object Object]";
+}
+export function isString(str) {
+  return typeof str == "string";
+}
+export function isToday(time) {
+  return new Date().getTime() - time < 86400000;
+}
+export function isEmpty(obj) {
+  if (!obj) return true;
+  if (Array.isArray(obj) && obj.length == 0) return true;
+  if (isPlainObject(obj) && Object.values(obj).length == 0) return true;
+  return false;
+}
+export function isUrl(str) {
+  const reg =
+    "^((https|http|ftp|rtsp|mms)?://)" +
+    "?(([0-9a-z_!~*'().&=+$%-]+: )?[0-9a-z_!~*'().&=+$%-]+@)?" + //ftp的user@
+    "(([0-9]{1,3}.){3}[0-9]{1,3}" + // IP形式的URL- 199.194.52.184
+    "|" + // 允许IP和DOMAIN(域名)
+    "([0-9a-z_!~*'()-]+.)*" + // 域名- www.
+    "([0-9a-z][0-9a-z-]{0,61})?[0-9a-z]." + // 二级域名
+    "[a-z]{2,6})" + // first level domain- .com or .museum
+    "(:[0-9]{1,4})?" + // 端口- :80
+    "((/?)|" + // 如果没有文件名,则不需要斜杠
+    "(/[0-9a-z_!~*'().;?:@&=+$,%#-]+)+/?)$";
+  return new RegExp(reg).test(str) ? true : false;
+}
+
+export function isFunction(val) {
+  return val && typeof val === "function";
+}
+
+export function isEng(val) {
+  return /^[A-Za-z]+$/.test(val);
+}

+ 2 - 1
src/main.js

@@ -38,7 +38,8 @@ Vue.component('ImageUpload',ImageUpload)
 import audio from 'vue-mobile-audio'
 Vue.use(audio)
 
-import LemonIMUI from 'lemon-imui';
+//import LemonIMUI from 'lemon-imui';
+import LemonIMUI from '@/components/LemonUI';
 Vue.use(LemonIMUI);
 
 Vue.prototype.callNumber = callNumber

+ 1 - 1
src/views/company/tcmScheduleReport/add.vue

@@ -107,7 +107,7 @@
             </el-col>
             <el-col :span="6">
                 <el-form-item label="目标业绩" label-width="90px" prop="targetMoney">
-                    <el-input-number :disabled="scheduleItem.status==0" v-model="form.targetMoney" :step="10" :precision="2"  :min="0.00" placeholder="请输入目标业绩" />
+                    <el-input-number  v-model="form.targetMoney" :step="10" :precision="2"  :min="0.00" placeholder="请输入目标业绩" />
                 </el-form-item>
             </el-col>
             <el-col :span="4">

+ 4 - 4
src/views/company/tcmScheduleReport/consume.vue

@@ -39,7 +39,7 @@
           icon="el-icon-plus"
           size="mini"
           @click="handleAdd"
-          v-hasPermi="['system:consume:add']"
+          v-hasPermi="['company:consume:add']"
         >新增</el-button>
       </el-col>
       <el-col :span="1.5">
@@ -49,7 +49,7 @@
           size="mini"
           :disabled="single"
           @click="handleUpdate"
-          v-hasPermi="['system:consume:edit']"
+          v-hasPermi="['company:consume:edit']"
         >修改</el-button>
       </el-col>
       <el-col :span="1.5">
@@ -59,7 +59,7 @@
           size="mini"
           :disabled="multiple"
           @click="handleDelete"
-          v-hasPermi="['system:consume:remove']"
+          v-hasPermi="['company:consume:remove']"
         >删除</el-button>
       </el-col>
       <el-col :span="1.5">
@@ -68,7 +68,7 @@
           icon="el-icon-download"
           size="mini"
           @click="handleExport"
-          v-hasPermi="['system:consume:export']"
+          v-hasPermi="['company:consume:export']"
         >导出</el-button>
       </el-col>
 	  <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>

+ 8 - 7
src/views/qw/qwChat/index.vue

@@ -41,12 +41,13 @@
 import { getQrCode } from '@/api/qw/login';
 import { getDeviceId} from '@/api/qw/account';
 
-import LemonMessageVoice from "lemon-imui/examples/lemon-message-voice";
-import QQIMUI from "lemon-imui/examples/qq";
-import ContactsData from "../database/contacts";
-import MessagesData from "../database/messages";
-import EmojiData from "../database/emoji";
-import 'lemon-imui/dist/index.css';
+import Search from '@/components/HeaderSearch'
+
+import LemonMessageVoice from "@/components/LemonUI/components/lemon-message-voice";
+import ContactsData from "@/components/LemonUI/database/contacts";
+import MessagesData from "@/components/LemonUI/database/messages";
+import EmojiData from "@/components/LemonUI/database/emoji";
+import '@/components/LemonUI/index.css';
 
 const tip = `export default {
           //组件的name必须以lemonMessage开头,后面跟上 Message.type
@@ -117,7 +118,7 @@ const generateMessage = (toContactId = "", fromUser) => {
 
 export default {
   name: "qwChat",
-  components: { LemonMessageVoice,QQIMUI },
+  components: { LemonMessageVoice },
   data() {
     return {
       theme: "default",

+ 5 - 5
src/views/qw/qwChat/qq.vue

@@ -101,12 +101,12 @@
 import { getQrCode } from '@/api/qw/login';
 import { getDeviceId} from '@/api/qw/account';
 
-import LemonMessageVoice from "lemon-imui/examples/lemon-message-voice";
+import LemonMessageVoice from "@/components/LemonUI/components/lemon-message-voice";
 
-import ContactsData from "../database/contacts";
-import MessagesData from "../database/messages";
-import EmojiData from "../database/emoji";
-import 'lemon-imui/dist/index.css';
+import ContactsData from "@/components/LemonUI/database/contacts";
+import MessagesData from "@/components/LemonUI/database/messages";
+import EmojiData from "@/components/LemonUI/database/emoji";
+import '@/components/LemonUI/index.css';
 
 export default {
   name: "qqChat",

+ 2 - 1
vue.config.js

@@ -54,7 +54,8 @@ module.exports = {
     name: name,
     resolve: {
       alias: {
-        '@': resolve('src')
+        '@': resolve('src'),
+        'styles': resolve("src/components/LemonUI/styles")
       }
     }
   },

部分文件因文件數量過多而無法顯示