UPDATE
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user