添加聊天模块

This commit is contained in:
icssoa 2022-06-14 16:07:47 +08:00
parent f70f4b5d73
commit 68858f794c
17 changed files with 1713 additions and 98 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -9,14 +9,14 @@
"lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix"
},
"dependencies": {
"@cool-vue/crud": "^5.0.11",
"@cool-vue/crud": "^5.0.13",
"@element-plus/icons-vue": "^1.1.3",
"@vueuse/core": "^8.2.5",
"axios": "^0.27.2",
"codemirror": "^5.62.0",
"core-js": "^3.6.5",
"echarts": "^5.0.2",
"element-plus": "^2.2.0",
"element-plus": "^2.2.5",
"file-saver": "^2.0.5",
"js-beautify": "^1.13.5",
"lodash": "^4.17.21",

View File

@ -1,7 +1,7 @@
<template>
<div class="cl-chat__wrap">
<div class="cl-chat__icon" @click="open">
<el-badge :value="19">
<el-badge :value="unCount">
<el-icon><BellFilled /></el-icon>
</el-badge>
</div>
@ -15,6 +15,7 @@
keep-alive
custom-class="cl-chat__dialog"
:close-on-click-modal="false"
:close-on-press-escape="false"
append-to-body
:controls="['slot-expand', 'cl-flex1', 'fullscreen', 'close']"
>
@ -50,12 +51,9 @@
</template>
<script lang="ts">
import { computed, defineComponent, reactive } from "vue";
import { useStore } from "../store";
export default defineComponent({
export default {
name: "cl-chat"
});
};
</script>
<script lang="ts" setup>
@ -69,6 +67,9 @@ import io from "socket.io-client";
import { Socket } from "socket.io-client";
import ChatMessage from "./message.vue";
import ChatSession from "./session.vue";
import { Chat } from "../types";
import { useStore } from "../store";
import dayjs from "dayjs";
const { mitt } = useCool();
@ -79,7 +80,7 @@ const { session, message } = useStore();
const { app, user } = useBase();
// 模块配置
const { options } = module.get("upload");
const { options } = module.get("chat");
// 是否可见
const visible = ref(false);
@ -87,6 +88,9 @@ const visible = ref(false);
// 是否展开
const isExpand = ref(true);
// 未读消息
const unCount = ref(parseInt(Math.random() * 100));
// Socket
let socket: Socket;
@ -95,7 +99,7 @@ function connect() {
return refresh();
if (!socket) {
socket = io(config.host + `/chat`, {
socket = io(config.host + options.path, {
auth: {
token: user.token
}
@ -130,6 +134,29 @@ function close() {
visible.value = false;
}
// 发送消息
function send(data: Chat.Message, isAppend?: boolean) {
// socket.emit("message", {});
if (isAppend) {
append(data);
}
}
// 追加消息
function append(data: Chat.Message) {
message.list.push({
fromId: user.info?.id,
toId: session.value?.userId,
avatar: user.info?.headImg,
nickName: user.info?.nickName,
createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
...data
});
scrollToBottom();
}
// 滚动到底部
const scrollToBottom = debounce(() => {
nextTick(() => {
@ -150,6 +177,7 @@ async function refresh() {
provide("chat", {
socket,
send,
scrollToBottom
});

View File

@ -21,20 +21,38 @@
<div
class="item"
:class="{
'is-right': item.type == 1
'is-right': item.isMy
}"
>
<div class="avatar">
<el-avatar :size="36" shape="square" :src="item.avatar"></el-avatar>
</div>
<div class="det">
<div
class="det"
@contextmenu="
(e) => {
onContextMenu(e, item);
}
"
>
<div class="h">
<span class="name">{{ item.nickName }}</span>
</div>
<div class="content">
<div class="is-text">
<span>{{ item.text }}</span>
<!-- 文本 -->
<div class="is-text" v-if="item.contentType == 0">
<span>{{ item.content.text }}</span>
</div>
<!-- 图片 -->
<div class="is-image" v-else-if="item.contentType == 1">
<el-image
:src="item.content.imageUrl"
:preview-src-list="previewUrls"
:initial-index="item._index"
scroll-container=".chat-message .list"
/>
</div>
</div>
</div>
@ -47,7 +65,11 @@
<div class="footer">
<div class="tools">
<ul>
<li></li>
<li>
<cl-upload @success="onImageSend" :show-file-list="false">
<el-icon><Picture /></el-icon>
</cl-upload>
</li>
</ul>
</div>
@ -63,7 +85,7 @@
}"
placeholder="输入内容"
></el-input>
<el-button size="small" type="success" @click="send" :disabled="!value"
<el-button size="small" type="success" @click="onTextSend" :disabled="!value"
>发送</el-button
>
</div>
@ -75,17 +97,93 @@
import { computed, ref } from "vue";
import { useChat } from "../hooks";
import { useStore } from "../store";
import { Picture } from "@element-plus/icons-vue";
import { useBase } from "/$/base";
import { ContextMenu } from "@cool-vue/crud";
import { useClipboard } from "@vueuse/core";
import { Chat } from "../types";
const { user } = useBase();
const { chat } = useChat();
const { message, session } = useStore();
const { copy } = useClipboard();
const value = ref("");
// 过滤列表
const list = computed(() => message.list);
const list = computed(() => {
let n = 0;
function send() {
chat?.scrollToBottom();
return message.list.map((e) => {
if (e.contentType == 1) {
e._index = n++;
}
// 是否自己发的消息
e.isMy = e.fromId == user.info?.id;
return e;
});
});
// 预览图片地址
const previewUrls = computed(() =>
message.list
.filter((e) => e.contentType == 1)
.map((e) => e.content?.imageUrl)
.filter(Boolean)
);
// 文本消息
function onTextSend() {
chat?.send(
{
contentType: 0,
content: {
text: value.value
}
},
true
);
value.value = "";
}
// 图片消息
function onImageSend(res: any) {
chat?.send(
{
contentType: 1,
content: {
imageUrl: res.url
}
},
true
);
value.value = "";
}
// 右键菜单
function onContextMenu(e: Event, item: Chat.Message) {
ContextMenu.open(e, {
hover: {
target: "content"
},
list: [
{
label: "复制",
callback(done) {
copy(item.content.text || "");
done();
}
},
{
label: "转发"
},
{
label: "删除"
}
]
});
}
</script>
@ -122,6 +220,8 @@ function send() {
ul {
& > li {
list-style: none;
.date {
display: flex;
align-items: center;
@ -161,9 +261,13 @@ function send() {
font-size: 14px;
}
.is-img {
.is-image {
background-color: #fff;
.el-image {
max-width: 300px;
display: block;
min-height: 100px;
max-width: 200px;
border-radius: 3px;
}
}
@ -204,12 +308,20 @@ function send() {
ul {
li {
height: 25px;
width: 25px;
height: 26px;
width: 26px;
border-radius: 3px;
background-color: #eee;
margin-right: 10px;
list-style: none;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
font-size: 18px;
&:hover {
background-color: #f7f7f7;
}
}
}
}

View File

@ -10,32 +10,33 @@
</ul>
</div>
<div class="list scroller1" v-loading="session?.loading">
<div
class="item"
v-for="(item, index) in list"
:key="index"
:class="{
'is-active': item.id == session?.value?.id
}"
@click="toDetail(item)"
>
<div class="avatar">
<el-badge :value="item.num" :hidden="item.num == 0">
<el-avatar shape="square" :src="item.avatar"></el-avatar>
</el-badge>
</div>
<div class="list" v-loading="session?.loading">
<div class="scroller1">
<div
class="item"
v-for="(item, index) in list"
:key="index"
:class="{
'is-active': item.id == session?.value?.id
}"
@click="toDetail(item)"
>
<div class="avatar">
<el-badge :value="item.num" :hidden="item.num == 0">
<el-avatar shape="square" :src="item.avatar"></el-avatar>
</el-badge>
</div>
<div class="det">
<p class="name">{{ item.nickName }}</p>
<p class="message">
{{ item.text }}
</p>
</div>
<div class="det">
<p class="name">{{ item.nickName }}</p>
<p class="message">
{{ item.text }}
</p>
</div>
<div class="status">
<p class="date">{{ item.createTime }}</p>
<!-- <el-tag size="small">厦门</el-tag> -->
<div class="status">
<p class="date">{{ item.createTime }}</p>
</div>
</div>
</div>
@ -49,19 +50,22 @@ import { computed, ref } from "vue";
import { useChat } from "../hooks";
import { useStore } from "../store";
import { Refresh } from "@element-plus/icons-vue";
import { Chat } from "../types";
const { chat } = useChat();
const { session } = useStore();
const { session, message } = useStore();
// 关键字
const keyWord = ref("");
// 过滤列表
const list = computed(() => session?.list.filter((e) => e.nickName.includes(keyWord.value)) || []);
const list = computed(() => session?.list.filter((e) => e.nickName?.includes(keyWord.value)) || []);
// 会话详情
function toDetail(item: any) {
chat?.setSession(item);
async function toDetail(item: Chat.Session) {
session.set(item);
await message.get({ page: 1 });
chat?.scrollToBottom();
}
</script>
@ -105,6 +109,12 @@ function toDetail(item: any) {
.list {
height: calc(100% - 51px);
overflow: hidden;
.scroller1 {
height: 100%;
}
.item {
display: flex;
padding: 15px 10px;

View File

@ -0,0 +1,4 @@
export default {
// socket.io 连接地址
path: "/chat"
};

View File

@ -2,7 +2,7 @@ import { inject } from "vue";
import { Chat } from "../types";
export function useChat() {
const chat = inject<Chat>("chat");
const chat = inject<Chat.Provide>("chat");
return {
chat

View File

@ -12,11 +12,8 @@ class ChatMessage extends BaseService {
nickName: "@name",
createTime: "@datetime(HH:mm:ss)",
text: "@cparagraph(5)",
content() {
return JSON.stringify({ text: this.text });
},
"contentType|0-3": 0,
"type|0-1": 0,
"contentType|0-1": 0,
"userId|1-2": 1,
avatar() {
return Mock.Random.image(
"40x40",
@ -25,6 +22,18 @@ class ChatMessage extends BaseService {
"png",
this.nickName[0]
);
},
content() {
return JSON.stringify({
text: this.text,
imageUrl: Mock.Random.image(
"100x100",
Mock.Random.color(),
"#FFF",
"png",
this.nickName
)
});
}
}
]

View File

@ -12,15 +12,27 @@ export const useMessageStore = defineStore("chat-message", () => {
const list = ref<any[]>([]);
// 分页
const pagination = ref();
const pagination = ref({
page: 1,
total: 0,
size: 20
});
// 获取列表
async function get(params?: any) {
loading.value = true;
// 清空
if (params?.page == 1) {
list.value = [];
}
// 发送请求
await service.chat.message.page(params).then((res) => {
list.value = res.list;
list.value = res.list.map((e) => {
e.content = JSON.parse(e.content);
return e;
});
pagination.value = res.pagination;
});

View File

@ -34,8 +34,6 @@ export const useSessionStore = defineStore("chat-session", () => {
// 设置值
function set(data: any) {
// 清空消息列表
list.value = [];
// 设置值
value.value = data;
}

View File

@ -1,31 +1,35 @@
import { Socket } from "socket.io-client";
export declare interface Item {
id: string;
avatar: string;
nickName: string;
[key: string]: any;
}
export namespace Chat {
enum ContentType {
"text" = 0,
"image" = 1,
"video" = 2
}
export declare interface Chat {
socket?: Socket;
inputValue: string;
session: {
loading: boolean;
value?: Item;
list: Item[];
};
message: {
loading: boolean;
list: Item[];
pagination: {
page: number;
total: number;
size: number;
interface Message {
fromId?: string;
toId?: string;
content: {
text?: string;
imageUrl?: string;
[key: string]: any;
};
};
scrollToBottom(): void;
getSession(params?: any): void;
setSession(data: any): void;
getMessage(params?: any): void;
contentType: ContentType;
[key: string]: any;
}
interface Session {
id: string;
avatar: string;
nickName: string;
[key: string]: any;
}
interface Provide {
socket?: Socket;
send(data: Message, isAppend?: boolean): void;
append(data: Message): void;
scrollToBottom(): void;
}
}

View File

@ -1,6 +1,14 @@
<template>
<div class="cl-upload__wrap">
<div class="cl-upload" :class="[`cl-upload--${type}`]">
<div
class="cl-upload"
:class="[
`cl-upload--${type}`,
{
'is-slot': $slots.default
}
]"
>
<!-- 列表 -->
<draggable
v-model="list"
@ -610,5 +618,15 @@ defineExpose({
width: 100%;
box-sizing: border-box;
}
&.is-slot {
.cl-upload__list {
margin: 0;
}
.un-drag {
display: flex;
}
}
}
</style>

View File

@ -26,7 +26,7 @@
<div class="cl-upload-space__header scroller1">
<el-button @click="refresh({ page: 1 })">刷新</el-button>
<div :style="{ marginLeft: '10px' }">
<div :style="{ margin: '0px 10px' }">
<cl-upload
ref="Upload"
type="file"

View File

@ -17,6 +17,7 @@
<li
v-for="(item, index) in flist"
:key="index"
class="item"
:class="{
'is-active': item.id == space.category.id
}"
@ -141,6 +142,9 @@ function onContextMenu(e: any, { id, name }: any) {
}
ContextMenu.open(e, {
hover: {
target: "item"
},
list: [
{
label: "刷新",
@ -238,7 +242,7 @@ onMounted(() => {
ul {
height: 100%;
li {
.item {
list-style: none;
font-size: 14px;
height: 40px;
@ -251,7 +255,7 @@ onMounted(() => {
color: var(--color-primary);
}
&:hover {
&:not(.cl-context-menu__target):hover {
background-color: #f7f7f7;
}
}

View File

@ -984,10 +984,10 @@
"@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0"
"@cool-vue/crud@^5.0.11":
version "5.0.11"
resolved "https://registry.npmjs.org/@cool-vue/crud/-/crud-5.0.11.tgz#fbca5084c4d1e9e82fba00f17bbbb984bd2ee38e"
integrity sha512-0N1w5RCZqDKz5DLmhwXNLxZIbJPt/lkW8DuFzXB/cpeZTYYLc+5VubbD+KTah1YyA6C2js+Wt1MlKhC0gY6ROA==
"@cool-vue/crud@^5.0.13":
version "5.0.13"
resolved "https://registry.npmjs.org/@cool-vue/crud/-/crud-5.0.13.tgz#6fe3dc2b0660ea5ccfa8a7420af97972de3887ec"
integrity sha512-yzzYWAlUKnOiJKX7+OQZgD39xXCyQCcuOWdp8foMWIWdUaWyHTxK3aFDd+V4EuK+T8lWbG+HggE784fKTxZJRQ==
dependencies:
array.prototype.flat "^1.2.4"
core-js "^3.21.1"
@ -1006,6 +1006,11 @@
resolved "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-1.1.4.tgz#5d2788ea356f1458068e6d400e724ca5f3d29aca"
integrity sha512-Iz/nHqdp1sFPmdzRwHkEQQA3lKvoObk8azgABZ81QUOpW9s/lUyQVUSh0tNtEPZXQlKwlSh7SPgoVxzrE0uuVQ==
"@element-plus/icons-vue@^2.0.5":
version "2.0.5"
resolved "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.0.5.tgz#8eb4143a7b5e4d8468d2e72af99eefee446f5ea0"
integrity sha512-jvNWyKcdvPvMDLTWjghrPY+bYHKqh7hbAFIPe+HWR073zilzt33csREzmKx3VwhdlJUW5u0nCqN+0rwI8jlH+w==
"@eslint/eslintrc@^0.4.3":
version "0.4.3"
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
@ -1026,6 +1031,11 @@
resolved "https://registry.npmjs.org/@floating-ui/core/-/core-0.6.2.tgz#f2813f0e5f3d5ed7af5029e1a082203dadf02b7d"
integrity sha512-jktYRmZwmau63adUG3GKOAVCofBXkk55S/zQ94XOorAHhwqFIOFAy1rSp2N0Wp6/tGbe9V3u/ExlGZypyY17rg==
"@floating-ui/core@^0.7.3":
version "0.7.3"
resolved "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz#d274116678ffae87f6b60e90f88cc4083eefab86"
integrity sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==
"@floating-ui/dom@^0.4.5":
version "0.4.5"
resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.4.5.tgz#2e88d16646119cc67d44683f75ee99840475bbfa"
@ -1033,6 +1043,13 @@
dependencies:
"@floating-ui/core" "^0.6.2"
"@floating-ui/dom@^0.5.2":
version "0.5.3"
resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.3.tgz#ade192cf9a911fc3e95fb614fe281658b654043c"
integrity sha512-vpjWB1uC7rajvgA58uzlJZgtWqrdDQLw+XVA3w63ZTmsWwRmVd0Gl5Dy9VMAViI9cP7hBWaJt23Jy3AVgVYnoQ==
dependencies:
"@floating-ui/core" "^0.7.3"
"@hapi/hoek@^9.0.0":
version "9.2.1"
resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17"
@ -1127,7 +1144,7 @@
resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
"@popperjs/core@npm:@sxzz/popperjs-es@^2.11.6":
"@popperjs/core@npm:@sxzz/popperjs-es@^2.11.6", "@popperjs/core@npm:@sxzz/popperjs-es@^2.11.7":
version "2.11.7"
resolved "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz#a7f69e3665d3da9b115f9e71671dae1b97e13671"
integrity sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==
@ -1812,6 +1829,15 @@
"@vueuse/shared" "8.4.2"
vue-demi "*"
"@vueuse/core@^8.6.0":
version "8.6.0"
resolved "https://registry.npmjs.org/@vueuse/core/-/core-8.6.0.tgz#a8f80363cc63d17382423f16beae57696f376e67"
integrity sha512-VirzExCm/N+QdrEWT7J4uSrvJ5hquKIAU9alQ37kUvIJk9XxCLxmfRnmekYc1kz2+6BnoyuKYXVmrMV351CB4w==
dependencies:
"@vueuse/metadata" "8.6.0"
"@vueuse/shared" "8.6.0"
vue-demi "*"
"@vueuse/metadata@8.2.5":
version "8.2.5"
resolved "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.2.5.tgz#51c7d95e04284ea378a5242a2e88b77494e2c117"
@ -1822,6 +1848,11 @@
resolved "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.4.2.tgz#b33e6b7bd5ca69e3f24ea41b149267118bcd566f"
integrity sha512-2BIj++7P0/I5dfMsEe8q7Kw0HqVAjVcyNOd9+G22/ILUC9TVLTeYOuJ1kwa1Gpr+0LWKHc6GqHiLWNL33+exoQ==
"@vueuse/metadata@8.6.0":
version "8.6.0"
resolved "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.6.0.tgz#34771443a72ee891ae001a70aa05dd9a1d799372"
integrity sha512-F+CKPvaExsm7QgRr8y+ZNJFwXasn89rs5wth/HeX9lJ1q8XEt+HJ16Q5Sxh4rfG5YSKXrStveVge8TKvPjMjFA==
"@vueuse/shared@8.2.5":
version "8.2.5"
resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-8.2.5.tgz#1ae200a240c4b8d42d41723b64d8f917aa57ff16"
@ -1836,6 +1867,13 @@
dependencies:
vue-demi "*"
"@vueuse/shared@8.6.0":
version "8.6.0"
resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-8.6.0.tgz#63dad9fc4b73a7fccbe5d6b97adeacf73d4fec41"
integrity sha512-Y/IVywZo7IfEoSSEtCYpkVEmPV7pU35mEIxV7PbD/D3ly18B3mEsBaPbtDkNM/QP3zAZ5mn4nEkOfddX4uwuIA==
dependencies:
vue-demi "*"
"@webassemblyjs/ast@1.11.1":
version "1.11.1"
resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
@ -2129,6 +2167,11 @@ async-validator@^4.0.7:
resolved "https://registry.npmjs.org/async-validator/-/async-validator-4.0.7.tgz#034a0fd2103a6b2ebf010da75183bec299247afe"
integrity sha512-Pj2IR7u8hmUEDOwB++su6baaRi+QvsgajuFB9j95foM1N2gy5HM4z60hfusIO0fBPG5uLAEl6yCJr1jNSVugEQ==
async-validator@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/async-validator/-/async-validator-4.1.1.tgz#3cd1437faa2de64743f7d56649dd904c946a18fe"
integrity sha512-p4DO/JXwjs8klJyJL8Q2oM4ks5fUTze/h5k10oPPKMiLe1fj3G1QMzPHNmN1Py4ycOk7WlO2DcGXv1qiESJCZA==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@ -2748,6 +2791,11 @@ dayjs@^1.11.1:
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz#fa0f5223ef0d6724b3d8327134890cfe3d72fbe5"
integrity sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==
dayjs@^1.11.3:
version "1.11.3"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz#4754eb694a624057b9ad2224b67b15d552589258"
integrity sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9"
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -2989,6 +3037,27 @@ element-plus@^2.2.0:
memoize-one "^6.0.0"
normalize-wheel-es "^1.1.2"
element-plus@^2.2.5:
version "2.2.5"
resolved "https://registry.npmjs.org/element-plus/-/element-plus-2.2.5.tgz#2bb889660c9bcb9bb71e18619915b35e0f48d569"
integrity sha512-Kl0yn/PQca5YQo3M3NPBP4Xl71NQuMtDx5zNXZGVyl5FjdMujXiFB9SXKYGDUCgFU3d/Rl14vB4Fpmcl2Iz+Hw==
dependencies:
"@ctrl/tinycolor" "^3.4.1"
"@element-plus/icons-vue" "^2.0.5"
"@floating-ui/dom" "^0.5.2"
"@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7"
"@types/lodash" "^4.14.182"
"@types/lodash-es" "^4.17.6"
"@vueuse/core" "^8.6.0"
async-validator "^4.1.1"
dayjs "^1.11.3"
escape-html "^1.0.3"
lodash "^4.17.21"
lodash-es "^4.17.21"
lodash-unified "^1.0.2"
memoize-one "^6.0.0"
normalize-wheel-es "^1.1.2"
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"