修复自定义指令lemon-contextmenu报错的问题
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
# Lemon IMUI
|
# Lemon IMUI
|
||||||
|
|
||||||
[中文文档](docs/APIs_zh.md).
|
|
||||||
基于 VUE 2.0 的 IM 聊天组件
|
基于 VUE 2.0 的 IM 聊天组件
|
||||||
|
|
||||||
#### 特性
|
#### 特性
|
||||||
|
|
||||||
- 拥有丰富的自定义功能,任意搭配出不同风格的聊天界面
|
- 拥有丰富的自定义功能,任意搭配出不同风格的聊天界面
|
||||||
- 可以单独使用内部组件,比如编辑框/按钮/popover 等
|
- 不依赖任何第三方 UI 组件库
|
||||||
- 不依赖任何第三方组件库
|
- 可任意扩展聊天消息类型
|
||||||
- 可任意扩展的聊天消息类型
|
|
||||||
|
|
||||||
#### 安装
|
#### 安装
|
||||||
|
|
||||||
@@ -17,15 +15,17 @@
|
|||||||
#### 使用
|
#### 使用
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import LemonIMUI from 'lemon-imui'
|
import LemonIMUI from 'lemon-imui';
|
||||||
import "lemon-imui/dist/index.css";
|
import 'lemon-imui/dist/index.css';
|
||||||
Vue.use(LemonIMUI)
|
Vue.use(LemonIMUI);
|
||||||
```
|
```
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<lemon-imui ref="IMUI" />
|
<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).
|
||||||
|
|||||||
Vendored
+10
@@ -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>
|
||||||
Vendored
+7012
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+7022
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
-232
@@ -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
File diff suppressed because it is too large
Load Diff
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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>
|
||||||
+7
File diff suppressed because one or more lines are too long
-7
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -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} 🔈</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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lemon-imui",
|
"name": "lemon-imui",
|
||||||
"version": "1.0.4",
|
"version": "1.6.5",
|
||||||
"main": "dist/index.umd.min.js",
|
"main": "dist/index.umd.min.js",
|
||||||
"description": "基于 VUE2.0 的 IM 聊天组件",
|
"description": "基于 VUE2.0 的 IM 聊天组件",
|
||||||
"homepage": "https://github.com/fanjyy/lemon-imui",
|
"homepage": "https://github.com/fanjyy/lemon-imui",
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "LemonAvatar",
|
name: "LemonAvatar",
|
||||||
|
inject: ["IMUI"],
|
||||||
props: {
|
props: {
|
||||||
src: String,
|
src: String,
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "lemon-icon-people"
|
default: "lemon-icon-people"
|
||||||
},
|
},
|
||||||
|
circle: {
|
||||||
|
type: Boolean,
|
||||||
|
default() {
|
||||||
|
return this.IMUI ? this.IMUI.avatarCricle : false;
|
||||||
|
}
|
||||||
|
},
|
||||||
size: {
|
size: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 32
|
default: 32
|
||||||
@@ -21,7 +28,7 @@ export default {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
style={this.style}
|
style={this.style}
|
||||||
class="lemon-avatar"
|
class={["lemon-avatar", { "lemon-avatar--circle": this.circle }]}
|
||||||
on-click={e => this.$emit("click", e)}
|
on-click={e => this.$emit("click", e)}
|
||||||
>
|
>
|
||||||
{this.imageFinishLoad && <i class={this.icon} />}
|
{this.imageFinishLoad && <i class={this.icon} />}
|
||||||
@@ -65,6 +72,8 @@ export default {
|
|||||||
overflow hidden
|
overflow hidden
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
|
+m(circle)
|
||||||
|
border-radius 50%
|
||||||
img
|
img
|
||||||
width 100%
|
width 100%
|
||||||
height 100%
|
height 100%
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
export default {
|
export default {
|
||||||
name: "LemonButton",
|
name: "LemonButton",
|
||||||
props: {
|
props: {
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: "default"
|
||||||
|
},
|
||||||
disabled: Boolean
|
disabled: Boolean
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
class="lemon-button"
|
class={["lemon-button", `lemon-button--color-${this.color}`]}
|
||||||
disabled={this.disabled}
|
disabled={this.disabled}
|
||||||
type="button"
|
type="button"
|
||||||
on-click={this._handleClick}
|
on-click={this._handleClick}
|
||||||
@@ -47,13 +51,20 @@ export default {
|
|||||||
background-color #fff
|
background-color #fff
|
||||||
box-shadow 0 2px 0 rgba(0, 0, 0, 0.015)
|
box-shadow 0 2px 0 rgba(0, 0, 0, 0.015)
|
||||||
text-shadow 0 -1px 0 rgba(0, 0, 0, 0.12)
|
text-shadow 0 -1px 0 rgba(0, 0, 0, 0.12)
|
||||||
&:hover:not([disabled])
|
+m(color-default)
|
||||||
border-color #666
|
&:hover:not([disabled])
|
||||||
color #333
|
border-color #666
|
||||||
&:active
|
color #333
|
||||||
background-color #ddd
|
&:active
|
||||||
&[disabled]
|
background-color #ddd
|
||||||
cursor not-allowed
|
&[disabled]
|
||||||
color #aaa
|
cursor not-allowed
|
||||||
background #eee
|
color #aaa
|
||||||
|
background #eee
|
||||||
|
+m(color-grey)
|
||||||
|
background #e1e1e1
|
||||||
|
border-color #e1e1e1
|
||||||
|
color #666
|
||||||
|
&:hover:not([disabled])
|
||||||
|
border-color #bbb
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import { isString, isToday } from "utils/validate";
|
import { isString, isToday } from "utils/validate";
|
||||||
import { timeFormat } from "utils";
|
import { timeFormat, useScopedSlot } from "utils";
|
||||||
export default {
|
export default {
|
||||||
name: "LemonContact",
|
name: "LemonContact",
|
||||||
components: {},
|
components: {},
|
||||||
|
inject: {
|
||||||
|
IMUI: {
|
||||||
|
from: "IMUI",
|
||||||
|
default() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
@@ -18,23 +26,33 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
const { contact } = this;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={["lemon-contact", { "lemon-contact--name-center": this.simple }]}
|
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
|
<lemon-badge
|
||||||
count={!this.simple ? contact.unread : 0}
|
count={!this.simple ? contact.unread : 0}
|
||||||
class="lemon-contact__avatar"
|
class="lemon-contact__avatar"
|
||||||
native-on-click={e => this._handleBubbleClick(e, contact)}
|
|
||||||
>
|
>
|
||||||
<lemon-avatar
|
<lemon-avatar size={40} src={contact.avatar} />
|
||||||
size={40}
|
</lemon-badge>,
|
||||||
native-on-click={e => this._handleAvatarClick(e, contact)}
|
|
||||||
src={contact.avatar}
|
|
||||||
/>
|
|
||||||
</lemon-badge>
|
|
||||||
<div class="lemon-contact__inner">
|
<div class="lemon-contact__inner">
|
||||||
<p class="lemon-contact__label">
|
<p class="lemon-contact__label">
|
||||||
<span class="lemon-contact__name">{contact.displayName}</span>
|
<span class="lemon-contact__name">{contact.displayName}</span>
|
||||||
@@ -54,24 +72,10 @@ export default {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
];
|
||||||
);
|
},
|
||||||
},
|
|
||||||
created() {},
|
|
||||||
mounted() {},
|
|
||||||
computed: {},
|
|
||||||
watch: {},
|
|
||||||
methods: {
|
|
||||||
_handleClick(e, data) {
|
_handleClick(e, data) {
|
||||||
this.$emit("click", 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
|
box-sizing border-box
|
||||||
overflow hidden
|
overflow hidden
|
||||||
background #efefef
|
background #efefef
|
||||||
|
text-align left
|
||||||
p
|
p
|
||||||
margin 0
|
margin 0
|
||||||
+m(active)
|
+m(active)
|
||||||
@@ -121,12 +126,17 @@ export default {
|
|||||||
+e(content)
|
+e(content)
|
||||||
font-size 12px
|
font-size 12px
|
||||||
color #999
|
color #999
|
||||||
|
height 18px
|
||||||
|
line-height 18px
|
||||||
|
margin-top 1px !important
|
||||||
ellipsis()
|
ellipsis()
|
||||||
img
|
img
|
||||||
height 14px
|
height 14px
|
||||||
display inline-block
|
display inline-block
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
margin 0 1px
|
margin 0 1px
|
||||||
|
position relative
|
||||||
|
top -1px
|
||||||
+m(name-center)
|
+m(name-center)
|
||||||
+e(label)
|
+e(label)
|
||||||
padding-bottom 0
|
padding-bottom 0
|
||||||
|
|||||||
+252
-56
@@ -1,31 +1,118 @@
|
|||||||
<script>
|
<script>
|
||||||
import { toEmojiName } from "utils";
|
import { toEmojiName, useScopedSlot, clearHtmlExcludeImg } from "utils";
|
||||||
const exec = (val, command = "insertHTML") => {
|
const exec = (val, command = "insertHTML") => {
|
||||||
document.execCommand(command, false, val);
|
document.execCommand(command, false, val);
|
||||||
};
|
};
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
let lastSelectionRange;
|
let lastSelectionRange;
|
||||||
let emojiData = [];
|
let emojiData = [];
|
||||||
|
let isInitTool = false;
|
||||||
export default {
|
export default {
|
||||||
name: "LemonEditor",
|
name: "LemonEditor",
|
||||||
|
inject: {
|
||||||
|
IMUI: {
|
||||||
|
from: "IMUI",
|
||||||
|
default() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {},
|
components: {},
|
||||||
props: {},
|
props: {
|
||||||
|
tools: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
sendText: {
|
||||||
|
type: String,
|
||||||
|
default: "发 送"
|
||||||
|
},
|
||||||
|
sendKey: {
|
||||||
|
type: Function,
|
||||||
|
default(e) {
|
||||||
|
return e.keyCode == 13 && e.ctrlKey === true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
|
this.clipboardBlob = null;
|
||||||
return {
|
return {
|
||||||
|
//剪切板图片URL
|
||||||
|
clipboardUrl: "",
|
||||||
submitDisabled: true,
|
submitDisabled: true,
|
||||||
|
proxyTools: [],
|
||||||
accept: ""
|
accept: ""
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {},
|
created() {
|
||||||
mounted() {
|
if (this.tools && this.tools.length > 0) {
|
||||||
//this.$refs.fileInput.addEventListener("change", this._handleChangeFile);
|
this.initTools(this.tools);
|
||||||
|
} else {
|
||||||
|
this.initTools([
|
||||||
|
{ name: "emoji" },
|
||||||
|
{ name: "uploadFile" },
|
||||||
|
{ name: "uploadImage" }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
this.IMUI.$on("change-contact", () => {
|
||||||
|
this.closeClipboardImage();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
computed: {},
|
|
||||||
watch: {},
|
|
||||||
render() {
|
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 (
|
return (
|
||||||
<div class="lemon-editor">
|
<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
|
<input
|
||||||
style="display:none"
|
style="display:none"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -35,26 +122,8 @@ export default {
|
|||||||
onChange={this._handleChangeFile}
|
onChange={this._handleChangeFile}
|
||||||
/>
|
/>
|
||||||
<div class="lemon-editor__tool">
|
<div class="lemon-editor__tool">
|
||||||
{emojiData.length > 0 && (
|
<div class="lemon-editor__tool-left">{toolLeft}</div>
|
||||||
<lemon-popover class="lemon-editor__emoji">
|
<div class="lemon-editor__tool-right">{toolRight}</div>
|
||||||
<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>
|
</div>
|
||||||
<div class="lemon-editor__inner">
|
<div class="lemon-editor__inner">
|
||||||
<div
|
<div
|
||||||
@@ -65,18 +134,22 @@ export default {
|
|||||||
on-keydown={this._handleKeydown}
|
on-keydown={this._handleKeydown}
|
||||||
on-paste={this._handlePaste}
|
on-paste={this._handlePaste}
|
||||||
on-click={this._handleClick}
|
on-click={this._handleClick}
|
||||||
on-input={this._handleInput}
|
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="lemon-editor__footer">
|
<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">
|
<div class="lemon-editor__submit">
|
||||||
<lemon-button
|
<lemon-button
|
||||||
disabled={this.submitDisabled}
|
disabled={this.submitDisabled}
|
||||||
on-click={this._handleSend}
|
on-click={this._handleSend}
|
||||||
>
|
>
|
||||||
发 送
|
{this.sendText}
|
||||||
</lemon-button>
|
</lemon-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,6 +157,68 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
_saveLastRange() {
|
||||||
lastSelectionRange = selection.getRangeAt(0);
|
lastSelectionRange = selection.getRangeAt(0);
|
||||||
},
|
},
|
||||||
@@ -97,9 +232,6 @@ export default {
|
|||||||
_handleClick() {
|
_handleClick() {
|
||||||
this._saveLastRange();
|
this._saveLastRange();
|
||||||
},
|
},
|
||||||
_handleInput() {
|
|
||||||
this._checkSubmitDisabled();
|
|
||||||
},
|
|
||||||
_renderEmojiTabs() {
|
_renderEmojiTabs() {
|
||||||
const renderImageGrid = items => {
|
const renderImageGrid = items => {
|
||||||
return items.map(item => (
|
return items.map(item => (
|
||||||
@@ -131,45 +263,65 @@ export default {
|
|||||||
_handleSelectEmoji(item) {
|
_handleSelectEmoji(item) {
|
||||||
this._focusLastRange();
|
this._focusLastRange();
|
||||||
exec(`<img emoji-name="${item.name}" src="${item.src}"></img>`);
|
exec(`<img emoji-name="${item.name}" src="${item.src}"></img>`);
|
||||||
|
this._checkSubmitDisabled();
|
||||||
this._saveLastRange();
|
this._saveLastRange();
|
||||||
},
|
},
|
||||||
async _handleSelectFile(accept) {
|
async selectFile(accept) {
|
||||||
this.accept = accept;
|
this.accept = accept;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.$refs.fileInput.click();
|
this.$refs.fileInput.click();
|
||||||
},
|
},
|
||||||
_handlePaste(e) {
|
_handlePaste(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { clipboardData } = e;
|
const clipboardData = e.clipboardData || window.clipboardData;
|
||||||
const text = clipboardData.getData("text");
|
const text = clipboardData.getData("Text");
|
||||||
exec(text, "insertText");
|
if (text) {
|
||||||
// Array.from(clipboardData.items).forEach(item => {
|
if (window.clipboardData) {
|
||||||
// console.log(item.type);
|
this.$refs.textarea.innerHTML = text;
|
||||||
// });
|
} else {
|
||||||
//e.target.innerText = text;
|
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) {
|
_handleKeyup(e) {
|
||||||
this._saveLastRange();
|
this._saveLastRange();
|
||||||
//this._checkSubmitDisabled();
|
this._checkSubmitDisabled();
|
||||||
},
|
},
|
||||||
_handleKeydown(e) {
|
_handleKeydown(e) {
|
||||||
const { keyCode } = e;
|
if (this.submitDisabled == false && this.sendKey(e)) {
|
||||||
if (keyCode == 13) {
|
this._handleSend();
|
||||||
// e.preventDefault();
|
|
||||||
// document.execCommand("defaultParagraphSeparator", false, false);
|
|
||||||
// exec("<br>");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getFormatValue() {
|
getFormatValue() {
|
||||||
return toEmojiName(
|
// return toEmojiName(
|
||||||
this.$refs.textarea.innerHTML
|
// this.$refs.textarea.innerHTML
|
||||||
.replace(/<br>|<\/br>/, "")
|
// .replace(/<br>|<\/br>/, "")
|
||||||
.replace(/<div>|<p>/g, "\r\n")
|
// .replace(/<div>|<p>/g, "\r\n")
|
||||||
.replace(/<\/div>|<\/p>/g, "")
|
// .replace(/<\/div>|<\/p>/g, "")
|
||||||
);
|
// );
|
||||||
|
return this.IMUI.emojiImageToName(this.$refs.textarea.innerHTML);
|
||||||
},
|
},
|
||||||
_checkSubmitDisabled() {
|
_checkSubmitDisabled() {
|
||||||
this.submitDisabled = !this.$refs.textarea.innerHTML.trim();
|
this.submitDisabled = !clearHtmlExcludeImg(
|
||||||
|
this.$refs.textarea.innerHTML.trim()
|
||||||
|
);
|
||||||
},
|
},
|
||||||
_handleSend(e) {
|
_handleSend(e) {
|
||||||
const text = this.getFormatValue();
|
const text = this.getFormatValue();
|
||||||
@@ -190,6 +342,10 @@ export default {
|
|||||||
initEmoji(data) {
|
initEmoji(data) {
|
||||||
emojiData = data;
|
emojiData = data;
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
|
},
|
||||||
|
setValue(val) {
|
||||||
|
this.$refs.textarea.innerHTML = this.IMUI.emojiNameToImage(val);
|
||||||
|
this._checkSubmitDisabled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -199,28 +355,66 @@ export default {
|
|||||||
gap = 10px;
|
gap = 10px;
|
||||||
+b(lemon-editor)
|
+b(lemon-editor)
|
||||||
height 200px
|
height 200px
|
||||||
|
position relative
|
||||||
flex-column()
|
flex-column()
|
||||||
+e(tool)
|
+e(tool)
|
||||||
display flex
|
display flex
|
||||||
height 40px
|
height 40px
|
||||||
align-items center
|
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)
|
+e(tool-item)
|
||||||
cursor pointer
|
cursor pointer
|
||||||
padding 4px gap
|
padding 4px gap
|
||||||
height 28px
|
height 28px
|
||||||
|
line-height 24px;
|
||||||
color #999
|
color #999
|
||||||
transition all ease .3s
|
transition all ease .3s
|
||||||
|
font-size 12px
|
||||||
[class^='lemon-icon-']
|
[class^='lemon-icon-']
|
||||||
line-height 26px
|
line-height 26px
|
||||||
font-size 22px
|
font-size 22px
|
||||||
&:hover
|
&:hover
|
||||||
color #333
|
color #333
|
||||||
|
+m(right){
|
||||||
|
margin-left:auto;
|
||||||
|
}
|
||||||
+e(inner)
|
+e(inner)
|
||||||
flex 1
|
flex 1
|
||||||
overflow-x hidden
|
overflow-x hidden
|
||||||
overflow-y auto
|
overflow-y auto
|
||||||
scrollbar-light()
|
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)
|
+e(input)
|
||||||
height 100%
|
height 100%
|
||||||
box-sizing border-box
|
box-sizing border-box
|
||||||
@@ -234,6 +428,8 @@ gap = 10px;
|
|||||||
height 20px
|
height 20px
|
||||||
padding 0 2px
|
padding 0 2px
|
||||||
pointer-events none
|
pointer-events none
|
||||||
|
position relative
|
||||||
|
top -1px
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
+e(footer)
|
+e(footer)
|
||||||
display flex
|
display flex
|
||||||
|
|||||||
+374
-134
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { useScopedSlot, fastDone, generateUUID } from "utils";
|
import { useScopedSlot, funCall, generateUUID, cloneDeep } from "utils";
|
||||||
import { isFunction, isString, isEmpty } from "utils/validate";
|
import { isFunction, isString, isEmpty } from "utils/validate";
|
||||||
|
import contextmenu from "../directives/contextmenu";
|
||||||
import {
|
import {
|
||||||
DEFAULT_MENUS,
|
DEFAULT_MENUS,
|
||||||
DEFAULT_MENU_LASTMESSAGES,
|
DEFAULT_MENU_LASTMESSAGES,
|
||||||
@@ -10,11 +11,7 @@ import lastContentRender from "../lastContentRender";
|
|||||||
|
|
||||||
import MemoryCache from "utils/cache/memory";
|
import MemoryCache from "utils/cache/memory";
|
||||||
|
|
||||||
const CacheContactContainer = new MemoryCache();
|
const allMessages = {};
|
||||||
const CacheMenuContainer = new MemoryCache();
|
|
||||||
const CacheMessageLoaded = new MemoryCache();
|
|
||||||
|
|
||||||
const messages = {};
|
|
||||||
const emojiMap = {};
|
const emojiMap = {};
|
||||||
let renderDrawerContent = () => {};
|
let renderDrawerContent = () => {};
|
||||||
|
|
||||||
@@ -26,6 +23,22 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: {
|
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
|
default: true
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 初始化时是否隐藏导航按钮上的头像
|
* 是否隐藏导航按钮上的头像
|
||||||
*/
|
*/
|
||||||
hideMenuAvatar: Boolean,
|
hideMenuAvatar: Boolean,
|
||||||
hideMenu: Boolean,
|
hideMenu: Boolean,
|
||||||
|
/**
|
||||||
|
* 是否隐藏消息列表内的联系人名字
|
||||||
|
*/
|
||||||
|
hideMessageName: Boolean,
|
||||||
|
/**
|
||||||
|
* 是否隐藏消息列表内的发送时间
|
||||||
|
*/
|
||||||
|
hideMessageTime: Boolean,
|
||||||
|
sendKey: Function,
|
||||||
|
sendText: String,
|
||||||
|
contextmenu: Array,
|
||||||
|
contactContextmenu: Array,
|
||||||
|
avatarCricle: Boolean,
|
||||||
user: {
|
user: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {
|
default: () => {
|
||||||
@@ -54,12 +80,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
this.CacheContactContainer = new MemoryCache();
|
||||||
|
this.CacheMenuContainer = new MemoryCache();
|
||||||
|
this.CacheMessageLoaded = new MemoryCache();
|
||||||
|
this.CacheDraft = new MemoryCache();
|
||||||
return {
|
return {
|
||||||
drawerVisible: !this.hideDrawer,
|
drawerVisible: !this.hideDrawer,
|
||||||
currentContactId: "",
|
currentContactId: null,
|
||||||
|
currentMessages: [],
|
||||||
activeSidebar: DEFAULT_MENU_LASTMESSAGES,
|
activeSidebar: DEFAULT_MENU_LASTMESSAGES,
|
||||||
contacts: [],
|
contacts: [],
|
||||||
menus: []
|
menus: [],
|
||||||
|
editorTools: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -79,9 +111,6 @@ export default {
|
|||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentMessages() {
|
|
||||||
return messages[this.currentContactId] || [];
|
|
||||||
},
|
|
||||||
currentContact() {
|
currentContact() {
|
||||||
return this.contacts.find(item => item.id == this.currentContactId) || {};
|
return this.contacts.find(item => item.id == this.currentContactId) || {};
|
||||||
},
|
},
|
||||||
@@ -124,9 +153,34 @@ export default {
|
|||||||
...message
|
...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) {
|
_emitSend(message, next, file) {
|
||||||
this.$emit(
|
this.$emit(
|
||||||
@@ -134,20 +188,21 @@ export default {
|
|||||||
message,
|
message,
|
||||||
(replaceMessage = { status: "succeed" }) => {
|
(replaceMessage = { status: "succeed" }) => {
|
||||||
next();
|
next();
|
||||||
message = Object.assign(message, replaceMessage);
|
this.updateMessage(Object.assign(message, replaceMessage));
|
||||||
this.forceUpdateMessage(message.id);
|
|
||||||
},
|
},
|
||||||
file
|
file
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_handleSend(text) {
|
_handleSend(text) {
|
||||||
const message = this._createMessage({ content: text });
|
const message = this._createMessage({ content: text });
|
||||||
this.appendMessage(message);
|
this.appendMessage(message, true);
|
||||||
this._emitSend(message, () => {
|
this._emitSend(message, () => {
|
||||||
this.updateContact(message.toContactId, {
|
this.updateContact({
|
||||||
|
id: message.toContactId,
|
||||||
lastContent: this.lastContentRender(message),
|
lastContent: this.lastContentRender(message),
|
||||||
lastSendTime: message.sendTime
|
lastSendTime: message.sendTime
|
||||||
});
|
});
|
||||||
|
this.CacheDraft.remove(message.toContactId);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
_handleUpload(file) {
|
_handleUpload(file) {
|
||||||
@@ -167,11 +222,12 @@ export default {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const message = this._createMessage(joinMessage);
|
const message = this._createMessage(joinMessage);
|
||||||
this.appendMessage(message);
|
this.appendMessage(message, true);
|
||||||
this._emitSend(
|
this._emitSend(
|
||||||
message,
|
message,
|
||||||
() => {
|
() => {
|
||||||
this.updateContact(message.toContactId, {
|
this.updateContact({
|
||||||
|
id: message.toContactId,
|
||||||
lastContent: this.lastContentRender(message),
|
lastContent: this.lastContentRender(message),
|
||||||
lastSendTime: message.sendTime
|
lastSendTime: message.sendTime
|
||||||
});
|
});
|
||||||
@@ -180,26 +236,36 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
_emitPullMessages(next) {
|
_emitPullMessages(next) {
|
||||||
|
this._changeContactLock = true;
|
||||||
this.$emit(
|
this.$emit(
|
||||||
"pull-messages",
|
"pull-messages",
|
||||||
this.currentContact,
|
this.currentContact,
|
||||||
(messages, isEnd = false) => {
|
(messages, isEnd = false) => {
|
||||||
this._addMessage(messages, this.currentContactId, 0);
|
this._addMessage(messages, this.currentContactId, 0);
|
||||||
CacheMessageLoaded.set(this.currentContactId, isEnd);
|
this.CacheMessageLoaded.set(this.currentContactId, isEnd);
|
||||||
if (isEnd == true) this.$refs.messages.loaded();
|
if (isEnd == true) this.$refs.messages.loaded();
|
||||||
|
this.updateCurrentMessages();
|
||||||
|
this._changeContactLock = false;
|
||||||
next(isEnd);
|
next(isEnd);
|
||||||
}
|
},
|
||||||
|
this
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
clearCacheContainer(name) {
|
clearCacheContainer(name) {
|
||||||
CacheContactContainer.remove(name);
|
this.CacheContactContainer.remove(name);
|
||||||
CacheMenuContainer.remove(name);
|
this.CacheMenuContainer.remove(name);
|
||||||
},
|
},
|
||||||
_renderWrapper(children) {
|
_renderWrapper(children) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
style={{
|
||||||
|
width: this.width,
|
||||||
|
height: this.height
|
||||||
|
}}
|
||||||
class={[
|
class={[
|
||||||
"lemon-wrapper",
|
"lemon-wrapper",
|
||||||
|
`lemon-wrapper--theme-${this.theme}`,
|
||||||
|
{ "lemon-wrapper--simple": this.simple },
|
||||||
this.drawerVisible && "lemon-wrapper--drawer-show"
|
this.drawerVisible && "lemon-wrapper--drawer-show"
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -245,7 +311,7 @@ export default {
|
|||||||
{ "lemon-menu__item--active": this.activeSidebar == name }
|
{ "lemon-menu__item--active": this.activeSidebar == name }
|
||||||
]}
|
]}
|
||||||
on-click={() => {
|
on-click={() => {
|
||||||
fastDone(click, () => {
|
funCall(click, () => {
|
||||||
if (name) this.changeMenu(name);
|
if (name) this.changeMenu(name);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -264,51 +330,56 @@ export default {
|
|||||||
_renderSidebarMessage() {
|
_renderSidebarMessage() {
|
||||||
return this._renderSidebar(
|
return this._renderSidebar(
|
||||||
[
|
[
|
||||||
useScopedSlot(this.$scopedSlots["message-sidebar"]),
|
useScopedSlot(this.$scopedSlots["sidebar-message-top"], null, this),
|
||||||
this.lastMessages.map(contact => {
|
this.lastMessages.map(contact => {
|
||||||
return this._renderContact(
|
return this._renderContact(
|
||||||
{
|
{
|
||||||
contact,
|
contact,
|
||||||
timeFormat: this.contactTimeFormat
|
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 {
|
const {
|
||||||
click: customClick,
|
click: customClick,
|
||||||
renderContainer,
|
renderContainer,
|
||||||
id: contactId
|
id: contactId
|
||||||
} = props.contact;
|
} = props.contact;
|
||||||
const click = () => {
|
const click = () => {
|
||||||
fastDone(customClick, () => {
|
funCall(customClick, () => {
|
||||||
onClick();
|
onClick();
|
||||||
this._customContainerReady(
|
this._customContainerReady(
|
||||||
renderContainer,
|
renderContainer,
|
||||||
CacheContactContainer,
|
this.CacheContactContainer,
|
||||||
contactId
|
contactId
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<lemon-contact
|
<lemon-contact
|
||||||
class={{
|
class={{
|
||||||
"lemon-contact--active": this.currentContactId == props.contact.id
|
"lemon-contact--active": this.currentContactId == props.contact.id
|
||||||
}}
|
}}
|
||||||
|
v-lemon-contextmenu_contact={this.contactContextmenu}
|
||||||
props={props}
|
props={props}
|
||||||
on-click={click}
|
on-click={click}
|
||||||
/>
|
scopedSlots={{ default: slot }}
|
||||||
|
></lemon-contact>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_renderSidebarContact() {
|
_renderSidebarContact() {
|
||||||
let prevIndex;
|
let prevIndex;
|
||||||
return this._renderSidebar(
|
return this._renderSidebar(
|
||||||
[
|
[
|
||||||
useScopedSlot(this.$scopedSlots["contact-sidebar"]),
|
useScopedSlot(this.$scopedSlots["sidebar-contact-top"], null, this),
|
||||||
this.contacts.map(contact => {
|
this.contacts.map(contact => {
|
||||||
if (!contact.index) return;
|
if (!contact.index) return;
|
||||||
contact.index = contact.index.replace(/\[[0-9]*\]/, "");
|
contact.index = contact.index.replace(/\[[0-9]*\]/, "");
|
||||||
@@ -321,20 +392,29 @@ export default {
|
|||||||
contact: contact,
|
contact: contact,
|
||||||
simple: true
|
simple: true
|
||||||
},
|
},
|
||||||
() => this.changeContact(contact.id)
|
() => {
|
||||||
|
this.changeContact(contact.id);
|
||||||
|
},
|
||||||
|
this.$scopedSlots["sidebar-contact"]
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
prevIndex = contact.index;
|
prevIndex = contact.index;
|
||||||
return node;
|
return node;
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
DEFAULT_MENU_CONTACTS
|
DEFAULT_MENU_CONTACTS,
|
||||||
|
useScopedSlot(this.$scopedSlots["sidebar-contact-fixedtop"], null, this)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_renderSidebar(children, name) {
|
_renderSidebar(children, name, fixedtop) {
|
||||||
return (
|
return (
|
||||||
<div class="lemon-sidebar" v-show={this.activeSidebar == name}>
|
<div
|
||||||
{children}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -356,22 +436,22 @@ export default {
|
|||||||
const cls = "lemon-container";
|
const cls = "lemon-container";
|
||||||
const curact = this.currentContact;
|
const curact = this.currentContact;
|
||||||
let defIsShow = true;
|
let defIsShow = true;
|
||||||
for (const name in CacheContactContainer.get()) {
|
for (const name in this.CacheContactContainer.get()) {
|
||||||
const show = curact.id == name && this.currentIsDefSidebar;
|
const show = curact.id == name && this.currentIsDefSidebar;
|
||||||
defIsShow = !show;
|
defIsShow = !show;
|
||||||
nodes.push(
|
nodes.push(
|
||||||
<div class={cls} v-show={show}>
|
<div class={cls} v-show={show}>
|
||||||
{CacheContactContainer.get(name)}
|
{this.CacheContactContainer.get(name)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const name in CacheMenuContainer.get()) {
|
for (const name in this.CacheMenuContainer.get()) {
|
||||||
nodes.push(
|
nodes.push(
|
||||||
<div
|
<div
|
||||||
class={cls}
|
class={cls}
|
||||||
v-show={this.activeSidebar == name && !this.currentIsDefSidebar}
|
v-show={this.activeSidebar == name && !this.currentIsDefSidebar}
|
||||||
>
|
>
|
||||||
{CacheMenuContainer.get(name)}
|
{this.CacheMenuContainer.get(name)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -384,7 +464,7 @@ export default {
|
|||||||
<div class="lemon-container__title">
|
<div class="lemon-container__title">
|
||||||
<div class="lemon-container__displayname">
|
<div class="lemon-container__displayname">
|
||||||
{useScopedSlot(
|
{useScopedSlot(
|
||||||
this.$scopedSlots["contact-title"],
|
this.$scopedSlots["message-title"],
|
||||||
curact.displayName,
|
curact.displayName,
|
||||||
curact
|
curact
|
||||||
)}
|
)}
|
||||||
@@ -392,6 +472,8 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
<lemon-messages
|
<lemon-messages
|
||||||
ref="messages"
|
ref="messages"
|
||||||
|
hide-time={this.hideMessageTime}
|
||||||
|
hide-name={this.hideMessageName}
|
||||||
time-format={this.messageTimeFormat}
|
time-format={this.messageTimeFormat}
|
||||||
reverse-user-id={this.user.id}
|
reverse-user-id={this.user.id}
|
||||||
on-reach-top={this._emitPullMessages}
|
on-reach-top={this._emitPullMessages}
|
||||||
@@ -399,6 +481,9 @@ export default {
|
|||||||
/>
|
/>
|
||||||
<lemon-editor
|
<lemon-editor
|
||||||
ref="editor"
|
ref="editor"
|
||||||
|
tools={this.editorTools}
|
||||||
|
sendText={this.sendText}
|
||||||
|
sendKey={this.sendKey}
|
||||||
onSend={this._handleSend}
|
onSend={this._handleSend}
|
||||||
onUpload={this._handleUpload}
|
onUpload={this._handleUpload}
|
||||||
/>
|
/>
|
||||||
@@ -421,6 +506,12 @@ export default {
|
|||||||
<h4>{curact.displayName}</h4>
|
<h4>{curact.displayName}</h4>
|
||||||
<lemon-button
|
<lemon-button
|
||||||
on-click={() => {
|
on-click={() => {
|
||||||
|
if (isEmpty(curact.lastContent)) {
|
||||||
|
this.updateContact({
|
||||||
|
id: curact.id,
|
||||||
|
lastContent: " "
|
||||||
|
});
|
||||||
|
}
|
||||||
this.changeContact(curact.id, DEFAULT_MENU_LASTMESSAGES);
|
this.changeContact(curact.id, DEFAULT_MENU_LASTMESSAGES);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -433,12 +524,14 @@ export default {
|
|||||||
);
|
);
|
||||||
return nodes;
|
return nodes;
|
||||||
},
|
},
|
||||||
|
_handleSidebarScroll() {
|
||||||
|
contextmenu.hide();
|
||||||
|
},
|
||||||
_addContact(data, t) {
|
_addContact(data, t) {
|
||||||
const type = {
|
const type = {
|
||||||
0: "unshift",
|
0: "unshift",
|
||||||
1: "push"
|
1: "push"
|
||||||
}[t];
|
}[t];
|
||||||
//this.contacts[type](cloneDeep(data));
|
|
||||||
this.contacts[type](data);
|
this.contacts[type](data);
|
||||||
},
|
},
|
||||||
_addMessage(data, contactId, t) {
|
_addMessage(data, contactId, t) {
|
||||||
@@ -447,10 +540,8 @@ export default {
|
|||||||
1: "push"
|
1: "push"
|
||||||
}[t];
|
}[t];
|
||||||
if (!Array.isArray(data)) data = [data];
|
if (!Array.isArray(data)) data = [data];
|
||||||
messages[contactId] = messages[contactId] || [];
|
allMessages[contactId] = allMessages[contactId] || [];
|
||||||
messages[contactId][type](...data);
|
allMessages[contactId][type](...data);
|
||||||
//console.log(messages[contactId]);
|
|
||||||
this.forceUpdateMessage();
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 设置最新消息DOM
|
* 设置最新消息DOM
|
||||||
@@ -461,6 +552,12 @@ export default {
|
|||||||
lastContentRender[messageType] = render;
|
lastContentRender[messageType] = render;
|
||||||
},
|
},
|
||||||
lastContentRender(message) {
|
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);
|
return lastContentRender[message.type].call(this, message);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -468,14 +565,22 @@ export default {
|
|||||||
* @param {String} str 被替换的字符串
|
* @param {String} str 被替换的字符串
|
||||||
* @return {String} 替换后的字符串
|
* @return {String} 替换后的字符串
|
||||||
*/
|
*/
|
||||||
replaceEmojiName(str) {
|
emojiNameToImage(str) {
|
||||||
return str.replace(/\[!(\w+)\]/gi, (str, match) => {
|
return str.replace(/\[!(\w+)\]/gi, (str, match) => {
|
||||||
const file = match;
|
const file = match;
|
||||||
return emojiMap[file]
|
return emojiMap[file]
|
||||||
? `<img src="${emojiMap[file]}" />`
|
? `<img emoji-name="${match}" src="${emojiMap[file]}" />`
|
||||||
: `[!${match}]`;
|
: `[!${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
|
* @param contactId 联系人 id
|
||||||
*/
|
*/
|
||||||
changeContact(contactId, menuName) {
|
async changeContact(contactId, menuName) {
|
||||||
if (this.currentContactId == contactId) {
|
|
||||||
this.currentContactId = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (menuName) {
|
if (menuName) {
|
||||||
this.changeMenu(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.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)) {
|
if (isFunction(this.currentContact.renderContainer)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this._menuIsMessages()) {
|
//填充草稿内容
|
||||||
if (!CacheMessageLoaded.has(contactId)) {
|
const draft = this.CacheDraft.get(contactId) || "";
|
||||||
this.$refs.messages.resetLoadState();
|
if (draft) this.setEditorValue(draft);
|
||||||
}
|
|
||||||
if (!messages[contactId]) {
|
if (this.CacheMessageLoaded.has(contactId)) {
|
||||||
this._emitPullMessages(isEnd => this.messageViewToBottom());
|
this.$refs.messages.loaded();
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
this.$refs.messages.resetLoadState();
|
||||||
this.messageViewToBottom();
|
}
|
||||||
}, 0);
|
|
||||||
}
|
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 messageId 消息 id
|
||||||
* @param contactId 联系人 id
|
* @param contactId 联系人 id
|
||||||
*/
|
*/
|
||||||
removeMessage(messageId, contactId) {
|
removeMessage(messageId) {
|
||||||
const index = this.findMessageIndexById(messageId, contactId);
|
const message = this.findMessage(messageId);
|
||||||
if (index !== -1) {
|
if (!message) return false;
|
||||||
messages[contactId].splice(index, 1);
|
const index = allMessages[message.toContactId].findIndex(
|
||||||
this.forceUpdateMessage();
|
({ id }) => id == messageId
|
||||||
}
|
);
|
||||||
|
allMessages[message.toContactId].splice(index, 1);
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 修改聊天一条聊天消息
|
* 修改聊天一条聊天消息
|
||||||
* @param {Message} data 根据 data.id 查找聊天消息并覆盖传入的值
|
* @param {Message} data 根据 data.id 查找聊天消息并覆盖传入的值
|
||||||
* @param contactId 联系人 id
|
* @param contactId 联系人 id
|
||||||
*/
|
*/
|
||||||
updateMessage(messageId, contactId, data) {
|
updateMessage(message) {
|
||||||
const index = this.findMessageIndexById(messageId, contactId);
|
if (!message.id) return false;
|
||||||
if (index !== -1) {
|
let historyMessage = this.findMessage(message.id);
|
||||||
messages[contactId][index] = Object.assign(
|
if (!historyMessage) return false;
|
||||||
messages[contactId][index],
|
historyMessage = Object.assign(historyMessage, message, {
|
||||||
data
|
toContactId: historyMessage.toContactId
|
||||||
);
|
});
|
||||||
console.log("--------", messages[contactId][index]);
|
return true;
|
||||||
this.forceUpdateMessage(messageId);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 手动更新对话消息
|
* 手动更新对话消息
|
||||||
@@ -567,6 +696,7 @@ export default {
|
|||||||
* @param {String} name 按钮 name
|
* @param {String} name 按钮 name
|
||||||
*/
|
*/
|
||||||
changeMenu(name) {
|
changeMenu(name) {
|
||||||
|
if (this._changeContactLock) return false;
|
||||||
this.$emit("change-menu", name);
|
this.$emit("change-menu", name);
|
||||||
this.activeSidebar = name;
|
this.activeSidebar = name;
|
||||||
},
|
},
|
||||||
@@ -577,11 +707,20 @@ export default {
|
|||||||
* EmojiItem = {name: wx,title: 微笑,src: url} 无分组
|
* EmojiItem = {name: wx,title: 微笑,src: url} 无分组
|
||||||
*/
|
*/
|
||||||
initEmoji(data) {
|
initEmoji(data) {
|
||||||
|
let flatData = [];
|
||||||
this.$refs.editor.initEmoji(data);
|
this.$refs.editor.initEmoji(data);
|
||||||
if (data[0].label) {
|
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 = [];
|
let menus = [];
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
const indexMap = {
|
const indexMap = {
|
||||||
lastMessages: 0,
|
messages: 0,
|
||||||
contacts: 1
|
contacts: 1
|
||||||
};
|
};
|
||||||
const indexKeys = Object.keys(indexMap);
|
const indexKeys = Object.keys(indexMap);
|
||||||
@@ -629,7 +768,7 @@ export default {
|
|||||||
if (item.renderContainer) {
|
if (item.renderContainer) {
|
||||||
this._customContainerReady(
|
this._customContainerReady(
|
||||||
item.renderContainer,
|
item.renderContainer,
|
||||||
CacheMenuContainer,
|
this.CacheMenuContainer,
|
||||||
item.name
|
item.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -647,7 +786,7 @@ export default {
|
|||||||
* @param {Array<Contact>} data 联系人列表
|
* @param {Array<Contact>} data 联系人列表
|
||||||
*/
|
*/
|
||||||
initContacts(data) {
|
initContacts(data) {
|
||||||
this.contacts.push(...data);
|
this.contacts = data;
|
||||||
this.sortContacts();
|
this.sortContacts();
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -659,13 +798,43 @@ export default {
|
|||||||
return a.index.localeCompare(b.index);
|
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.id;
|
||||||
delete data.toContactId;
|
|
||||||
|
|
||||||
const index = this.findContactIndexById(contactId);
|
const index = this.findContactIndexById(contactId);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
@@ -690,16 +859,22 @@ export default {
|
|||||||
findContactIndexById(contactId) {
|
findContactIndexById(contactId) {
|
||||||
return this.contacts.findIndex(item => item.id == contactId);
|
return this.contacts.findIndex(item => item.id == contactId);
|
||||||
},
|
},
|
||||||
findMessageIndexById(messageId, contactId) {
|
/**
|
||||||
const msg = messages[contactId];
|
* 根据 id 查找判断是否存在联系人
|
||||||
if (isEmpty(msg)) {
|
* @param contactId 联系人 id
|
||||||
return -1;
|
* @return {Boolean}
|
||||||
}
|
*/
|
||||||
return msg.findIndex(item => item.id == messageId);
|
hasContact(contactId) {
|
||||||
|
return this.findContactIndexById(contactId) !== -1;
|
||||||
},
|
},
|
||||||
findMessageById(messageId, contactId) {
|
findMessage(messageId) {
|
||||||
const index = this.findMessageIndexById(messageId, contactId);
|
for (const key in allMessages) {
|
||||||
if (index !== -1) return messages[contactId][index];
|
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() {
|
getContacts() {
|
||||||
return this.contacts;
|
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>}
|
* @return {Object<Contact.id,Message>}
|
||||||
*/
|
*/
|
||||||
getMessages(contactId) {
|
getMessages(contactId) {
|
||||||
return (contactId ? messages[contactId] : messages) || [];
|
return (contactId ? allMessages[contactId] : allMessages) || [];
|
||||||
},
|
|
||||||
// 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;
|
|
||||||
},
|
},
|
||||||
changeDrawer(render) {
|
changeDrawer(render) {
|
||||||
this.drawerVisible = !this.drawerVisible;
|
this.drawerVisible = !this.drawerVisible;
|
||||||
@@ -754,16 +919,14 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="stylus">
|
<style lang="stylus">
|
||||||
wrapper-width = 850px
|
|
||||||
drawer-width = 200px
|
drawer-width = 200px
|
||||||
bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
|
bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
|
||||||
@import '~styles/utils/index'
|
@import '~styles/utils/index'
|
||||||
|
|
||||||
+b(lemon-wrapper)
|
+b(lemon-wrapper)
|
||||||
width wrapper-width
|
|
||||||
height 580px
|
|
||||||
display flex
|
display flex
|
||||||
font-size 14px
|
font-size 14px
|
||||||
|
font-family "Microsoft YaHei"
|
||||||
//mask-image radial-gradient(circle, white 100%, black 100%)
|
//mask-image radial-gradient(circle, white 100%, black 100%)
|
||||||
background #efefef
|
background #efefef
|
||||||
transition all .4s bezier
|
transition all .4s bezier
|
||||||
@@ -812,13 +975,18 @@ bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
|
|||||||
+b(lemon-sidebar)
|
+b(lemon-sidebar)
|
||||||
width 250px
|
width 250px
|
||||||
background #efefef
|
background #efefef
|
||||||
overflow-y auto
|
display flex
|
||||||
scrollbar-light()
|
flex-direction column
|
||||||
|
+e(scroll){
|
||||||
|
overflow-y auto
|
||||||
|
scrollbar-light()
|
||||||
|
}
|
||||||
+e(label)
|
+e(label)
|
||||||
padding 6px 14px 6px 14px
|
padding 6px 14px 6px 14px
|
||||||
color #666
|
color #666
|
||||||
font-size 12px
|
font-size 12px
|
||||||
margin 0
|
margin 0
|
||||||
|
text-align left
|
||||||
+b(lemon-contact--active)
|
+b(lemon-contact--active)
|
||||||
background #d9d9d9
|
background #d9d9d9
|
||||||
+b(lemon-container)
|
+b(lemon-container)
|
||||||
@@ -827,7 +995,7 @@ bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
|
|||||||
background #f4f4f4
|
background #f4f4f4
|
||||||
word-break()
|
word-break()
|
||||||
position relative
|
position relative
|
||||||
z-index 2
|
z-index 10
|
||||||
+e(title)
|
+e(title)
|
||||||
padding 15px 15px
|
padding 15px 15px
|
||||||
+e(displayname)
|
+e(displayname)
|
||||||
@@ -842,11 +1010,10 @@ bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
|
|||||||
overflow hidden
|
overflow hidden
|
||||||
background #f4f4f4
|
background #f4f4f4
|
||||||
transition width .4s bezier
|
transition width .4s bezier
|
||||||
z-index 1
|
z-index 9
|
||||||
width drawer-width
|
width drawer-width
|
||||||
height 100%
|
height 100%
|
||||||
box-sizing border-box
|
box-sizing border-box
|
||||||
//border-left 1px solid #e9e9e9
|
|
||||||
+b(lemon-wrapper)
|
+b(lemon-wrapper)
|
||||||
+m(drawer-show)
|
+m(drawer-show)
|
||||||
+b(lemon-drawer)
|
+b(lemon-drawer)
|
||||||
@@ -861,4 +1028,77 @@ bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
|
|||||||
font-weight normal
|
font-weight normal
|
||||||
margin 10px 0 20px 0
|
margin 10px 0 20px 0
|
||||||
user-select none
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { useScopedSlot } from "utils";
|
||||||
export default {
|
export default {
|
||||||
name: "lemonMessageBasic",
|
name: "lemonMessageBasic",
|
||||||
inject: ["IMUI"],
|
inject: {
|
||||||
|
IMUI: {
|
||||||
|
from: "IMUI",
|
||||||
|
default() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
|
contextmenu: Array,
|
||||||
message: {
|
message: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {
|
default: () => {
|
||||||
@@ -14,7 +23,8 @@ export default {
|
|||||||
default: () => ""
|
default: () => ""
|
||||||
},
|
},
|
||||||
reverse: Boolean,
|
reverse: Boolean,
|
||||||
hiddenTitle: Boolean
|
hideName: Boolean,
|
||||||
|
hideTime: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {};
|
return {};
|
||||||
@@ -25,9 +35,10 @@ export default {
|
|||||||
<div
|
<div
|
||||||
class={[
|
class={[
|
||||||
"lemon-message",
|
"lemon-message",
|
||||||
|
`lemon-message--status-${status}`,
|
||||||
{
|
{
|
||||||
"lemon-message--reverse": this.reverse,
|
"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}
|
{fromUser.displayName}
|
||||||
</span>
|
</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>
|
||||||
<div
|
<div class="lemon-message__content-flex">
|
||||||
class="lemon-message__content"
|
<div
|
||||||
on-click={e => {
|
v-lemon-contextmenu_message={this.IMUI.contextmenu}
|
||||||
this._emitClick(e, "content");
|
class="lemon-message__content"
|
||||||
}}
|
on-click={e => {
|
||||||
>
|
this._emitClick(e, "content");
|
||||||
{this.useScopedSlots("content", this.message)}
|
}}
|
||||||
</div>
|
>
|
||||||
<div
|
{useScopedSlot(this.$scopedSlots["content"], null, this.message)}
|
||||||
class="lemon-message__status"
|
</div>
|
||||||
on-click={e => {
|
<div class="lemon-message__content-after">
|
||||||
this._emitClick(e, "status");
|
{useScopedSlot(
|
||||||
}}
|
this.IMUI.$scopedSlots["message-after"],
|
||||||
>
|
null,
|
||||||
{this._renderStatue(status)}
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,29 +116,7 @@ export default {
|
|||||||
watch: {},
|
watch: {},
|
||||||
methods: {
|
methods: {
|
||||||
_emitClick(e, key) {
|
_emitClick(e, key) {
|
||||||
this.IMUI.$emit("message-click", e, key, this.message);
|
this.IMUI.$emit("message-click", e, key, this.message, this.IMUI);
|
||||||
},
|
|
||||||
_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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -118,8 +134,8 @@ arrow()
|
|||||||
display flex
|
display flex
|
||||||
padding 10px 0
|
padding 10px 0
|
||||||
+e(time)
|
+e(time)
|
||||||
color #bbb
|
color #b9b9b9
|
||||||
padding 0 4px
|
padding 0 5px
|
||||||
+e(inner)
|
+e(inner)
|
||||||
position relative
|
position relative
|
||||||
+e(avatar)
|
+e(avatar)
|
||||||
@@ -133,7 +149,9 @@ arrow()
|
|||||||
line-height 14px
|
line-height 14px
|
||||||
padding-bottom 6px
|
padding-bottom 6px
|
||||||
user-select none
|
user-select none
|
||||||
color #999
|
color #666
|
||||||
|
+e(content-flex)
|
||||||
|
display flex
|
||||||
+e(content)
|
+e(content)
|
||||||
font-size 14px
|
font-size 14px
|
||||||
line-height 20px
|
line-height 20px
|
||||||
@@ -141,7 +159,7 @@ arrow()
|
|||||||
background #fff
|
background #fff
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
position relative
|
position relative
|
||||||
margin 0 46px 0 0
|
margin 0
|
||||||
img
|
img
|
||||||
video
|
video
|
||||||
background #e9e9e9
|
background #e9e9e9
|
||||||
@@ -151,22 +169,49 @@ arrow()
|
|||||||
left -4px
|
left -4px
|
||||||
border-left none
|
border-left none
|
||||||
border-right-color #fff
|
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)
|
+e(status)
|
||||||
position absolute
|
position absolute
|
||||||
top 23px
|
top 23px
|
||||||
right 20px
|
right 20px
|
||||||
color #aaa
|
color #aaa
|
||||||
font-size 20px
|
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)
|
+m(reverse)
|
||||||
flex-direction row-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)
|
+e(title)
|
||||||
flex-direction row-reverse
|
flex-direction row-reverse
|
||||||
+e(status)
|
+e(status)
|
||||||
left 20px
|
left 26px
|
||||||
right auto
|
right auto
|
||||||
+e(content)
|
+e(content)
|
||||||
background #35d863
|
background #35d863
|
||||||
margin 0 0 0 46px
|
|
||||||
&:before
|
&:before
|
||||||
arrow()
|
arrow()
|
||||||
left auto
|
left auto
|
||||||
@@ -178,9 +223,9 @@ arrow()
|
|||||||
+e(avatar)
|
+e(avatar)
|
||||||
padding-right 0
|
padding-right 0
|
||||||
padding-left 10px
|
padding-left 10px
|
||||||
+m(hidden-title)
|
+m(hide-name)
|
||||||
+e(status)
|
+e(status)
|
||||||
top 7px
|
top 3px
|
||||||
+e(title)
|
+e(title)
|
||||||
display none
|
display none
|
||||||
+e(content)
|
+e(content)
|
||||||
|
|||||||
@@ -2,13 +2,24 @@
|
|||||||
export default {
|
export default {
|
||||||
name: "lemonMessageEvent",
|
name: "lemonMessageEvent",
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
|
inject: ["IMUI"],
|
||||||
render() {
|
render() {
|
||||||
const { content } = this.$attrs.message;
|
const { content } = this.$attrs.message;
|
||||||
return (
|
return (
|
||||||
<div class="lemon-message lemon-message-event">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
_emitClick(e, key) {
|
||||||
|
this.IMUI.$emit("message-click", e, key, this.$attrs.message, this.IMUI);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default {
|
|||||||
props={{ ...this.$attrs }}
|
props={{ ...this.$attrs }}
|
||||||
scopedSlots={{
|
scopedSlots={{
|
||||||
content: props => {
|
content: props => {
|
||||||
const content = this.IMUI.replaceEmojiName(props.content);
|
const content = this.IMUI.emojiNameToImage(props.content);
|
||||||
return <span domProps={{ innerHTML: content }} />;
|
return <span domProps={{ innerHTML: content }} />;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -29,6 +29,8 @@ export default {
|
|||||||
height 18px
|
height 18px
|
||||||
display inline-block
|
display inline-block
|
||||||
background transparent
|
background transparent
|
||||||
|
position relative
|
||||||
|
top -1px
|
||||||
padding 0 2px
|
padding 0 2px
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import { hoursTimeFormat } from "utils";
|
import { hoursTimeFormat } from "utils";
|
||||||
|
import contextmenu from "../directives/contextmenu";
|
||||||
export default {
|
export default {
|
||||||
name: "LemonMessages",
|
name: "LemonMessages",
|
||||||
components: {},
|
components: {},
|
||||||
props: {
|
props: {
|
||||||
|
//是否隐藏消息发送人昵称
|
||||||
|
hideName: Boolean,
|
||||||
|
//是否隐藏显示消息时间
|
||||||
|
hideTime: Boolean,
|
||||||
reverseUserId: String,
|
reverseUserId: String,
|
||||||
timeRange: {
|
timeRange: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -52,24 +57,26 @@ export default {
|
|||||||
message: {
|
message: {
|
||||||
id: "__time__",
|
id: "__time__",
|
||||||
type: "event",
|
type: "event",
|
||||||
content: this.timeFormat(message.sendTime)
|
content: hoursTimeFormat(message.sendTime)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
node.push(
|
|
||||||
<tagName
|
let attrs;
|
||||||
ref="message"
|
if (message.type == "event") {
|
||||||
refInFor={true}
|
attrs = { message: message };
|
||||||
attrs={{
|
} else {
|
||||||
timeFormat: this.msecRange > 0 ? () => {} : this.timeFormat,
|
attrs = {
|
||||||
message: message,
|
timeFormat: this.timeFormat,
|
||||||
reverse: this.reverseUserId == message.fromUser.id,
|
message: message,
|
||||||
hiddenTitle: false
|
reverse: this.reverseUserId == message.fromUser.id,
|
||||||
}}
|
hideTime: this.hideTime,
|
||||||
/>
|
hideName: this.hideName
|
||||||
);
|
};
|
||||||
|
}
|
||||||
|
node.push(<tagName ref="message" refInFor={true} attrs={attrs} />);
|
||||||
return node;
|
return node;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -90,6 +97,7 @@ export default {
|
|||||||
},
|
},
|
||||||
loaded() {
|
loaded() {
|
||||||
this._loadend = true;
|
this._loadend = true;
|
||||||
|
this.$forceUpdate();
|
||||||
},
|
},
|
||||||
resetLoadState() {
|
resetLoadState() {
|
||||||
this._loading = false;
|
this._loading = false;
|
||||||
@@ -97,6 +105,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async _handleScroll(e) {
|
async _handleScroll(e) {
|
||||||
const { target } = e;
|
const { target } = e;
|
||||||
|
contextmenu.hide();
|
||||||
if (
|
if (
|
||||||
target.scrollTop == 0 &&
|
target.scrollTop == 0 &&
|
||||||
this._loading == false &&
|
this._loading == false &&
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
const popoverCloseQueue = [];
|
const popoverCloseQueue = [];
|
||||||
const popoverCloseAll = () => popoverCloseQueue.forEach(callback => callback());
|
import contextmenu from "../directives/contextmenu";
|
||||||
const triggerEvents = {
|
const triggerEvents = {
|
||||||
hover(el) {},
|
hover(el) {},
|
||||||
focus(el) {
|
focus(el) {
|
||||||
@@ -14,6 +14,7 @@ const triggerEvents = {
|
|||||||
click(el) {
|
click(el) {
|
||||||
el.addEventListener("click", e => {
|
el.addEventListener("click", e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
contextmenu.hide();
|
||||||
this.changeVisible();
|
this.changeVisible();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -51,7 +52,7 @@ export default {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<span style="position:relative">
|
<span style="position:relative">
|
||||||
<transition name="slide-top">
|
<transition name="lemon-slide-top">
|
||||||
{this.visible && (
|
{this.visible && (
|
||||||
<div
|
<div
|
||||||
class="lemon-popover"
|
class="lemon-popover"
|
||||||
@@ -59,7 +60,6 @@ export default {
|
|||||||
style={this.popoverStyle}
|
style={this.popoverStyle}
|
||||||
on-click={e => e.stopPropagation()}
|
on-click={e => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div class="lemon-popover__title" />
|
|
||||||
<div class="lemon-popover__content">{this.$slots.content}</div>
|
<div class="lemon-popover__content">{this.$slots.content}</div>
|
||||||
<div class="lemon-popover__arrow" />
|
<div class="lemon-popover__arrow" />
|
||||||
</div>
|
</div>
|
||||||
@@ -96,9 +96,12 @@ export default {
|
|||||||
this.visible ? this.close() : this.open();
|
this.visible ? this.close() : this.open();
|
||||||
},
|
},
|
||||||
open() {
|
open() {
|
||||||
popoverCloseAll();
|
this.closeAll();
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
},
|
},
|
||||||
|
closeAll() {
|
||||||
|
popoverCloseQueue.forEach(callback => callback());
|
||||||
|
},
|
||||||
close() {
|
close() {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
}
|
}
|
||||||
@@ -117,7 +120,7 @@ export default {
|
|||||||
z-index 10
|
z-index 10
|
||||||
background-color #fff
|
background-color #fff
|
||||||
border-radius 4px
|
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
|
position absolute
|
||||||
transform-origin 50% 150%
|
transform-origin 50% 150%
|
||||||
+e(content)
|
+e(content)
|
||||||
@@ -135,9 +138,9 @@ export default {
|
|||||||
width 8px
|
width 8px
|
||||||
height 8px
|
height 8px
|
||||||
background #fff
|
background #fff
|
||||||
.slide-top-leave-active ,.slide-top-enter-active
|
.lemon-slide-top-leave-active ,.lemon-slide-top-enter-active
|
||||||
transition all .3s cubic-bezier(0.645, 0.045, 0.355, 1)
|
transition all .2s cubic-bezier(0.645, 0.045, 0.355, 1)
|
||||||
.slide-top-enter, .slide-top-leave-to
|
.lemon-slide-top-enter, .lemon-slide-top-leave-to
|
||||||
transform translateY(-10px) scale(.8)
|
transform translateY(-10px) scale(.8)
|
||||||
opacity 0
|
opacity 0
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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
@@ -1,3 +1,4 @@
|
|||||||
|
import Contextmenu from "./directives/contextmenu";
|
||||||
import LemonTabs from "./components/tabs";
|
import LemonTabs from "./components/tabs";
|
||||||
import LemonPopover from "./components/popover";
|
import LemonPopover from "./components/popover";
|
||||||
import LemonButton from "./components/button";
|
import LemonButton from "./components/button";
|
||||||
@@ -14,7 +15,7 @@ import lemonMessageEvent from "./components/message/event";
|
|||||||
|
|
||||||
import LemonIMUI from "./components/index";
|
import LemonIMUI from "./components/index";
|
||||||
import "./styles/common/index.styl";
|
import "./styles/common/index.styl";
|
||||||
const version = "0.1";
|
const version = "1.4.2";
|
||||||
const components = [
|
const components = [
|
||||||
LemonIMUI,
|
LemonIMUI,
|
||||||
LemonContact,
|
LemonContact,
|
||||||
@@ -32,6 +33,7 @@ const components = [
|
|||||||
lemonMessageEvent
|
lemonMessageEvent
|
||||||
];
|
];
|
||||||
const install = (Vue, opts = {}) => {
|
const install = (Vue, opts = {}) => {
|
||||||
|
Vue.directive("LemonContextmenu", Contextmenu);
|
||||||
components.forEach(component => {
|
components.forEach(component => {
|
||||||
Vue.component(component.name, component);
|
Vue.component(component.name, component);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
|
import {clearHtml } from 'utils';
|
||||||
export default {
|
export default {
|
||||||
voice(message) {
|
|
||||||
return "[语音]";
|
|
||||||
},
|
|
||||||
file(message) {
|
file(message) {
|
||||||
return "[文件]";
|
return "[文件]";
|
||||||
},
|
},
|
||||||
video(message) {
|
|
||||||
return "[视频]";
|
|
||||||
},
|
|
||||||
image(message) {
|
image(message) {
|
||||||
return "[图片]";
|
return "[图片]";
|
||||||
},
|
},
|
||||||
text(message) {
|
text(message) {
|
||||||
return this.replaceEmojiName(message.content);
|
return this.emojiNameToImage(clearHtml(message.content));
|
||||||
}
|
},
|
||||||
|
event(message){
|
||||||
|
return '[通知]';
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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-face {
|
||||||
font-family: 'lemon-icons';
|
font-family: 'lemon-icons';
|
||||||
src: url('//at.alicdn.com/t/font_1312162_neqltsj20an.eot');
|
src:url('../fonts/icon.woff') format('woff');
|
||||||
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');
|
|
||||||
|
|
||||||
}
|
}
|
||||||
[class^='lemon-icon-'],
|
[class^='lemon-icon-'],
|
||||||
[class*=' lemon-icon-']
|
[class*=' lemon-icon-']
|
||||||
|
|||||||
Binary file not shown.
Vendored
-15
@@ -22,18 +22,3 @@ export default class MemoryCache {
|
|||||||
return !!this.table[key];
|
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,6 +1,6 @@
|
|||||||
export const EMIT_AVATAR_CLICK = "avatar-click";
|
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_MENU_CONTACTS = "contacts";
|
||||||
export const DEFAULT_MENUS = [DEFAULT_MENU_LASTMESSAGES, DEFAULT_MENU_CONTACTS];
|
export const DEFAULT_MENUS = [DEFAULT_MENU_LASTMESSAGES, DEFAULT_MENU_CONTACTS];
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function timeFormat(t, format) {
|
|||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fastDone(event, callback) {
|
export function funCall(event, callback) {
|
||||||
if (isFunction(event)) {
|
if (isFunction(event)) {
|
||||||
event(() => {
|
event(() => {
|
||||||
callback();
|
callback();
|
||||||
@@ -70,7 +70,14 @@ export function fastDone(event, callback) {
|
|||||||
export function arrayIntersect(a, b) {
|
export function arrayIntersect(a, b) {
|
||||||
return a.filter(x => b.includes(x));
|
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) {
|
export function error(text) {
|
||||||
throw new Error(text);
|
throw new Error(text);
|
||||||
}
|
}
|
||||||
@@ -96,9 +103,6 @@ export function mergeDeep(o1, o2) {
|
|||||||
return o1;
|
return o1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toEmojiName(str) {
|
|
||||||
return str.replace(/<img emoji-name=\"([^\"]*?)\" [^>]*>/gi, "[!$1]");
|
|
||||||
}
|
|
||||||
export function formatByte(value) {
|
export function formatByte(value) {
|
||||||
if (null == value || value == "") {
|
if (null == value || value == "") {
|
||||||
return "0 Bytes";
|
return "0 Bytes";
|
||||||
|
|||||||
Reference in New Issue
Block a user