Files
lemon-imui/packages/components/index.vue
T
fan 3603d0c03b 增加右键菜单配置
增加setEditorValue、getEditorValue方法
修改updateContact传参
修复只选择表情发送按钮是灰色的问题
修复send之后toContactId丢失的问题
2021-01-30 21:03:15 +08:00

1054 lines
28 KiB
Vue
Raw Blame History

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