添加聊天模块

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,7 +10,8 @@
</ul>
</div>
<div class="list scroller1" v-loading="session?.loading">
<div class="list" v-loading="session?.loading">
<div class="scroller1">
<div
class="item"
v-for="(item, index) in list"
@ -35,7 +36,7 @@
<div class="status">
<p class="date">{{ item.createTime }}</p>
<!-- <el-tag size="small">厦门</el-tag> -->
</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 {
export namespace Chat {
enum ContentType {
"text" = 0,
"image" = 1,
"video" = 2
}
interface Message {
fromId?: string;
toId?: string;
content: {
text?: string;
imageUrl?: string;
[key: string]: any;
};
contentType: ContentType;
[key: string]: any;
}
interface Session {
id: string;
avatar: string;
nickName: string;
[key: string]: any;
}
}
export declare interface Chat {
interface Provide {
socket?: Socket;
inputValue: string;
session: {
loading: boolean;
value?: Item;
list: Item[];
};
message: {
loading: boolean;
list: Item[];
pagination: {
page: number;
total: number;
size: number;
};
};
send(data: Message, isAppend?: boolean): void;
append(data: Message): void;
scrollToBottom(): void;
getSession(params?: any): void;
setSession(data: any): void;
getMessage(params?: any): 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"