修复自定义指令lemon-contextmenu报错的问题

This commit is contained in:
范君
2021-02-01 15:16:37 +08:00
parent 134353495f
commit 740d9a527d
36 changed files with 16518 additions and 691 deletions
-1
View File
@@ -1,6 +1,5 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
+9 -9
View File
@@ -1,14 +1,12 @@
# Lemon IMUI
[中文文档](docs/APIs_zh.md).
基于 VUE 2.0 的 IM 聊天组件
#### 特性
- 拥有丰富的自定义功能,任意搭配出不同风格的聊天界面
- 可以单独使用内部组件,比如编辑框/按钮/popover 等
- 不依赖任何第三方组件库
- 可任意扩展的聊天消息类型
- 不依赖任何第三方 UI 组件库
- 可任意扩展聊天消息类型
#### 安装
@@ -17,15 +15,17 @@
#### 使用
```javascript
import LemonIMUI from 'lemon-imui'
import "lemon-imui/dist/index.css";
Vue.use(LemonIMUI)
import LemonIMUI from 'lemon-imui';
import 'lemon-imui/dist/index.css';
Vue.use(LemonIMUI);
```
```html
<lemon-imui ref="IMUI" />
```
#### 示例
#### 示例 · 文档
[lemon-imui-examples](http://june000.gitee.io/lemon-im).
[lemon-imui](http://june000.gitee.io/lemon-im).
[QQ 交流群:1081773406](https://qm.qq.com/cgi-bin/qm/qr?k=MzwO4MT20zYQEXP8gq-GbjSJFA0qK15_&jump_from=webapi).
+10
View File
@@ -0,0 +1,10 @@
<meta charset="utf-8">
<title>index demo</title>
<script src="./index.umd.js"></script>
<link rel="stylesheet" href="./index.css">
<script>
console.log(index)
</script>
+7012
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
+7022
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
-232
View File
@@ -1,232 +0,0 @@
# Lemon-IMUI
### Contents
- contact
```javascript
{
//用户唯一ID
id: "",
//昵称
displayName: "工作协作群",
//头像URL
avatar: "http://upload.qqbodys.com/img/weixin/20170804/ji5qxg1am5ztm.jpg",
//会话类型 single | many
type: "single",
//通讯录索引,默认根据字母排序,也可以手动排序“[1]最近联系人”
index: "A",
//未读消息
unread: 0,
//最近消息时间
lastSendTime: 1566047865417,
//最近消息内容
lastContent: "2"
}
```
- message
```javascript
{
//消息唯一ID
id: "",
status: "succeed",
//消息类型 voice | file | video | image | text
type: "text",
//消息发送时间
sendTime: 1572415923000,
//消息内容 | URL
content: generateRandWord(),
//文件大小
fileSize: 1231,
//文件名称
fileName: "asdasd.doc",
//当前会话ID
toContactId:"",
//发送消息的用户
fromUser:{
id: "system",
displayName: "系统测试",
avatar: "http://upload.qqbodys.com/allimg/1710/1035512943-0.jpg"
};
}
```
- menu
```javascript
{
//导航名称, 保留字段 lastMessages 和 contacts
name: "custom1",
//鼠标停留时显示文字
title: "自定义按钮1",
//未读角标
unread: 0,
//外观
render: menu => {
return <i class="lemon-icon-attah" />;
},
//打开内容
renderContainer: () => {
return <div>自定义</div>;
},
//强制显示在底部
isBottom: true
}
```
### Props
- user
```javascript
{
id:'',
avatar:'',
displayName:'',
}
```
个人信息
- currentContactId
当前会话联系人 ID
- currentContact
当前会话联系人信息
- messageTimeFormat
消息列表时间格式化函数
- contactTimeFormat
联系人时间格式化规则
- hideDrawer
是否隐藏抽屉
- hideMenuAvatar
是否隐藏导航头像
- hideMenuAvatar
是否隐藏导航
### Methods
- initMenus([menu]);
初始化导航
- initContacts([contact]);
初始化联系人
- initEmoji()
初始化表情
```javascript
IMUI.initEmoji([
{
label: '表情',
children: [
{
name: '1f600',
title: '微笑',
src: 'https://twemoji.maxcdn.com/2/72x72/1f600.png'
},
{
name: '1f62c',
title: '微笑',
src: 'https://twemoji.maxcdn.com/2/72x72/1f62c.png'
}
]
},
{
label: '收藏',
children: [
{
name: '1f62c',
title: '微笑',
src: 'https://twemoji.maxcdn.com/2/72x72/1f62c.png'
}
]
}
])
```
- appendMessage(message)
在当前聊天窗口插入新消息
- removeMessage(messageId, contactId)
删除聊天消息
- updateMessage(messageId, contactId, message)
修改聊天聊天消息
- updateContact(contactId,contact)
修改联系人
- getMessages(contactId)
返回所有本地消息,传入 contactId 只返回与该联系人的消息
- getContacts()
获取所有联系人
- openDrawer(vnode)
打开抽屉
- closeDrawer()
关闭抽屉
- changeDrawer(vnode)
切换抽屉显示
- changeMenu(menuName)
切换导航
- changeContact(contactId)
切换聊天对象
- messageViewToBottom()
将当前聊天窗口滚动到底部
- setLastContentRender(messageType, render)
配置联系人列表最新消息的渲染函数
```javascript
IMUI.setLastContentRender('image', message => {
return <span>[最新图片]</span>
})
```
- lastContentRender(message)
根据 message 渲染联系人列表最新消息 DOM
```javascript
IMUI.updateContact(contact.id, {
lastContent: IMUI.lastContentRender(message)
})
```
### Scoped Slot
- cover
自定义聊天封面
- contact-title 参数{ contact }
自定义联系人标题
- message-sidebar
插入到最新消息列顶部
- contact-sidebar
插入到联系人列顶部
- contact-info 参数{ contact }
自定义联系人信息
### Events
- change-menu(menuName)
切换导航
- change-contact(contact)
切换导航会话
- pull-messages(contact,next)
拉取新消息
- next([message],isEnd) [isEnd 是否无更多数据]
- message-click(event, key, message)
- event 事件
- key 触发目标
- message 消息内容
- menu-avatar-click()
点击导航头像
- send(message, next, file)
- message 当前消息体
- next(message) 调用该函数完成消息发送
- file 上传的文件
+1482 -101
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><link rel=icon href=favicon.ico><title>Lemon IMUI</title><link href=css/index.08b1f4f3.css rel=preload as=style><link href=js/chunk-vendors.e4810482.js rel=preload as=script><link href=js/index.20b5dfe7.js rel=preload as=script><link href=css/index.08b1f4f3.css rel=stylesheet></head><body><noscript><strong>We're sorry but flat-im doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=js/chunk-vendors.e4810482.js></script><script src=js/index.20b5dfe7.js></script></body></html>
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><link rel=icon href=favicon.ico><title>Lemon IMUI</title><link href=css/index.e91d4c69.css rel=preload as=style><link href=js/chunk-vendors.2abee366.js rel=preload as=script><link href=js/index.e80c8c21.js rel=preload as=script><link href=css/index.e91d4c69.css rel=stylesheet></head><body><noscript><strong>We're sorry but flat-im doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=js/chunk-vendors.2abee366.js></script><script src=js/index.e80c8c21.js></script></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+30
View File
@@ -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>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "lemon-imui",
"version": "1.0.4",
"version": "1.6.5",
"main": "dist/index.umd.min.js",
"description": "基于 VUE2.0 的 IM 聊天组件",
"homepage": "https://github.com/fanjyy/lemon-imui",
+10 -1
View File
@@ -1,12 +1,19 @@
<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
@@ -21,7 +28,7 @@ export default {
return (
<span
style={this.style}
class="lemon-avatar"
class={["lemon-avatar", { "lemon-avatar--circle": this.circle }]}
on-click={e => this.$emit("click", e)}
>
{this.imageFinishLoad && <i class={this.icon} />}
@@ -65,6 +72,8 @@ export default {
overflow hidden
vertical-align middle
border-radius 4px
+m(circle)
border-radius 50%
img
width 100%
height 100%
+21 -10
View File
@@ -2,12 +2,16 @@
export default {
name: "LemonButton",
props: {
color: {
type: String,
default: "default"
},
disabled: Boolean
},
render() {
return (
<button
class="lemon-button"
class={["lemon-button", `lemon-button--color-${this.color}`]}
disabled={this.disabled}
type="button"
on-click={this._handleClick}
@@ -47,13 +51,20 @@ export default {
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)
&:hover:not([disabled])
border-color #666
color #333
&:active
background-color #ddd
&[disabled]
cursor not-allowed
color #aaa
background #eee
+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>
+36 -26
View File
@@ -1,9 +1,17 @@
<script>
import { isString, isToday } from "utils/validate";
import { timeFormat } from "utils";
import { timeFormat, useScopedSlot } from "utils";
export default {
name: "LemonContact",
components: {},
inject: {
IMUI: {
from: "IMUI",
default() {
return this;
}
}
},
data() {
return {};
},
@@ -18,23 +26,33 @@ export default {
}
},
render() {
const { contact } = this;
return (
<div
class={["lemon-contact", { "lemon-contact--name-center": this.simple }]}
on-click={e => this._handleClick(e, contact)}
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"
native-on-click={e => this._handleBubbleClick(e, contact)}
>
<lemon-avatar
size={40}
native-on-click={e => this._handleAvatarClick(e, contact)}
src={contact.avatar}
/>
</lemon-badge>
<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>
@@ -54,24 +72,10 @@ export default {
</p>
)}
</div>
</div>
);
},
created() {},
mounted() {},
computed: {},
watch: {},
methods: {
];
},
_handleClick(e, data) {
this.$emit("click", data);
},
_handleAvatarClick(e, data) {
e.stopPropagation();
this.$emit("avatar-click", data);
},
_handleBubbleClick(e, data) {
e.stopPropagation();
this.$emit("bubble-click", data);
}
}
};
@@ -85,6 +89,7 @@ export default {
box-sizing border-box
overflow hidden
background #efefef
text-align left
p
margin 0
+m(active)
@@ -121,12 +126,17 @@ export default {
+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
+252 -56
View File
@@ -1,31 +1,118 @@
<script>
import { toEmojiName } from "utils";
import { toEmojiName, useScopedSlot, clearHtmlExcludeImg } from "utils";
const exec = (val, command = "insertHTML") => {
document.execCommand(command, false, val);
};
const selection = window.getSelection();
let lastSelectionRange;
let emojiData = [];
let isInitTool = false;
export default {
name: "LemonEditor",
inject: {
IMUI: {
from: "IMUI",
default() {
return this;
}
}
},
components: {},
props: {},
props: {
tools: {
type: Array,
default: () => []
},
sendText: {
type: String,
default: "发 送"
},
sendKey: {
type: Function,
default(e) {
return e.keyCode == 13 && e.ctrlKey === true;
}
}
},
data() {
this.clipboardBlob = null;
return {
//剪切板图片URL
clipboardUrl: "",
submitDisabled: true,
proxyTools: [],
accept: ""
};
},
created() {},
mounted() {
//this.$refs.fileInput.addEventListener("change", this._handleChangeFile);
created() {
if (this.tools && this.tools.length > 0) {
this.initTools(this.tools);
} else {
this.initTools([
{ name: "emoji" },
{ name: "uploadFile" },
{ name: "uploadImage" }
]);
}
this.IMUI.$on("change-contact", () => {
this.closeClipboardImage();
});
},
computed: {},
watch: {},
render() {
//<a-popover trigger="click" overlay-class-name="lemon-editor__emoji">
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"
@@ -35,26 +122,8 @@ export default {
onChange={this._handleChangeFile}
/>
<div class="lemon-editor__tool">
{emojiData.length > 0 && (
<lemon-popover class="lemon-editor__emoji">
<template slot="content">{this._renderEmojiTabs()}</template>
<div class="lemon-editor__tool-item">
<i class="lemon-icon-emoji" />
</div>
</lemon-popover>
)}
<div
class="lemon-editor__tool-item"
on-click={() => this._handleSelectFile("*")}
>
<i class="lemon-icon-folder" />
</div>
<div
class="lemon-editor__tool-item"
on-click={() => this._handleSelectFile("image/*")}
>
<i class="lemon-icon-image" />
</div>
<div class="lemon-editor__tool-left">{toolLeft}</div>
<div class="lemon-editor__tool-right">{toolRight}</div>
</div>
<div class="lemon-editor__inner">
<div
@@ -65,18 +134,22 @@ export default {
on-keydown={this._handleKeydown}
on-paste={this._handlePaste}
on-click={this._handleClick}
on-input={this._handleInput}
spellcheck="false"
/>
</div>
<div class="lemon-editor__footer">
<div class="lemon-editor__tip">使用 ctrl + enter 快捷发送消息</div>
<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>
@@ -84,6 +157,68 @@ export default {
);
},
methods: {
closeClipboardImage() {
this.clipboardUrl = "";
this.clipboardBlob = null;
},
sendClipboardImage() {
if (!this.clipboardBlob) return;
this.$emit("upload", this.clipboardBlob);
this.closeClipboardImage();
},
/**
* 初始化工具栏
*/
initTools(data) {
if (!data) 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(data)) {
const indexMap = {
emoji: 0,
uploadFile: 1,
uploadImage: 2
};
const indexKeys = Object.keys(indexMap);
tools = data.map(item => {
if (indexKeys.includes(item.name)) {
return {
...defaultTools[indexMap[item.name]],
...item
};
}
return item;
});
} else {
tools = defaultTools;
}
this.proxyTools = tools;
},
_saveLastRange() {
lastSelectionRange = selection.getRangeAt(0);
},
@@ -97,9 +232,6 @@ export default {
_handleClick() {
this._saveLastRange();
},
_handleInput() {
this._checkSubmitDisabled();
},
_renderEmojiTabs() {
const renderImageGrid = items => {
return items.map(item => (
@@ -131,45 +263,65 @@ export default {
_handleSelectEmoji(item) {
this._focusLastRange();
exec(`<img emoji-name="${item.name}" src="${item.src}"></img>`);
this._checkSubmitDisabled();
this._saveLastRange();
},
async _handleSelectFile(accept) {
async selectFile(accept) {
this.accept = accept;
await this.$nextTick();
this.$refs.fileInput.click();
},
_handlePaste(e) {
e.preventDefault();
const { clipboardData } = e;
const text = clipboardData.getData("text");
exec(text, "insertText");
// Array.from(clipboardData.items).forEach(item => {
// console.log(item.type);
// });
//e.target.innerText = text;
const clipboardData = e.clipboardData || window.clipboardData;
const text = clipboardData.getData("Text");
if (text) {
if (window.clipboardData) {
this.$refs.textarea.innerHTML = text;
} else {
exec(text, "insertText");
}
} 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._saveLastRange();
//this._checkSubmitDisabled();
this._checkSubmitDisabled();
},
_handleKeydown(e) {
const { keyCode } = e;
if (keyCode == 13) {
// e.preventDefault();
// document.execCommand("defaultParagraphSeparator", false, false);
// exec("<br>");
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 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 = !this.$refs.textarea.innerHTML.trim();
this.submitDisabled = !clearHtmlExcludeImg(
this.$refs.textarea.innerHTML.trim()
);
},
_handleSend(e) {
const text = this.getFormatValue();
@@ -190,6 +342,10 @@ export default {
initEmoji(data) {
emojiData = data;
this.$forceUpdate();
},
setValue(val) {
this.$refs.textarea.innerHTML = this.IMUI.emojiNameToImage(val);
this._checkSubmitDisabled();
}
}
};
@@ -199,28 +355,66 @@ export default {
gap = 10px;
+b(lemon-editor)
height 200px
position relative
flex-column()
+e(tool)
display flex
height 40px
align-items center
padding-left 5px
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
@@ -234,6 +428,8 @@ gap = 10px;
height 20px
padding 0 2px
pointer-events none
position relative
top -1px
vertical-align middle
+e(footer)
display flex
+374 -134
View File
@@ -1,6 +1,7 @@
<script>
import { useScopedSlot, fastDone, generateUUID } from "utils";
import { useScopedSlot, funCall, generateUUID, cloneDeep } from "utils";
import { isFunction, isString, isEmpty } from "utils/validate";
import contextmenu from "../directives/contextmenu";
import {
DEFAULT_MENUS,
DEFAULT_MENU_LASTMESSAGES,
@@ -10,11 +11,7 @@ import lastContentRender from "../lastContentRender";
import MemoryCache from "utils/cache/memory";
const CacheContactContainer = new MemoryCache();
const CacheMenuContainer = new MemoryCache();
const CacheMessageLoaded = new MemoryCache();
const messages = {};
const allMessages = {};
const emojiMap = {};
let renderDrawerContent = () => {};
@@ -26,6 +23,22 @@ export default {
};
},
props: {
width: {
type: String,
default: "850px"
},
height: {
type: String,
default: "580px"
},
theme: {
type: String,
default: "default"
},
simple: {
type: Boolean,
default: false
},
/**
* 消息时间格式化规则
*/
@@ -42,10 +55,23 @@ export default {
default: true
},
/**
* 初始化时是否隐藏导航按钮上的头像
* 是否隐藏导航按钮上的头像
*/
hideMenuAvatar: Boolean,
hideMenu: Boolean,
/**
* 是否隐藏消息列表内的联系人名字
*/
hideMessageName: Boolean,
/**
* 是否隐藏消息列表内的发送时间
*/
hideMessageTime: Boolean,
sendKey: Function,
sendText: String,
contextmenu: Array,
contactContextmenu: Array,
avatarCricle: Boolean,
user: {
type: Object,
default: () => {
@@ -54,12 +80,18 @@ export default {
}
},
data() {
this.CacheContactContainer = new MemoryCache();
this.CacheMenuContainer = new MemoryCache();
this.CacheMessageLoaded = new MemoryCache();
this.CacheDraft = new MemoryCache();
return {
drawerVisible: !this.hideDrawer,
currentContactId: "",
currentContactId: null,
currentMessages: [],
activeSidebar: DEFAULT_MENU_LASTMESSAGES,
contacts: [],
menus: []
menus: [],
editorTools: []
};
},
@@ -79,9 +111,6 @@ export default {
await this.$nextTick();
},
computed: {
currentMessages() {
return messages[this.currentContactId] || [];
},
currentContact() {
return this.contacts.find(item => item.id == this.currentContactId) || {};
},
@@ -124,9 +153,34 @@ export default {
...message
};
},
appendMessage(message, contactId = this.currentContactId) {
this._addMessage(message, contactId, 1);
this.messageViewToBottom();
/**
* 新增一条消息
*/
appendMessage(message, scrollToBottom = false) {
if (allMessages[message.toContactId] === undefined) {
this.updateContact({
id: message.toContactId,
unread: "+1",
lastSendTime: message.sendTime,
lastContent: this.lastContentRender(message)
});
} else {
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 = "+1";
}
this.updateContact(updateContact);
}
},
_emitSend(message, next, file) {
this.$emit(
@@ -134,20 +188,21 @@ export default {
message,
(replaceMessage = { status: "succeed" }) => {
next();
message = Object.assign(message, replaceMessage);
this.forceUpdateMessage(message.id);
this.updateMessage(Object.assign(message, replaceMessage));
},
file
);
},
_handleSend(text) {
const message = this._createMessage({ content: text });
this.appendMessage(message);
this.appendMessage(message, true);
this._emitSend(message, () => {
this.updateContact(message.toContactId, {
this.updateContact({
id: message.toContactId,
lastContent: this.lastContentRender(message),
lastSendTime: message.sendTime
});
this.CacheDraft.remove(message.toContactId);
});
},
_handleUpload(file) {
@@ -167,11 +222,12 @@ export default {
};
}
const message = this._createMessage(joinMessage);
this.appendMessage(message);
this.appendMessage(message, true);
this._emitSend(
message,
() => {
this.updateContact(message.toContactId, {
this.updateContact({
id: message.toContactId,
lastContent: this.lastContentRender(message),
lastSendTime: message.sendTime
});
@@ -180,26 +236,36 @@ export default {
);
},
_emitPullMessages(next) {
this._changeContactLock = true;
this.$emit(
"pull-messages",
this.currentContact,
(messages, isEnd = false) => {
this._addMessage(messages, this.currentContactId, 0);
CacheMessageLoaded.set(this.currentContactId, isEnd);
this.CacheMessageLoaded.set(this.currentContactId, isEnd);
if (isEnd == true) this.$refs.messages.loaded();
this.updateCurrentMessages();
this._changeContactLock = false;
next(isEnd);
}
},
this
);
},
clearCacheContainer(name) {
CacheContactContainer.remove(name);
CacheMenuContainer.remove(name);
this.CacheContactContainer.remove(name);
this.CacheMenuContainer.remove(name);
},
_renderWrapper(children) {
return (
<div
style={{
width: this.width,
height: this.height
}}
class={[
"lemon-wrapper",
`lemon-wrapper--theme-${this.theme}`,
{ "lemon-wrapper--simple": this.simple },
this.drawerVisible && "lemon-wrapper--drawer-show"
]}
>
@@ -245,7 +311,7 @@ export default {
{ "lemon-menu__item--active": this.activeSidebar == name }
]}
on-click={() => {
fastDone(click, () => {
funCall(click, () => {
if (name) this.changeMenu(name);
});
}}
@@ -264,51 +330,56 @@ export default {
_renderSidebarMessage() {
return this._renderSidebar(
[
useScopedSlot(this.$scopedSlots["message-sidebar"]),
useScopedSlot(this.$scopedSlots["sidebar-message-top"], null, this),
this.lastMessages.map(contact => {
return this._renderContact(
{
contact,
timeFormat: this.contactTimeFormat
},
() => this.changeContact(contact.id)
() => this.changeContact(contact.id),
this.$scopedSlots["sidebar-message"]
);
})
],
DEFAULT_MENU_LASTMESSAGES
DEFAULT_MENU_LASTMESSAGES,
useScopedSlot(this.$scopedSlots["sidebar-message-fixedtop"], null, this)
);
},
_renderContact(props, onClick) {
_renderContact(props, onClick, slot) {
const {
click: customClick,
renderContainer,
id: contactId
} = props.contact;
const click = () => {
fastDone(customClick, () => {
funCall(customClick, () => {
onClick();
this._customContainerReady(
renderContainer,
CacheContactContainer,
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 }}
></lemon-contact>
);
},
_renderSidebarContact() {
let prevIndex;
return this._renderSidebar(
[
useScopedSlot(this.$scopedSlots["contact-sidebar"]),
useScopedSlot(this.$scopedSlots["sidebar-contact-top"], null, this),
this.contacts.map(contact => {
if (!contact.index) return;
contact.index = contact.index.replace(/\[[0-9]*\]/, "");
@@ -321,20 +392,29 @@ export default {
contact: contact,
simple: true
},
() => this.changeContact(contact.id)
() => {
this.changeContact(contact.id);
},
this.$scopedSlots["sidebar-contact"]
)
];
prevIndex = contact.index;
return node;
})
],
DEFAULT_MENU_CONTACTS
DEFAULT_MENU_CONTACTS,
useScopedSlot(this.$scopedSlots["sidebar-contact-fixedtop"], null, this)
);
},
_renderSidebar(children, name) {
_renderSidebar(children, name, fixedtop) {
return (
<div class="lemon-sidebar" v-show={this.activeSidebar == name}>
{children}
<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>
);
},
@@ -356,22 +436,22 @@ export default {
const cls = "lemon-container";
const curact = this.currentContact;
let defIsShow = true;
for (const name in CacheContactContainer.get()) {
for (const name in this.CacheContactContainer.get()) {
const show = curact.id == name && this.currentIsDefSidebar;
defIsShow = !show;
nodes.push(
<div class={cls} v-show={show}>
{CacheContactContainer.get(name)}
{this.CacheContactContainer.get(name)}
</div>
);
}
for (const name in CacheMenuContainer.get()) {
for (const name in this.CacheMenuContainer.get()) {
nodes.push(
<div
class={cls}
v-show={this.activeSidebar == name && !this.currentIsDefSidebar}
>
{CacheMenuContainer.get(name)}
{this.CacheMenuContainer.get(name)}
</div>
);
}
@@ -384,7 +464,7 @@ export default {
<div class="lemon-container__title">
<div class="lemon-container__displayname">
{useScopedSlot(
this.$scopedSlots["contact-title"],
this.$scopedSlots["message-title"],
curact.displayName,
curact
)}
@@ -392,6 +472,8 @@ export default {
</div>
<lemon-messages
ref="messages"
hide-time={this.hideMessageTime}
hide-name={this.hideMessageName}
time-format={this.messageTimeFormat}
reverse-user-id={this.user.id}
on-reach-top={this._emitPullMessages}
@@ -399,6 +481,9 @@ export default {
/>
<lemon-editor
ref="editor"
tools={this.editorTools}
sendText={this.sendText}
sendKey={this.sendKey}
onSend={this._handleSend}
onUpload={this._handleUpload}
/>
@@ -421,6 +506,12 @@ export default {
<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);
}}
>
@@ -433,12 +524,14 @@ export default {
);
return nodes;
},
_handleSidebarScroll() {
contextmenu.hide();
},
_addContact(data, t) {
const type = {
0: "unshift",
1: "push"
}[t];
//this.contacts[type](cloneDeep(data));
this.contacts[type](data);
},
_addMessage(data, contactId, t) {
@@ -447,10 +540,8 @@ export default {
1: "push"
}[t];
if (!Array.isArray(data)) data = [data];
messages[contactId] = messages[contactId] || [];
messages[contactId][type](...data);
//console.log(messages[contactId]);
this.forceUpdateMessage();
allMessages[contactId] = allMessages[contactId] || [];
allMessages[contactId][type](...data);
},
/**
* 设置最新消息DOM
@@ -461,6 +552,12 @@ export default {
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);
},
/**
@@ -468,14 +565,22 @@ export default {
* @param {String} str 被替换的字符串
* @return {String} 替换后的字符串
*/
replaceEmojiName(str) {
emojiNameToImage(str) {
return str.replace(/\[!(\w+)\]/gi, (str, match) => {
const file = match;
return emojiMap[file]
? `<img src="${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];
},
/**
* 将当前聊天窗口滚动到底部
*/
@@ -486,30 +591,54 @@ export default {
* 改变聊天对象
* @param contactId 联系人 id
*/
changeContact(contactId, menuName) {
if (this.currentContactId == contactId) {
this.currentContactId = undefined;
}
async changeContact(contactId, menuName) {
if (menuName) {
this.changeMenu(menuName);
} else {
if (this._changeContactLock || this.currentContactId == contactId)
return false;
}
const prevCurrentContactId = this.currentContactId;
//保存上个聊天目标的草稿
if (prevCurrentContactId) {
const editorValue = this.getEditorValue();
if (editorValue) {
this.CacheDraft.set(prevCurrentContactId, editorValue);
this.updateContact({
id: prevCurrentContactId,
lastContent: `<span style="color:red;">[草稿]</span><span>${this.lastContentRender(
{ type: "text", content: editorValue }
)}</span>`
});
this.setEditorValue("");
}
}
this.currentContactId = contactId;
this.$emit("change-contact", this.currentContact);
if (!this.currentContactId) return false;
this.$emit("change-contact", this.currentContact, this);
if (isFunction(this.currentContact.renderContainer)) {
return;
}
if (this._menuIsMessages()) {
if (!CacheMessageLoaded.has(contactId)) {
this.$refs.messages.resetLoadState();
}
if (!messages[contactId]) {
this._emitPullMessages(isEnd => this.messageViewToBottom());
} else {
setTimeout(() => {
this.messageViewToBottom();
}, 0);
}
//填充草稿内容
const draft = this.CacheDraft.get(contactId) || "";
if (draft) this.setEditorValue(draft);
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);
}
},
/**
@@ -517,28 +646,28 @@ export default {
* @param messageId 消息 id
* @param contactId 联系人 id
*/
removeMessage(messageId, contactId) {
const index = this.findMessageIndexById(messageId, contactId);
if (index !== -1) {
messages[contactId].splice(index, 1);
this.forceUpdateMessage();
}
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(messageId, contactId, data) {
const index = this.findMessageIndexById(messageId, contactId);
if (index !== -1) {
messages[contactId][index] = Object.assign(
messages[contactId][index],
data
);
console.log("--------", messages[contactId][index]);
this.forceUpdateMessage(messageId);
}
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;
},
/**
* 手动更新对话消息
@@ -567,6 +696,7 @@ export default {
* @param {String} name 按钮 name
*/
changeMenu(name) {
if (this._changeContactLock) return false;
this.$emit("change-menu", name);
this.activeSidebar = name;
},
@@ -577,11 +707,20 @@ export default {
* EmojiItem = {name: wx,title: 微笑,src: url} 无分组
*/
initEmoji(data) {
let flatData = [];
this.$refs.editor.initEmoji(data);
if (data[0].label) {
data = data.flatMap(item => item.children);
data.forEach(item => {
flatData.push(...item.children);
});
} else {
flatData = data;
}
data.forEach(({ name, src }) => (emojiMap[name] = src));
flatData.forEach(({ name, src }) => (emojiMap[name] = src));
},
initEditorTools(data) {
this.editorTools = data;
this.$refs.editor.initTools(data);
},
/**
* 初始化左侧按钮
@@ -613,7 +752,7 @@ export default {
let menus = [];
if (Array.isArray(data)) {
const indexMap = {
lastMessages: 0,
messages: 0,
contacts: 1
};
const indexKeys = Object.keys(indexMap);
@@ -629,7 +768,7 @@ export default {
if (item.renderContainer) {
this._customContainerReady(
item.renderContainer,
CacheMenuContainer,
this.CacheMenuContainer,
item.name
);
}
@@ -647,7 +786,7 @@ export default {
* @param {Array<Contact>} data 联系人列表
*/
initContacts(data) {
this.contacts.push(...data);
this.contacts = data;
this.sortContacts();
},
/**
@@ -659,13 +798,43 @@ export default {
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: "",
lastSendTime: ""
},
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 修改的数据,根据 data.id 查找联系人并覆盖传入的值
* @param {Contact} data 修改的数据,根据 Contact.id 查找联系人并覆盖传入的值
*/
updateContact(contactId, data) {
updateContact(data) {
const contactId = data.id;
delete data.id;
delete data.toContactId;
const index = this.findContactIndexById(contactId);
if (index !== -1) {
@@ -690,16 +859,22 @@ export default {
findContactIndexById(contactId) {
return this.contacts.findIndex(item => item.id == contactId);
},
findMessageIndexById(messageId, contactId) {
const msg = messages[contactId];
if (isEmpty(msg)) {
return -1;
}
return msg.findIndex(item => item.id == messageId);
/**
* 根据 id 查找判断是否存在联系人
* @param contactId 联系人 id
* @return {Boolean}
*/
hasContact(contactId) {
return this.findContactIndexById(contactId) !== -1;
},
findMessageById(messageId, contactId) {
const index = this.findMessageIndexById(messageId, contactId);
if (index !== -1) return messages[contactId][index];
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);
},
/**
* 返回所有联系人
@@ -708,36 +883,26 @@ export default {
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();
},
/**
* 返回所有消息
* @return {Object<Contact.id,Message>}
*/
getMessages(contactId) {
return (contactId ? messages[contactId] : messages) || [];
},
// appendContact(data) {
// this._addContact(data, 0);
// },
// prependContact(data) {
// this._addContact(data, 1);
// },
// addContactMessage(data) {
// this._addContact(data, 0);
// },
// prependContactMessage(data) {
// this._addContact(data, 1);
// },
// appendMessage(data) {},
// prependMessage(data) {},
// removeContact(contactId) {},
// removeContactMessage(contactId) {},
// removeContactAll(contactId) {},
/**
* 将自定义的HTML显示在主窗口内
*/
openrenderContainer(vnode) {
//renderContainerQueue[this.activeSidebar] = vnode;
//this.$slots._renderContainer = vnode;
return (contactId ? allMessages[contactId] : allMessages) || [];
},
changeDrawer(render) {
this.drawerVisible = !this.drawerVisible;
@@ -754,16 +919,14 @@ export default {
};
</script>
<style lang="stylus">
wrapper-width = 850px
drawer-width = 200px
bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
@import '~styles/utils/index'
+b(lemon-wrapper)
width wrapper-width
height 580px
display flex
font-size 14px
font-family "Microsoft YaHei"
//mask-image radial-gradient(circle, white 100%, black 100%)
background #efefef
transition all .4s bezier
@@ -812,13 +975,18 @@ bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
+b(lemon-sidebar)
width 250px
background #efefef
overflow-y auto
scrollbar-light()
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)
@@ -827,7 +995,7 @@ bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
background #f4f4f4
word-break()
position relative
z-index 2
z-index 10
+e(title)
padding 15px 15px
+e(displayname)
@@ -842,11 +1010,10 @@ bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
overflow hidden
background #f4f4f4
transition width .4s bezier
z-index 1
z-index 9
width drawer-width
height 100%
box-sizing border-box
//border-left 1px solid #e9e9e9
+b(lemon-wrapper)
+m(drawer-show)
+b(lemon-drawer)
@@ -861,4 +1028,77 @@ bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
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 10
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>
+95 -50
View File
@@ -1,8 +1,17 @@
<script>
import { useScopedSlot } from "utils";
export default {
name: "lemonMessageBasic",
inject: ["IMUI"],
inject: {
IMUI: {
from: "IMUI",
default() {
return this;
}
}
},
props: {
contextmenu: Array,
message: {
type: Object,
default: () => {
@@ -14,7 +23,8 @@ export default {
default: () => ""
},
reverse: Boolean,
hiddenTitle: Boolean
hideName: Boolean,
hideTime: Boolean
},
data() {
return {};
@@ -25,9 +35,10 @@ export default {
<div
class={[
"lemon-message",
`lemon-message--status-${status}`,
{
"lemon-message--reverse": this.reverse,
"lemon-message--hidden-title": this.hiddenTitle
"lemon-message--hide-name": this.hideName
}
]}
>
@@ -50,23 +61,50 @@ export default {
>
{fromUser.displayName}
</span>
<span class="lemon-message__time">{this.timeFormat(sendTime)}</span>
{this.hideTime == true && (
<span
class="lemon-message__time"
on-click={e => {
this._emitClick(e, "sendTime");
}}
>
{this.timeFormat(sendTime)}
</span>
)}
</div>
<div
class="lemon-message__content"
on-click={e => {
this._emitClick(e, "content");
}}
>
{this.useScopedSlots("content", this.message)}
</div>
<div
class="lemon-message__status"
on-click={e => {
this._emitClick(e, "status");
}}
>
{this._renderStatue(status)}
<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>
@@ -78,29 +116,7 @@ export default {
watch: {},
methods: {
_emitClick(e, key) {
this.IMUI.$emit("message-click", e, key, this.message);
},
_renderStatue(status) {
if (status == "going") {
return <i class="lemon-icon-loading lemonani-spin" />;
} else if (status == "failed") {
return (
<i
class="lemon-icon-prompt"
title="重发消息"
style={{
color: "#ff2525",
cursor: "pointer"
}}
/>
);
}
return;
},
useScopedSlots(name, params, defVnode = "", context = this) {
return context.$scopedSlots[name]
? context.$scopedSlots[name](params)
: defVnode;
this.IMUI.$emit("message-click", e, key, this.message, this.IMUI);
}
}
};
@@ -118,8 +134,8 @@ arrow()
display flex
padding 10px 0
+e(time)
color #bbb
padding 0 4px
color #b9b9b9
padding 0 5px
+e(inner)
position relative
+e(avatar)
@@ -133,7 +149,9 @@ arrow()
line-height 14px
padding-bottom 6px
user-select none
color #999
color #666
+e(content-flex)
display flex
+e(content)
font-size 14px
line-height 20px
@@ -141,7 +159,7 @@ arrow()
background #fff
border-radius 4px
position relative
margin 0 46px 0 0
margin 0
img
video
background #e9e9e9
@@ -151,22 +169,49 @@ 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 20px
left 26px
right auto
+e(content)
background #35d863
margin 0 0 0 46px
&:before
arrow()
left auto
@@ -178,9 +223,9 @@ arrow()
+e(avatar)
padding-right 0
padding-left 10px
+m(hidden-title)
+m(hide-name)
+e(status)
top 7px
top 3px
+e(title)
display none
+e(content)
+12 -1
View File
@@ -2,13 +2,24 @@
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">{content}</span>
<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>
+3 -1
View File
@@ -10,7 +10,7 @@ export default {
props={{ ...this.$attrs }}
scopedSlots={{
content: props => {
const content = this.IMUI.replaceEmojiName(props.content);
const content = this.IMUI.emojiNameToImage(props.content);
return <span domProps={{ innerHTML: content }} />;
}
}}
@@ -29,6 +29,8 @@ export default {
height 18px
display inline-block
background transparent
position relative
top -1px
padding 0 2px
vertical-align middle
</style>
+22 -13
View File
@@ -1,9 +1,14 @@
<script>
import { hoursTimeFormat } from "utils";
import contextmenu from "../directives/contextmenu";
export default {
name: "LemonMessages",
components: {},
props: {
//是否隐藏消息发送人昵称
hideName: Boolean,
//是否隐藏显示消息时间
hideTime: Boolean,
reverseUserId: String,
timeRange: {
type: Number,
@@ -52,24 +57,26 @@ export default {
message: {
id: "__time__",
type: "event",
content: this.timeFormat(message.sendTime)
content: hoursTimeFormat(message.sendTime)
}
}}
/>
);
}
node.push(
<tagName
ref="message"
refInFor={true}
attrs={{
timeFormat: this.msecRange > 0 ? () => {} : this.timeFormat,
message: message,
reverse: this.reverseUserId == message.fromUser.id,
hiddenTitle: false
}}
/>
);
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>
@@ -90,6 +97,7 @@ export default {
},
loaded() {
this._loadend = true;
this.$forceUpdate();
},
resetLoadState() {
this._loading = false;
@@ -97,6 +105,7 @@ export default {
},
async _handleScroll(e) {
const { target } = e;
contextmenu.hide();
if (
target.scrollTop == 0 &&
this._loading == false &&
+11 -8
View File
@@ -1,6 +1,6 @@
<script>
const popoverCloseQueue = [];
const popoverCloseAll = () => popoverCloseQueue.forEach(callback => callback());
import contextmenu from "../directives/contextmenu";
const triggerEvents = {
hover(el) {},
focus(el) {
@@ -14,6 +14,7 @@ const triggerEvents = {
click(el) {
el.addEventListener("click", e => {
e.stopPropagation();
contextmenu.hide();
this.changeVisible();
});
},
@@ -51,7 +52,7 @@ export default {
render() {
return (
<span style="position:relative">
<transition name="slide-top">
<transition name="lemon-slide-top">
{this.visible && (
<div
class="lemon-popover"
@@ -59,7 +60,6 @@ export default {
style={this.popoverStyle}
on-click={e => e.stopPropagation()}
>
<div class="lemon-popover__title" />
<div class="lemon-popover__content">{this.$slots.content}</div>
<div class="lemon-popover__arrow" />
</div>
@@ -96,9 +96,12 @@ export default {
this.visible ? this.close() : this.open();
},
open() {
popoverCloseAll();
this.closeAll();
this.visible = true;
},
closeAll() {
popoverCloseQueue.forEach(callback => callback());
},
close() {
this.visible = false;
}
@@ -117,7 +120,7 @@ export default {
z-index 10
background-color #fff
border-radius 4px
box-shadow 0 2px 8px rgba(0, 0, 0, 0.15)
box-shadow 0 2px 8px rgba(0, 0, 0, 0.08)
position absolute
transform-origin 50% 150%
+e(content)
@@ -135,9 +138,9 @@ export default {
width 8px
height 8px
background #fff
.slide-top-leave-active ,.slide-top-enter-active
transition all .3s cubic-bezier(0.645, 0.045, 0.355, 1)
.slide-top-enter, .slide-top-leave-to
.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>
+75
View File
@@ -0,0 +1,75 @@
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("contextmenu", e => {
if (isEmpty(binding.value) || !Array.isArray(binding.value)) return;
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();
});
},
inserted(el, binding, vnode) {}
};
+3 -1
View File
@@ -1,3 +1,4 @@
import Contextmenu from "./directives/contextmenu";
import LemonTabs from "./components/tabs";
import LemonPopover from "./components/popover";
import LemonButton from "./components/button";
@@ -14,7 +15,7 @@ import lemonMessageEvent from "./components/message/event";
import LemonIMUI from "./components/index";
import "./styles/common/index.styl";
const version = "0.1";
const version = "1.4.2";
const components = [
LemonIMUI,
LemonContact,
@@ -32,6 +33,7 @@ const components = [
lemonMessageEvent
];
const install = (Vue, opts = {}) => {
Vue.directive("LemonContextmenu", Contextmenu);
components.forEach(component => {
Vue.component(component.name, component);
});
+6 -8
View File
@@ -1,17 +1,15 @@
import {clearHtml } from 'utils';
export default {
voice(message) {
return "[语音]";
},
file(message) {
return "[文件]";
},
video(message) {
return "[视频]";
},
image(message) {
return "[图片]";
},
text(message) {
return this.replaceEmojiName(message.content);
}
return this.emojiNameToImage(clearHtml(message.content));
},
event(message){
return '[通知]';
},
};
+10 -7
View File
@@ -1,12 +1,15 @@
// @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('//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');
src:url('../fonts/icon.woff') format('woff');
}
[class^='lemon-icon-'],
[class*=' lemon-icon-']
Binary file not shown.
-15
View File
@@ -22,18 +22,3 @@ export default class MemoryCache {
return !!this.table[key];
}
}
// export default {
// data: {},
// get(name) {
// console.log(this.data);
// }
// };
// class MemoryCache {
// constructor() {
// super();
// }
// get($name) {
// console.log(1);
// }
// }
// export default MemoryCache;
+1 -1
View File
@@ -1,6 +1,6 @@
export const EMIT_AVATAR_CLICK = "avatar-click";
export const DEFAULT_MENU_LASTMESSAGES = "lastMessages";
export const DEFAULT_MENU_LASTMESSAGES = "messages";
export const DEFAULT_MENU_CONTACTS = "contacts";
export const DEFAULT_MENUS = [DEFAULT_MENU_LASTMESSAGES, DEFAULT_MENU_CONTACTS];
/**
+9 -5
View File
@@ -53,7 +53,7 @@ export function timeFormat(t, format) {
return format;
}
export function fastDone(event, callback) {
export function funCall(event, callback) {
if (isFunction(event)) {
event(() => {
callback();
@@ -70,7 +70,14 @@ export function fastDone(event, callback) {
export function arrayIntersect(a, b) {
return a.filter(x => b.includes(x));
}
//清除字符串内的所有HTML标签
export function clearHtml(str){
return str.replace(/<.*?>/ig,"");
}
//清除字符串内的所有HTML标签,除了IMG
export function clearHtmlExcludeImg(str){
return str.replace(/<(?!img).*?>/ig, "");
}
export function error(text) {
throw new Error(text);
}
@@ -96,9 +103,6 @@ export function mergeDeep(o1, o2) {
return o1;
}
export function toEmojiName(str) {
return str.replace(/<img emoji-name=\"([^\"]*?)\" [^>]*>/gi, "[!$1]");
}
export function formatByte(value) {
if (null == value || value == "") {
return "0 Bytes";