This commit is contained in:
fan
2019-10-24 19:48:49 +08:00
parent f4bda4688c
commit 387ba9640c
55 changed files with 4784 additions and 2756 deletions
+72
View File
@@ -0,0 +1,72 @@
<script>
export default {
name: "LemonAvatar",
props: {
src: String,
icon: {
type: String,
default: "lemon-icon-people"
},
size: {
type: Number,
default: 32
}
},
data() {
return {
imageFinishLoad: true
};
},
render() {
return (
<span
style={this.style}
class="lemon-avatar"
on-click={e => this.$emit("click", e)}
>
{this.imageFinishLoad && <i class={this.icon} />}
<img src={this.src} onLoad={this._handleLoad} />
</span>
);
},
computed: {
style() {
const size = `${this.size}px`;
return {
width: size,
height: size,
lineHeight: size,
fontSize: `${this.size / 2}px`
};
}
},
methods: {
_handleLoad() {
this.imageFinishLoad = false;
}
}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
+b(lemon-avatar)
font-variant tabular-nums
line-height 1.5
box-sizing border-box
margin 0
padding 0
list-style none
display inline-block
text-align center
background #ccc
color rgba(255,255,255,0.7)
white-space nowrap
position relative
overflow hidden
vertical-align middle
border-radius 4px
img
width 100%
height 100%
display block
</style>
+74
View File
@@ -0,0 +1,74 @@
<script>
export default {
name: "LemonBadge",
props: {
count: [Number, Boolean],
overflowCount: {
type: Number,
default: 99
}
},
render() {
return (
<span class="lemon-badge">
{this.$slots.default}
{this.count !== 0 && this.count !== undefined && (
<span
class={[
"lemon-badge__label",
this.isDot && "lemon-badge__label--dot"
]}
>
{this.label}
</span>
)}
</span>
);
},
computed: {
isDot() {
return this.count === true;
},
label() {
if (this.isDot) return "";
return this.count > this.overflowCount
? `${this.overflowCount}+`
: this.count;
}
},
methods: {}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
+b(lemon-badge)
position relative
display inline-block
+e(label)
border-radius 10px
background #f5222d
color #fff
text-align center
font-size 12px
font-weight normal
white-space nowrap
box-shadow 0 0 0 1px #fff
z-index 10
position absolute
transform translateX(50%)
transform-origin 100%
display inline-block
padding 0 4px
height 18px
line-height 17px
min-width 10px
top -4px
right 6px
+m(dot)
width 10px
height 10px
min-width auto
padding 0
top -3px
right 2px
</style>
+59
View File
@@ -0,0 +1,59 @@
<script>
export default {
name: "LemonButton",
props: {
disabled: Boolean
},
render() {
return (
<button
class="lemon-button"
disabled={this.disabled}
type="button"
on-click={this._handleClick}
>
{this.$slots.default}
</button>
);
},
methods: {
_handleClick(e) {
this.$emit("click", e);
}
}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
+b(lemon-button)
outline none
line-height 1.499
display inline-block
font-weight 400
text-align center
touch-action manipulation
cursor pointer
background-image none
border 1px solid #ddd
box-sizing border-box
white-space nowrap
padding 0 15px
font-size 14px
border-radius 4px
height 32px
user-select none
transition all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1)
color rgba(0, 0, 0, 0.65)
background-color #fff
box-shadow 0 2px 0 rgba(0, 0, 0, 0.015)
text-shadow 0 -1px 0 rgba(0, 0, 0, 0.12)
&:hover:not([disabled])
border-color #666
color #333
&:active
background-color #ddd
&[disabled]
cursor not-allowed
color #aaa
background #eee
</style>
@@ -1,43 +0,0 @@
<template>
<div class='lemon-contact-list'>
<div class="lemon-contact-item"
v-for="item in control.friends"
:key="item.id"
@click="control._changeMessageView(item)">
{{item.diaplayName}}
</div>
</div>
</template>
<script>
export default {
name: 'ContactList',
inject: ["control"],
data () {
return {
};
},
computed: {},
watch: {},
methods: {
},
created () {
},
mounted () {
},
}
</script>
<style lang='scss'>
@import '~styles/utils/index';
@include b(contact-item) {
padding: 10px 15px;
cursor: pointer;
&:hover {
background: #000;
}
}
</style>
+134
View File
@@ -0,0 +1,134 @@
<script>
import { isString, isToday } from "utils/validate";
import { timeFormat } from "utils";
export default {
name: "LemonContact",
components: {},
data() {
return {};
},
props: {
contact: Object,
simple: Boolean,
timeFormat: {
type: Function,
default(val) {
return timeFormat(val, isToday(val) ? "h:i" : "y/m/d");
}
}
},
render() {
const { contact } = this;
return (
<div
class={["lemon-contact", { "lemon-contact--name-center": this.simple }]}
on-click={e => this._handleClick(e, contact)}
>
<lemon-badge
count={!this.simple ? contact.unread : 0}
class="lemon-contact__avatar"
native-on-click={e => this._handleBubbleClick(e, contact)}
>
<lemon-avatar
size={40}
native-on-click={e => this._handleAvatarClick(e, contact)}
src={contact.avatar}
/>
</lemon-badge>
<div class="lemon-contact__inner">
<p class="lemon-contact__label">
<span class="lemon-contact__name">{contact.displayName}</span>
{!this.simple && (
<span class="lemon-contact__time">
{this.timeFormat(contact.lastSendTime)}
</span>
)}
</p>
{!this.simple && (
<p class="lemon-contact__content">
{isString(contact.lastContent) ? (
<span domProps={{ innerHTML: contact.lastContent }} />
) : (
contact.lastContent
)}
</p>
)}
</div>
</div>
);
},
created() {},
mounted() {},
computed: {},
watch: {},
methods: {
_handleClick(e, data) {
this.$emit("click", data);
},
_handleAvatarClick(e, data) {
e.stopPropagation();
this.$emit("avatar-click", data);
},
_handleBubbleClick(e, data) {
e.stopPropagation();
this.$emit("bubble-click", data);
}
}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
+b(lemon-contact)
padding 10px 14px
cursor pointer
user-select none
box-sizing border-box
overflow hidden
background #efefef
p
margin 0
+m(active)
background #bebdbd
&:hover:not(.lemon-contact--active)
background #e3e3e3
.el-badge__content
border-color #ddd
+e(avatar)
float left
margin-right 10px
img
display block
.ant-badge-count
display inline-block
padding 0 4px
height 18px
line-height 18px
min-width 18px
top -4px
right 7px
+e(label)
display flex
+e(time)
font-size 12px
line-height 18px
padding-left 6px
color #999
white-space nowrap
+e(name)
display block
width 100%
ellipsis()
+e(content)
font-size 12px
color #999
ellipsis()
img
height 14px
display inline-block
vertical-align middle
margin 0 1px
+m(name-center)
+e(label)
padding-bottom 0
line-height 38px
</style>
+295
View File
@@ -0,0 +1,295 @@
<script>
import { toEmojiName } from "utils";
const exec = (val, command = "insertHTML") => {
document.execCommand(command, false, val);
};
const selection = window.getSelection();
let lastSelectionRange;
let emojiData = [];
export default {
name: "LemonEditor",
components: {},
props: {},
data() {
return {
submitDisabled: true,
accept: ""
};
},
created() {},
mounted() {
//this.$refs.fileInput.addEventListener("change", this._handleChangeFile);
},
computed: {},
watch: {},
render() {
//<a-popover trigger="click" overlay-class-name="lemon-editor__emoji">
return (
<div class="lemon-editor">
<input
style="display:none"
type="file"
multiple="multiple"
ref="fileInput"
accept={this.accept}
onChange={this._handleChangeFile}
/>
<div class="lemon-editor__tool">
{emojiData.length > 0 && (
<lemon-popover class="lemon-editor__emoji">
<template slot="content">{this._renderEmojiTabs()}</template>
<div class="lemon-editor__tool-item">
<i class="lemon-icon-emoji" />
</div>
</lemon-popover>
)}
<div
class="lemon-editor__tool-item"
on-click={() => this._handleSelectFile("*")}
>
<i class="lemon-icon-folder" />
</div>
<div
class="lemon-editor__tool-item"
on-click={() => this._handleSelectFile("image/*")}
>
<i class="lemon-icon-image" />
</div>
</div>
<div class="lemon-editor__inner">
<div
class="lemon-editor__input"
ref="textarea"
contenteditable="true"
on-keyup={this._handleKeyup}
on-keydown={this._handleKeydown}
on-paste={this._handlePaste}
on-click={this._handleClick}
on-input={this._handleInput}
spellcheck="false"
/>
</div>
<div class="lemon-editor__footer">
<div class="lemon-editor__tip">使用 ctrl + enter 快捷发送消息</div>
<div class="lemon-editor__submit">
<lemon-button
disabled={this.submitDisabled}
on-click={this._handleSend}
>
</lemon-button>
</div>
</div>
</div>
);
},
methods: {
_saveLastRange() {
lastSelectionRange = selection.getRangeAt(0);
},
_focusLastRange() {
this.$refs.textarea.focus();
if (lastSelectionRange) {
selection.removeAllRanges();
selection.addRange(lastSelectionRange);
}
},
_handleClick() {
this._saveLastRange();
},
_handleInput() {
this._checkSubmitDisabled();
},
_renderEmojiTabs() {
const renderImageGrid = items => {
return items.map(item => (
<img
src={item.src}
title={item.title}
class="lemon-editor__emoji-item"
on-click={() => this._handleSelectEmoji(item)}
/>
));
};
if (emojiData[0].label) {
const nodes = emojiData.map((item, index) => {
return (
<div slot="tab-pane" index={index} tab={item.label}>
{renderImageGrid(item.children)}
{renderImageGrid(item.children)}
</div>
);
});
return <lemon-tabs style="width: 412px">{nodes}</lemon-tabs>;
} else {
return (
<div class="lemon-tabs-content" style="width:406px">
{renderImageGrid(emojiData)}
</div>
);
}
},
_handleSelectEmoji(item) {
this._focusLastRange();
exec(`<img emoji-name="${item.name}" src="${item.src}"></img>`);
this._saveLastRange();
},
async _handleSelectFile(accept) {
this.accept = accept;
await this.$nextTick();
this.$refs.fileInput.click();
},
_handlePaste(e) {
e.preventDefault();
const { clipboardData } = e;
const text = clipboardData.getData("text");
exec(text, "insertText");
// Array.from(clipboardData.items).forEach(item => {
// console.log(item.type);
// });
//e.target.innerText = text;
},
_handleKeyup(e) {
this._saveLastRange();
//this._checkSubmitDisabled();
},
_handleKeydown(e) {
const { keyCode } = e;
if (keyCode == 13) {
// e.preventDefault();
// document.execCommand("defaultParagraphSeparator", false, false);
// exec("<br>");
}
},
getFormatValue() {
return toEmojiName(
this.$refs.textarea.innerHTML
.replace(/<br>|<\/br>/, "")
.replace(/<div>|<p>/g, "\r\n")
.replace(/<\/div>|<\/p>/g, "")
);
},
_checkSubmitDisabled() {
this.submitDisabled = !this.$refs.textarea.innerHTML.trim();
},
_handleSend(e) {
const text = this.getFormatValue();
this.$emit("send", text);
this.clear();
this._checkSubmitDisabled();
},
_handleChangeFile(e) {
const { fileInput } = this.$refs;
Array.from(fileInput.files).forEach(file => {
this.$emit("upload", file);
});
fileInput.value = "";
},
clear() {
this.$refs.textarea.innerHTML = "";
},
initEmoji(data) {
emojiData = data;
this.$forceUpdate();
// this.emoji = [
// {
// label: "表情",
// name: "face",
// data: [
// {
// name: "wx",
// src: "微笑"
// }
// ]
// },
// {
// label: "武器",
// name: "wa",
// data: [
// {
// name: "wx",
// src: "微笑"
// }
// ]
// }
// ];
}
}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
gap = 10px;
+b(lemon-editor)
height 200px
flex-column()
+e(tool)
display flex
height 40px
align-items center
padding-left 5px
+e(tool-item)
cursor pointer
padding 4px gap
height 28px
color #999
transition all ease .3s
[class^='lemon-icon-']
line-height 26px
font-size 22px
&:hover
color #333
+e(inner)
flex 1
overflow-x hidden
overflow-y auto
scrollbar-light()
+e(input)
height 100%
box-sizing border-box
border none
outline none
padding 0 gap
scrollbar-light()
p,div
margin 0
img
height 20px
padding 0 2px
pointer-events none
vertical-align middle
+e(footer)
display flex
height 52px
justify-content flex-end
padding 0 gap
align-items center
+e(tip)
margin-right 10px
font-size 12px
color #999
user-select none
+e(emoji)
user-select none
.lemon-popover
background #f6f6f6
.lemon-popover__content
padding 0
.lemon-popover__arrow
background #f6f6f6
.lemon-tabs-content
box-sizing border-box
padding 8px
height 200px
overflow-x hidden
overflow-y auto
scrollbar-light()
margin-bottom 8px
+e(emoji-item)
cursor pointer
width 22px
padding 4px
border-radius 4px
&:hover
background #e9e9e9
</style>
-30
View File
@@ -1,30 +0,0 @@
<template>
<div class=''>
LemonGroupList
</div>
</template>
<script>
export default {
name: 'GroupList',
components: {},
data () {
return {
};
},
computed: {},
watch: {},
methods: {
},
created () {
},
mounted () {
},
}
</script>
<style lang='scss'>
</style>
+890
View File
@@ -0,0 +1,890 @@
<script>
import { useScopedSlot, fastDone, generateUUID } from "utils";
import { isFunction, isString, isEmpty } from "utils/validate";
import {
DEFAULT_MENUS,
DEFAULT_MENU_LASTMESSAGES,
DEFAULT_MENU_CONTACTS
} from "utils/constant";
import lastContentRender from "../lastContentRender";
import MemoryCache from "utils/cache/memory";
const CacheContactContainer = new MemoryCache();
const CacheMenuContainer = new MemoryCache();
const CacheMessageLoaded = new MemoryCache();
import {
//constraintContactMessages,
constraintContact
//constraintMessage
} from "utils/constraint";
const messages = {};
const emojiMap = {};
let renderDrawerContent = () => {};
export default {
name: "LemonImui",
provide() {
return {
IMUI: this
};
},
props: {
/**
* 消息时间格式化规则
*/
messageTimeFormat: Function,
/**
* 联系人最新消息时间格式化规则
*/
contactTimeFormat: Function,
/**
* 初始化时是否隐藏抽屉
*/
hideDrawer: {
type: Boolean,
default: true
},
/**
* 初始化时是否隐藏导航按钮上的头像
*/
hideMenuAvatar: Boolean,
user: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {
drawerVisible: !this.hideDrawer,
currentContactId: "",
activeSidebar: DEFAULT_MENU_LASTMESSAGES,
contacts: [],
menus: []
};
},
render() {
return this._renderWrapper([
this._renderMenu(),
this._renderSidebarMessage(),
this._renderSidebarContact(),
this._renderContainer(),
this._renderDrawer()
]);
},
created() {
this.initMenus();
},
async mounted() {
await this.$nextTick();
},
computed: {
currentMessages() {
return messages[this.currentContactId] || [];
},
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
};
// const message = {
// id: "123",
// status: "succeed",
// type: "image",
// sendTime: 12312312312,
// content: "asdas",
// fromContactId: "123",
// fromUser: { id: "123", displayName: "123", avatar: "123",}
// }
},
// _setDefMessages(id) {
// //this.messages[id] = this.messages[id] || [];
// if (!messages[id]) {
// this.$set(messages, id, []);
// }
// },
appendMessage(message, contactId = this.currentContactId) {
this._addMessage(message, contactId, 1);
this.messageViewToBottom();
},
_emitSend(message, next, file) {
this.$emit(
"send",
message,
(replaceMessage = { status: "succeed" }) => {
next();
message = Object.assign(message, replaceMessage);
this.forceUpdateMessage(message.id);
},
file
);
},
_handleSend(text) {
const message = this._createMessage({ content: text });
this.appendMessage(message);
this._emitSend(message, () => {
this.updateContact(message.toContactId, {
lastContent: lastContentRender[message.type].call(this, 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);
this._emitSend(
message,
() => {
this.updateContact(message.toContactId, {
lastContent: lastContentRender[message.type].call(this, message),
lastSendTime: message.sendTime
});
},
file
);
},
_handleReachTop(next) {
// const messages = {
// id: "8ad7e98e-5225-4892-8131-4b2ee7797599",
// type: "text",
// status: "succeed",
// sendTime: 1564926674646,
// fromContactId: "superadmin",
// fromUser: {
// id: "hehe",
// displayName: "I KNOEW",
// avatar:
// "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4085009425,1005454674&fm=26&gp=0.jpg"
// },
// content: "测试消息哦..."
// };
this.$emit(
"pull-messages",
this.currentContact,
(messages, isEnd = false) => {
this._addMessage(
Array(10).fill(messages[1]),
this.currentContactId,
0
);
CacheMessageLoaded.set(this.currentContactId, isEnd);
next(isEnd);
}
);
// setTimeout(() => {
// CacheMessageLoaded.set(this.currentContactId, isEnd);
// }, 2000);
},
clearCacheContainer(name) {
CacheContactContainer.remove(name);
CacheMenuContainer.remove(name);
},
_renderWrapper(children) {
return (
<div
class={[
"lemon-wrapper",
this.drawerVisible && "lemon-wrapper--drawer-show"
]}
>
{children}
</div>
);
},
_renderMenu() {
const menuItem = this._renderMenuItem();
return (
<div class="lemon-menu">
{this.hideMenuAvatar == false && (
<lemon-avatar
on-click={e => {
console.log("menu avatar click");
}}
class="lemon-menu__avatar"
src="https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=400062461,2874561526&fm=26&gp=0.jpg"
/>
)}
{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={() => {
fastDone(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(
this.lastMessages.map(contact => {
return this._renderContact(
{
contact,
timeFormat: this.contactTimeFormat
},
() => this.changeContact(contact.id)
);
}),
DEFAULT_MENU_LASTMESSAGES
);
},
_renderContact(props, onClick) {
const {
click: customClick,
renderContainer,
id: contactId
} = props.contact;
const click = () => {
fastDone(customClick, () => {
onClick();
this._customContainerReady(
renderContainer,
CacheContactContainer,
contactId
);
});
};
return (
<lemon-contact
class={{
"lemon-contact--active": this.currentContactId == props.contact.id
}}
props={props}
on-click={click}
/>
);
},
_renderSidebarContact() {
let prevIndex;
return this._renderSidebar(
this.contacts.map(contact => {
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)
)
];
prevIndex = contact.index;
return node;
}),
DEFAULT_MENU_CONTACTS
);
},
_renderSidebar(children, name) {
return (
<div class="lemon-sidebar" v-show={this.activeSidebar == name}>
{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 CacheContactContainer.get()) {
const show = curact.id == name && this.currentIsDefSidebar;
defIsShow = !show;
nodes.push(
<div class={cls} v-show={show}>
{CacheContactContainer.get(name)}
</div>
);
}
for (const name in CacheMenuContainer.get()) {
nodes.push(
<div
class={cls}
v-show={this.activeSidebar == name && !this.currentIsDefSidebar}
>
{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["contact-title"],
curact.displayName,
curact
)}
</div>
</div>
<lemon-messages
ref="messages"
time-format={this.messageTimeFormat}
reverse-user-id={this.user.id}
on-reach-top={this._handleReachTop}
messages={this.currentMessages}
/>
<lemon-editor
ref="editor"
onSend={this._handleSend}
onUpload={this._handleUpload}
/>
</div>
);
nodes.push(
<div class={cls} v-show={!curact.id}>
{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={() => {
this.changeContact(curact.id, DEFAULT_MENU_LASTMESSAGES);
}}
>
{" "}
发送消息{" "}
</lemon-button>
</div>,
curact
)}
</div>
);
return nodes;
},
_addContact(data, t) {
const type = {
0: "unshift",
1: "push"
}[t];
constraintContact(data);
//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];
messages[contactId] = messages[contactId] || [];
messages[contactId][type](...data);
//console.log(messages[contactId]);
this.forceUpdateMessage();
},
/**
* 设置最新消息DOM
* @param {String} messageType 消息类型
* @param {Function} render 返回消息 vnode
*/
setLastContentRender(messageType, render) {
lastContentRender[messageType] = render;
},
/**
* 将字符串内的 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}]`;
});
},
/**
* 将当前聊天窗口滚动到底部
*/
messageViewToBottom() {
this.$refs.messages.scrollToBottom();
},
/**
* 改变聊天对象
* @param contactId 联系人 id
*/
changeContact(contactId, menuName) {
if (this.currentContactId == contactId) {
this.currentContactId = undefined;
}
if (menuName) {
this.changeMenu(menuName);
}
this.currentContactId = contactId;
this.$emit("change-contact", this.currentContact);
if (isFunction(this.currentContact.renderContainer)) {
return;
}
if (this._menuIsMessages()) {
if (!CacheMessageLoaded.has(contactId)) {
this.$refs.messages.resetLoadState();
}
if (!messages[contactId]) {
this.$emit(
"pull-messages",
this.currentContact,
(messages, isEnd) => {
this._addMessage(messages, contactId, 0);
this.messageViewToBottom();
}
);
} else {
this.messageViewToBottom();
}
}
},
/**
* 删除一条聊天消息
* @param messageId 消息 id
* @param contactId 联系人 id
*/
removeMessage(messageId, contactId) {
const index = this.findMessageIndexById(messageId, contactId);
if (index !== -1) {
messages[contactId].splice(index, 1);
this.forceUpdateMessage();
}
},
/**
* 修改聊天一条聊天消息
* @param {Message} data 根据 data.id 查找聊天消息并覆盖传入的值
* @param contactId 联系人 id
*/
updateMessage(messageId, contactId, data) {
const index = this.findMessageIndexById(messageId, contactId);
if (index !== -1) {
messages[contactId][index] = {
...messages[contactId][index],
...data
};
this.forceUpdateMessage(messageId);
}
},
/**
* 手动更新对话消息
* @param {String} messageId 消息ID,如果为空则更新当前聊天窗口的所有消息
*/
forceUpdateMessage(messageId) {
if (!messageId) {
this.$refs.messages.$forceUpdate();
} else {
const components = this.$refs.messages.$refs.message;
if (components) {
const messageComponent = components.find(
com => com.$attrs.message.id == messageId
);
if (messageComponent) messageComponent.$forceUpdate();
}
}
},
_customContainerReady(render, cacheDrive, key) {
if (isFunction(render) && !cacheDrive.has(key)) {
cacheDrive.set(key, render.call(this));
}
},
/**
* 切换左侧按钮
* @param {String} name 按钮 name
*/
changeMenu(name) {
this.$emit("change-menu", name);
this.activeSidebar = name;
const { renderContainer } = this.currentMenu;
this._customContainerReady(renderContainer, CacheMenuContainer, 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) {
this.$refs.editor.initEmoji(data);
if (data[0].label) {
data = data.flatMap(item => item.children);
}
data.forEach(({ name, src }) => (emojiMap[name] = src));
},
/**
* 初始化左侧按钮
* @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 = {
lastMessages: 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 }
};
}
return item;
});
} else {
menus = defaultMenus;
}
this.menus = menus;
},
/**
* 初始化联系人数据
* @param {Array<Contact>} data 联系人列表
*/
initContacts(data) {
this.contacts.push(...data);
this.sortContacts();
},
/**
* 使用 联系人的 index 值进行排序
*/
sortContacts() {
this.contacts.sort((a, b) => {
return a.index.localeCompare(b.index);
});
},
/**
* 修改联系人数据
* @param {Contact} data 修改的数据,根据 data.id 查找联系人并覆盖传入的值
*/
updateContact(contactId, data) {
delete data.id;
delete data.toContactId;
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);
},
findMessageIndexById(messageId, contactId) {
const msg = messages[contactId];
if (isEmpty(msg)) {
return -1;
}
return msg.findIndex(item => item.id == messageId);
},
findMessageById(messageId, contactId) {
const index = this.findMessageIndexById(messageId, contactId);
if (index !== -1) return messages[contactId][index];
},
/**
* 返回所有联系人
* @return {Array<Contact>}
*/
getContacts() {
return this.contacts;
},
/**
* 返回所有消息
* @return {Object<Contact.id,Message>}
*/
getMessages() {
return messages;
},
// appendContact(data) {
// this._addContact(data, 0);
// },
// prependContact(data) {
// this._addContact(data, 1);
// },
// addContactMessage(data) {
// this._addContact(data, 0);
// },
// prependContactMessage(data) {
// this._addContact(data, 1);
// },
// appendMessage(data) {},
// prependMessage(data) {},
// removeContact(contactId) {},
// removeContactMessage(contactId) {},
// removeContactAll(contactId) {},
/**
* 将自定义的HTML显示在主窗口内
*/
openrenderContainer(vnode) {
//renderContainerQueue[this.activeSidebar] = vnode;
//this.$slots._renderContainer = vnode;
},
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">
wrapper-width = 850px
drawer-width = 200px
bezier = cubic-bezier(0.645, 0.045, 0.355, 1)
@import '~styles/utils/index'
+b(lemon-wrapper)
width wrapper-width
height 580px
display flex
font-size 14px
border-radius 5px
//mask-image radial-gradient(circle, white 100%, black 100%)
background #efefef
transition all .4s bezier
border-radius 4px
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
+b(lemon-contact--active)
background #d9d9d9
+b(lemon-container)
flex 1
flex-column()
background #f4f4f4
word-break()
position relative
z-index 2
+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
width drawer-width
background #f4f4f4
transition width .4s bezier
height 100%
z-index 1
//border-left 1px solid #e9e9e9
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
</style>
-226
View File
@@ -1,226 +0,0 @@
<template>
<el-container class="lemon-container lemon-container--center"
ref="container">
<el-aside class="lemon-sidebar"
width="240px">
<ul class="lemon-tab">
<li v-for="item in tabList"
:key="item.name"
:tab-name="item.name"
:class="['lemon-tab__item', item.name == currentTab && 'lemon-tab__item--active']"
@click="tabChange(item.name)">
<span :class="item.icon"></span>
</li>
</ul>
<div class="lemon-tabview">
<div class="lemon-tabview__item"
v-for="item in tabList"
v-show="item.name == currentTab"
:key="item.name"
:tabview-name="item.name">
<component :is="item.componentName"
@changeMessageView="_changeMessageView"></component>
</div>
</div>
</el-aside>
<el-container class="lemon-main">
<el-header class="lemon-header"
height="48px">
宜宾劲越二手车市场上江北 (500)
</el-header>
<el-main>
<lemon-message-view></lemon-message-view>
</el-main>
<el-main>工具欄</el-main>
</el-container>
</el-container>
</template>
<script>
import LemonContactList from '../contact-list'
import LemonGroupList from '../group-list'
import LemonMessageList from '../message-list'
import LemonMessageView from '../message-view'
const components = {
LemonContactList,
LemonGroupList,
LemonMessageList,
LemonMessageView
}
export default {
name: 'LemonIm',
components,
provide () {
return {
control: this
}
},
props: {
friends: {
type: Array,
default: () => []
},
groups: {
type: Array,
default: () => []
}
},
data () {
this.tabList = [{
name: 'message',
icon: 'el-icon-s-comment',
componentName: 'lemon-message-list',
}, {
name: 'contact',
icon: 'el-icon-user-solid',
componentName: 'lemon-contact-list',
}, {
name: 'group',
icon: 'el-icon-message-solid',
componentName: 'lemon-group-list',
}]
return {
//当前会话对象的ID 根据 currentTab
currentTab: 'contact',
//当前聊天用户ID 根据聊天类型不一样 代表用户ID 群组ID 讨论组ID
currentMessageId: null,
//聊天视图是否加载中
messageViewloading: true,
//群聊天记录 groupId => [message]
groupMessageData: {
},
//好友聊天记录 friendId => [message]
friendMessageData: {
}
};
},
created () {
},
mounted () {
this._initialize()
},
computed: {
//聊天窗口中的数据
messageViewData () {
return this.friendMessageData[this.currentMessageId]
}
},
watch: {},
methods: {
//左侧选项卡切换
tabChange (name) {
this.currentTab = name;
this.findNode(`.lemon-tabview`).scrollTop = '0px'
},
//打开一个群组聊天窗口
openGroupMessageView (group) {
this._changeMessageViewComplete(group.id)
},
//打开普通聊天窗口
openFriendMessageView (friend) {
if (!this.friendMessageData[friend.id]) {
this.$emit('pull-friends-message', friend, (newData) => {
this._changeMessageViewComplete(friend.id)
this.friendMessageData[friend.id] = [...newData]
})
} else {
this._changeMessageViewComplete(friend.id)
}
},
_changeMessageViewComplete (id) {
this.messageViewloading = false
this.currentMessageId = id
},
_changeMessageView (item) {
this.openFriendMessageView(item)
/**
const resolve = (newData) => {
this.messageViewloading = false
this.
}
const reject = () => {
this.messageViewloading = false
}
this.$emit('pullFriendsMessage', item, {
resolve,
reject
})
*/
},
_openMessageView () {
},
findNode (query) {
return this.$refs.container.$el.querySelector(query)
},
_initialize () {
},
//将左侧的滚动条重置到顶部
_tabviewScrollToTop () {
},
}
}
</script>
<style lang='scss'>
@import '~styles/utils/index';
body {
background: #8d9198;
}
@include b(container) {
width: 900px;
height: 700px;
@include m(center) {
@include position-center(fixed);
}
}
@include b(sidebar) {
background: #1f252d;
display: flex;
flex-direction: column;
color: #fff;
overflow: hidden;
.el-tabs--card {
.el-tabs__nav,
.el-tabs__item,
.el-tabs__header {
border: none;
}
}
}
@include b(tab) {
display: flex;
width: 100%;
@include e(item) {
cursor: pointer;
line-height: 56px;
text-align: center;
flex: 1;
transition: all ease-in-out 0.3s;
font-size: 22px;
color: #6d6d6d;
@include m(active) {
color: #11d207;
}
}
}
@include b(tabview) {
flex: 1;
overflow-y: auto;
@include scrollbar-dark();
@include e(item) {
}
}
@include b(main) {
background: #eceef1;
}
@include b(header) {
line-height: 48px;
}
</style>
@@ -1,29 +0,0 @@
<template>
<div class='lemon-message-list'>
</div>
</template>
<script>
export default {
name: 'MessageList',
inject: ["control"],
data () {
return {
};
},
created () {
},
mounted () {
},
computed: {},
watch: {},
methods: {
}
}
</script>
<style lang='scss'>
</style>
@@ -1,31 +0,0 @@
<template>
<div class='lemon-message-view'
v-loading="control.messageViewloading"
element-loading-background="transparent">
{{ control.messageViewData }}
</div>
</template>
<script>
export default {
name: 'MessageView',
components: {},
inject: ["control"],
created () {
},
mounted () {
},
computed: {},
watch: {},
methods: {
}
}
</script>
<style lang='scss'>
@import '~styles/utils/index';
@include b(message-view) {
height: 100%;
}
</style>
+178
View File
@@ -0,0 +1,178 @@
<script>
export default {
name: "lemonMessageBasic",
props: {
message: {
type: Object,
default: () => {
return {};
}
},
timeFormat: {
type: Function,
default: () => ""
},
reverse: Boolean,
hiddenTitle: Boolean
},
data() {
return {};
},
render() {
const { fromUser, status, sendTime } = this.message;
return (
<div
class={[
"lemon-message",
{
"lemon-message--reverse": this.reverse,
"lemon-message--hidden-title": this.hiddenTitle
}
]}
>
<div class="lemon-message__avatar">
<lemon-avatar
size={36}
shape="square"
src={fromUser.avatar}
on-click={() => {
console.log("message avatar click");
}}
/>
</div>
<div class="lemon-message__inner">
<div class="lemon-message__title">
<span
on-click={() => {
console.log("message displayname click");
}}
>
{fromUser.displayName}
</span>
<span class="lemon-message__time">{this.timeFormat(sendTime)}</span>
</div>
<div
class="lemon-message__content"
on-click={() => {
console.log("message content click");
}}
>
{this.useScopedSlots("content", this.message)}
</div>
<div class="lemon-message__status">{this._renderStatue(status)}</div>
</div>
</div>
);
},
created() {},
mounted() {},
computed: {},
watch: {},
methods: {
_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;
}
}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
arrow()
content ' '
position absolute
top 6px
width 0
height 0
border 4px solid transparent
+b(lemon-message)
display flex
padding 10px 0
+e(time)
color #bbb
padding 0 4px
+e(inner)
position relative
+e(avatar)
padding-right 10px
user-select none
.lemon-avatar
cursor pointer
+e(title)
display flex
font-size 12px
line-height 14px
padding-bottom 6px
user-select none
color #999
+e(content)
font-size 14px
line-height 20px
padding 8px 10px
background #fff
border-radius 4px
position relative
margin 0 46px 0 0
img
video
background #e9e9e9
height 100px
&:before
arrow()
left -4px
border-left none
border-right-color #fff
+e(status)
position absolute
top 23px
right 20px
color #aaa
font-size 20px
+m(reverse)
flex-direction row-reverse
+e(title)
flex-direction row-reverse
+e(status)
left 20px
right auto
+e(content)
background #35d863
margin 0 0 0 46px
&:before
arrow()
left auto
right -4px
border-right none
border-left-color #35d863
+e(title)
text-align right
+e(avatar)
padding-right 0
padding-left 10px
+m(hidden-title)
+e(status)
top 7px
+e(title)
display none
+e(content)
&:before
top 14px
</style>
+27
View File
@@ -0,0 +1,27 @@
<script>
export default {
name: "lemonMessageEvent",
inheritAttrs: false,
render() {
const { content } = this.$attrs.message;
return (
<div class="lemon-message lemon-message-event">
<span class="lemon-message-event__content">{content}</span>
</div>
);
}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
+b(lemon-message-event)
+e(content)
user-select none
display inline-block
background #e9e9e9
color #aaa
font-size 12px
margin 0 auto
padding 5px 10px
border-radius 4px
</style>
+59
View File
@@ -0,0 +1,59 @@
<script>
import { formatByte } from "utils";
export default {
name: "lemonMessageFile",
inheritAttrs: false,
render() {
return (
<lemon-message-basic
class="lemon-message-file"
props={{ ...this.$attrs }}
scopedSlots={{
content: props => [
<div class="lemon-message-file__inner">
<p class="lemon-message-file__name">{props.fileName}</p>
<p class="lemon-message-file__byte">
{formatByte(props.fileSize)}
</p>
</div>,
<div class="lemon-message-file__sfx">
<i class="lemon-icon-attah" />
</div>
]
}}
/>
);
}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
+b(lemon-message-file)
+b(lemon-message)
+e(content)
display flex
cursor pointer
width 200px
background #fff
padding 12px 18px
overflow hidden
p
margin 0
+e(tip)
display none
+e(inner)
flex 1
+e(name)
font-size 14px
+e(byte)
font-size 12px
color #aaa
+e(sfx)
display flex
align-items center
justify-content center
font-weight bold
user-select none
font-size 34px
color #ccc
</style>
+30
View File
@@ -0,0 +1,30 @@
<script>
export default {
name: "lemonMessageImage",
inheritAttrs: false,
render() {
return (
<lemon-message-basic
class="lemon-message-image"
props={{ ...this.$attrs }}
scopedSlots={{
content: props => <img src={props.content} />
}}
/>
);
}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
+b(lemon-message-image)
+b(lemon-message)
+e(content)
padding 0
cursor pointer
overflow hidden
img
max-width 100%
min-width 100px
display block
</style>
+35
View File
@@ -0,0 +1,35 @@
<script>
import IMUIProxy from "mixins/IMUIProxy";
export default {
name: "lemonMessageText",
inheritAttrs: false,
mixins: [IMUIProxy],
render() {
return (
<lemon-message-basic
class="lemon-message-text"
props={{ ...this.$attrs }}
scopedSlots={{
content: props => {
const content = this.IMUI.replaceEmojiName(props.content);
return <span domProps={{ innerHTML: content }} />;
}
}}
/>
);
}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
+b(lemon-message-text)
+b(lemon-message)
+e(content)
img
width 18px
height 18px
display inline-block
background transparent
padding 0 2px
vertical-align middle
</style>
+142
View File
@@ -0,0 +1,142 @@
<script>
import { hoursTimeFormat } from "utils";
export default {
name: "LemonMessages",
components: {},
props: {
reverseUserId: String,
timeRange: {
type: Number,
default: 1
},
timeFormat: {
type: Function,
default(val) {
return hoursTimeFormat(val);
}
},
messages: {
type: Array,
default: () => []
}
},
data() {
return {
loading: false,
loadend: false
};
},
render() {
return (
<div class="lemon-messages" ref="wrap" on-scroll={this._handleScroll}>
<div
class={[
"lemon-messages__load",
`lemon-messages__load--${this.loadend ? "end" : "ing"}`
]}
>
{this.loadend ? this._renderLoadEnd() : this._renderLoading()}
</div>
{this.messages.map((message, index) => {
const node = [];
const tagName = `lemon-message-${message.type}`;
const prev = this.messages[index - 1];
if (
prev &&
this.msecRange &&
message.sendTime - prev.sendTime > this.msecRange
) {
node.push(
<lemon-message-event
attrs={{
message: {
id: "__time__",
type: "event",
content: this.timeFormat(message.sendTime)
}
}}
/>
);
}
node.push(
<tagName
ref="message"
refInFor={true}
attrs={{
timeFormat: this.msecRange > 0 ? () => {} : this.timeFormat,
message: message,
reverse: this.reverseUserId == message.fromUser.id,
hiddenTitle: false
}}
/>
);
return node;
})}
</div>
);
},
computed: {
msecRange() {
return this.timeRange * 1000 * 60;
}
},
watch: {},
methods: {
_renderLoading() {
return <i class="lemon-icon-loading lemonani-spin" />;
},
_renderLoadEnd() {
return <span>暂无消息</span>;
},
resetLoadState() {
this.loading = false;
this.loadend = false;
},
async _handleScroll(e) {
const { target } = e;
if (
target.scrollTop == 0 &&
this.loading == false &&
this.loadend == false
) {
this.loading = true;
await this.$nextTick();
const hst = target.scrollHeight;
this.$emit("reach-top", async isEnd => {
await this.$nextTick();
target.scrollTop = target.scrollHeight - hst;
this.loading = false;
this.loadend = !!isEnd;
});
}
},
async scrollToBottom() {
await this.$nextTick();
const { wrap } = this.$refs;
if (wrap) wrap.scrollTop = wrap.scrollHeight;
}
},
created() {},
mounted() {}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
+b(lemon-messages)
height 400px
overflow-x hidden
overflow-y auto
scrollbar-light()
padding 10px 15px
+e(time)
text-align center
font-size 12px
+e(load)
font-size 12px
text-align center
color #999
line-height 30px
+m(ing)
font-size 22px
</style>
+143
View File
@@ -0,0 +1,143 @@
<script>
const popoverCloseQueue = [];
const popoverCloseAll = () => popoverCloseQueue.forEach(callback => callback());
const triggerEvents = {
hover(el) {},
focus(el) {
el.addEventListener("focus", e => {
this.changeVisible();
});
el.addEventListener("blur", e => {
this.changeVisible();
});
},
click(el) {
el.addEventListener("click", e => {
e.stopPropagation();
this.changeVisible();
});
},
contextmenu(el) {
el.addEventListener("contextmenu", e => {
e.preventDefault();
this.changeVisible();
});
}
};
export default {
name: "LemonPopover",
props: {
trigger: {
type: String,
default: "click",
validator(val) {
return Object.keys(triggerEvents).includes(val);
}
}
},
data() {
return {
popoverStyle: {},
visible: false
};
},
created() {
document.addEventListener("click", this._documentClickEvent);
popoverCloseQueue.push(this.close);
},
mounted() {
triggerEvents[this.trigger].call(this, this.$slots.default[0].elm);
},
render() {
return (
<span style="position:relative">
<transition name="slide-top">
{this.visible && (
<div
class="lemon-popover"
ref="popover"
style={this.popoverStyle}
on-click={e => e.stopPropagation()}
>
<div class="lemon-popover__title" />
<div class="lemon-popover__content">{this.$slots.content}</div>
<div class="lemon-popover__arrow" />
</div>
)}
</transition>
{this.$slots.default}
</span>
);
},
destroyed() {
document.removeEventListener("click", this._documentClickEvent);
},
computed: {},
watch: {
async visible(val) {
if (val) {
await this.$nextTick();
const defaultEl = this.$slots.default[0].elm;
const contentEl = this.$refs.popover;
this.popoverStyle = {
top: `-${contentEl.offsetHeight + 10}px`,
left: `${defaultEl.offsetWidth / 2 - contentEl.offsetWidth / 2}px`
};
}
}
},
methods: {
_documentClickEvent(e) {
e.stopPropagation();
if (this.visible) this.close();
},
changeVisible() {
this.visible ? this.close() : this.open();
},
open() {
popoverCloseAll();
this.visible = true;
},
close() {
this.visible = false;
}
}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
+b(lemon-popover)
border 1px solid #eee
border-radius 4px
font-size 14px
font-variant tabular-nums
line-height 1.5
color rgba(0, 0, 0, 0.65)
z-index 10
background-color #fff
border-radius 4px
box-shadow 0 2px 8px rgba(0, 0, 0, 0.15)
position absolute
transform-origin 50% 150%
+e(content)
padding 15px
box-sizing border-box
position relative
z-index 1
+e(arrow)
left 50%
transform translateX(-50%) rotate(45deg)
position absolute
z-index 0
bottom -4px
box-shadow 3px 3px 7px rgba(0, 0, 0, 0.07)
width 8px
height 8px
background #fff
.slide-top-leave-active ,.slide-top-enter-active
transition all .3s cubic-bezier(0.645, 0.045, 0.355, 1)
.slide-top-enter, .slide-top-leave-to
transform translateY(-10px) scale(.8)
opacity 0
</style>
+77
View File
@@ -0,0 +1,77 @@
<script>
export default {
name: "LemonTabs",
props: {
activeIndex: String
},
data() {
return {
active: this.activeIndex
};
},
mounted() {
if (!this.active) {
this.active = this.$slots["tab-pane"][0].data.attrs.index;
}
},
render() {
const pane = [];
const nav = [];
this.$slots["tab-pane"].map(vnode => {
const { tab, index } = vnode.data.attrs;
pane.push(
<div class="lemon-tabs-content__pane" v-show={this.active == index}>
{vnode}
</div>
);
nav.push(
<div
class={[
"lemon-tabs-nav__item",
this.active == index && "lemon-tabs-nav__item--active"
]}
on-click={() => this._handleNavClick(index)}
>
{tab}
</div>
);
});
return (
<div class="lemon-tabs">
<div class="lemon-tabs-content">{pane}</div>
<div class="lemon-tabs-nav">{nav}</div>
</div>
);
},
methods: {
_handleNavClick(index) {
this.active = index;
}
}
};
</script>
<style lang="stylus">
@import '~styles/utils/index'
pane-color = #f6f6f6
+b(lemon-tabs)
background pane-color
+b(lemon-tabs-content)
width 100%
height 100%
padding 15px
+e(pane)
//scrollbar-light()
//overflow-y auto
height 100%
width 100%
+b(lemon-tabs-nav)
display flex
background #eee
+e(item)
line-height 38px
padding 0 15px
cursor pointer
transition all .3s cubic-bezier(0.645, 0.045, 0.355, 1)
+m(active)
background pane-color
</style>