添加聊天模块

This commit is contained in:
icssoa 2022-06-09 18:23:20 +08:00
parent e4ad03a744
commit 8218a4ba81
8 changed files with 471 additions and 76 deletions

View File

@ -9,7 +9,7 @@
"lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix" "lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix"
}, },
"dependencies": { "dependencies": {
"@cool-vue/crud": "^5.0.10", "@cool-vue/crud": "^5.0.11",
"@element-plus/icons-vue": "^1.1.3", "@element-plus/icons-vue": "^1.1.3",
"@vueuse/core": "^8.2.5", "@vueuse/core": "^8.2.5",
"axios": "^0.27.2", "axios": "^0.27.2",

View File

@ -18,18 +18,29 @@
append-to-body append-to-body
:controls="['slot-expand', 'cl-flex1', 'fullscreen', 'close']" :controls="['slot-expand', 'cl-flex1', 'fullscreen', 'close']"
> >
<div class="cl-chat"> <div
class="cl-chat"
:class="{
'is-mini': app.browser.isMini,
'is-expand': isExpand
}"
>
<div class="cl-chat__session">
<chat-session /> <chat-session />
</div>
<div class="cl-chat__right">
<chat-message /> <chat-message />
</div> </div>
</div>
<!-- 展开按钮 --> <!-- 展开按钮 -->
<template #slot-expand> <template #slot-expand>
<button class="cl-dialog__controls-icon"> <button class="cl-dialog__controls-icon">
<el-icon @click="session.visible = false" v-if="session.visible"> <el-icon @click="isExpand = true" v-if="!isExpand">
<notebook /> <notebook />
</el-icon> </el-icon>
<el-icon @click="session.visible = true" v-else> <el-icon @click="isExpand = false" v-else>
<arrow-left /> <arrow-left />
</el-icon> </el-icon>
</button> </button>
@ -47,13 +58,14 @@ export default defineComponent({
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { provide, ref, watch } from "vue"; import { nextTick, provide, ref, watch } from "vue";
import { module } from "/@/cool/utils"; import { module } from "/@/cool/utils";
import { useCool } from "/@/cool"; import { useCool } from "/@/cool";
import { useBase } from "/$/base"; import { useBase } from "/$/base";
import ChatMessage from "./message.vue"; import ChatMessage from "./message.vue";
import ChatSession from "./session.vue"; import ChatSession from "./session.vue";
import { Notebook, ArrowLeft, BellFilled } from "@element-plus/icons-vue"; import { Notebook, ArrowLeft, BellFilled } from "@element-plus/icons-vue";
import { debounce } from "lodash";
const { service } = useCool(); const { service } = useCool();
@ -64,19 +76,74 @@ const { app } = useBase();
const { options } = module.get("upload"); const { options } = module.get("upload");
// //
const visible = ref<boolean>(false); const visible = ref(false);
// //
const session = reactive({ const isExpand = ref(true);
visible: true,
//
const chat = reactive({
inputValue: "",
session: {
loading: false,
value: null,
list: [] list: []
},
message: {
loading: false,
list: [],
pagination: {}
},
//
scrollToBottom: debounce(() => {
nextTick(() => {
const box = document.querySelector(".cl-chat .chat-message .list");
box?.scroll({
top: 100000 + Math.random(),
behavior: "smooth"
});
});
}, 300),
//
async getSession() {
this.session.loading = true;
await service.im.session.page().then((res) => {
chat.session.list = res.list;
//
if (!this.session.value) {
this.setSession(res.list[0]);
}
});
this.session.loading = false;
},
//
async setSession(data: any) {
//
this.message.list = [];
//
this.session.value = data;
//
await this.getMessage();
//
this.scrollToBottom();
},
//
async getMessage() {
this.message.loading = true;
await service.im.message.page().then((res) => {
chat.message.list = res.list;
chat.message.pagination = res.pagination;
});
this.message.loading = false;
}
}); });
// //
watch( watch(
() => app.browser.isMini, () => app.browser.isMini,
(val) => { (val) => {
session.visible = val ? false : true; isExpand.value = val ? false : true;
}, },
{ {
immediate: true immediate: true
@ -93,9 +160,7 @@ function close() {
visible.value = false; visible.value = false;
} }
provide("chat", { provide("chat", chat);
session
});
defineExpose({ defineExpose({
open, open,
@ -106,12 +171,15 @@ defineExpose({
<style lang="scss"> <style lang="scss">
.cl-chat { .cl-chat {
display: flex; display: flex;
justify-content: flex-end;
background-color: #eee; background-color: #eee;
height: 100%; height: 100%;
padding: 5px; padding: 5px;
box-sizing: border-box; box-sizing: border-box;
position: relative;
&__icon { &__icon {
padding: 5px;
.el-badge__content { .el-badge__content {
transform: translateY(-50%) translateX(100%) scale(0.8) !important; transform: translateY(-50%) translateX(100%) scale(0.8) !important;
} }
@ -126,5 +194,42 @@ defineExpose({
&__footer { &__footer {
padding: 9px 0; padding: 9px 0;
} }
&__session {
height: calc(100% - 10px);
width: 345px;
position: absolute;
left: 5px;
top: 5px;
}
&__right {
position: relative;
z-index: 99;
transition: width 0.3s;
width: 100%;
}
&.is-mini {
&.is-expand {
.cl-chat__session {
z-index: 100;
}
}
.cl-chat__session {
width: calc(100% - 10px);
}
.cl-chat__right {
width: 100% !important;
}
}
&.is-expand {
.cl-chat__right {
width: calc(100% - 350px);
}
}
} }
</style> </style>

View File

@ -1,7 +1,57 @@
<template> <template>
<div class="cl-chat__message"> <div
<div class="head"></div> class="chat-message"
<div class="list scroller1"></div> v-loading="chat?.message.loading"
element-loading-text="消息列表加载中"
>
<!-- 头部 -->
<div class="head">
<template v-if="chat?.session.value">
<div class="avatar">
<el-avatar
:size="30"
shape="square"
:src="chat?.session.value.avatar"
></el-avatar>
</div>
<span class="name">{{ chat?.session.value.nickName }}聊天中</span>
<ul class="tools">
<li></li>
</ul>
</template>
</div>
<!-- 消息列表 -->
<div class="list scroller1">
<ul>
<li v-for="(item, index) in list" :key="index">
<div
class="item"
:class="{
'is-right': item.type == 1
}"
>
<div class="avatar">
<el-avatar :size="36" shape="square" :src="item.avatar"></el-avatar>
</div>
<div class="det">
<div class="h">
<span class="name">{{ item.nickName }}</span>
</div>
<div class="content">
<div class="is-text">
<span>{{ item.text }}</span>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
<!-- 底部 -->
<div class="footer"> <div class="footer">
<div class="tools"> <div class="tools">
<ul> <ul>
@ -10,35 +60,144 @@
</div> </div>
<div class="input"> <div class="input">
<el-input type="textarea" :rows="1" placeholder="输入内容"></el-input> <el-input
<el-button type="success">发送</el-button> v-model="chat.inputValue"
type="textarea"
:rows="4"
resize="none"
:autosize="{
minRows: 4,
maxRows: 10
}"
placeholder="输入内容"
></el-input>
<el-button size="small" type="success" @click="send">发送</el-button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup></script> <script lang="ts" setup>
import { computed } from "vue-demi";
import { useChat } from "../hooks";
const { chat } = useChat();
//
const list = computed(() => chat?.message.list);
function send() {
chat?.scrollToBottom();
}
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.cl-chat__message { .chat-message {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #fff; background-color: #fff;
flex: 1;
border-radius: 5px; border-radius: 5px;
height: 100%;
box-sizing: border-box;
.head { .head {
display: flex; display: flex;
align-items: center;
height: 50px; height: 50px;
border-bottom: 1px solid #eee; padding: 0 10px;
.name {
margin-left: 10px;
font-size: 14px;
}
ul {
li {
list-style: none;
}
}
} }
.list { .list {
flex: 1; flex: 1;
background-color: #f7f7f7;
ul {
& > li {
.date {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
font-size: 12px;
}
.item {
display: flex;
padding: 10px;
.avatar {
margin-right: 10px;
}
.det {
.h {
display: flex;
align-items: center;
.name {
font-size: 12px;
color: #666;
}
}
.content {
display: flex;
flex-direction: column;
margin-top: 5px;
.is-text {
background-color: #fff;
padding: 8px;
border-radius: 0 5px 5px 5px;
max-width: 400px;
font-size: 14px;
}
.is-img {
.el-image {
max-width: 300px;
border-radius: 3px;
}
}
}
}
&.is-right {
flex-direction: row-reverse;
.avatar {
margin-left: 10px;
margin-right: 0;
}
.det {
.h {
justify-content: flex-end;
}
.content {
.is-text {
border-radius: 5px 0 5px 5px;
}
}
}
}
}
}
}
} }
.footer { .footer {
border-top: 1px solid #eee;
padding: 10px; padding: 10px;
.tools { .tools {
@ -47,8 +206,8 @@
ul { ul {
li { li {
height: 30px; height: 25px;
width: 30px; width: 25px;
border-radius: 3px; border-radius: 3px;
background-color: #eee; background-color: #eee;
margin-right: 10px; margin-right: 10px;
@ -59,9 +218,13 @@
.input { .input {
display: flex; display: flex;
position: relative;
.el-button { .el-button {
margin-left: 10px; margin-left: 10px;
position: absolute;
right: 10px;
bottom: 10px;
} }
} }
} }

View File

@ -1,75 +1,85 @@
<template> <template>
<div <div class="chat-session">
class="cl-chat__session" <div class="head">
:class="{ <el-input v-model="keyWord" placeholder="关键字搜索" clearable></el-input>
'is-position': app.browser.isMini, </div>
'is-show': chat?.session.visible
}"
>
<div class="head"></div>
<div class="list scroller1"> <div class="list scroller1" v-loading="chat?.session.loading">
<div class="item" v-for="(item, index) in 13" :key="index"> <div
class="item"
v-for="(item, index) in list"
:key="index"
:class="{
'is-active': item.id == chat?.session.value?.id
}"
@click="toDetail(item)"
>
<div class="avatar"> <div class="avatar">
<el-badge value="2"> <el-badge :value="item.num" :hidden="item.num == 0">
<el-avatar shape="square"></el-avatar> <el-avatar shape="square" :src="item.avatar"></el-avatar>
</el-badge> </el-badge>
</div> </div>
<div class="det"> <div class="det">
<p class="name">神仙都没用</p> <p class="name">{{ item.nickName }}</p>
<p class="message"> <p class="message">
https://g0qwq7gr7l.feishu.cn/docx/doxcnkMF3PFehilJTyHbEivkUod {{ item.text }}
</p> </p>
</div> </div>
<div class="status"> <div class="status">
<p class="date">2022-04-21 12:22</p> <p class="date">{{ item.createTime }}</p>
<el-tag size="small">厦门</el-tag> <!-- <el-tag size="small">厦门</el-tag> -->
</div> </div>
</div> </div>
<el-empty v-if="list.length == 0" image-size="100" description="暂无会话"></el-empty>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref } from "vue";
import { useChat } from "../hooks"; import { useChat } from "../hooks";
import { useBase } from "/$/base"; import { useCool } from "/@/cool";
const { app } = useBase(); const { service } = useCool();
const { chat } = useChat(); const { chat } = useChat();
//
const keyWord = ref("");
//
const list = computed(
() => chat?.session.list.filter((e) => e.nickName.includes(keyWord.value)) || []
);
//
function toDetail(item: any) {
chat?.setSession(item);
}
onMounted(() => {
chat?.getSession();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.cl-chat__session { .chat-session {
height: 100%; height: 100%;
width: 0; width: 100%;
background-color: #fff; background-color: #fff;
overflow: hidden;
transition: width 0.2s ease-in-out;
border-radius: 5px; border-radius: 5px;
&.is-show {
width: 350px;
margin-right: 5px;
}
&.is-position {
position: absolute;
left: 5px;
top: 51px;
height: calc(100% - 56px);
z-index: 3000;
&.is-show {
width: calc(100% - 10px);
}
}
.head { .head {
display: flex; display: flex;
height: 50px;
border-bottom: 1px solid #f7f7f7; border-bottom: 1px solid #f7f7f7;
padding: 10px;
.el-input {
height: 30px;
background-color: #eee !important;
}
} }
.list { .list {
@ -101,7 +111,7 @@ const { chat } = useChat();
.name { .name {
font-size: 14px; font-size: 14px;
margin-bottom: 2px; margin-bottom: 4px;
} }
.message { .message {
@ -118,10 +128,15 @@ const { chat } = useChat();
.date { .date {
margin-bottom: 5px; margin-bottom: 5px;
color: #999;
} }
} }
&:hover { &.is-active {
background-color: #eee;
}
&:not(.is-active):hover {
background-color: #f7f7f7; background-color: #f7f7f7;
} }
} }

View File

@ -1,10 +1,32 @@
import { inject } from "vue"; import { inject } from "vue";
declare interface Item {
id: string;
avatar: string;
nickName: string;
[key: string]: any;
}
declare interface Chat { declare interface Chat {
inputValue: string;
session: { session: {
visible: boolean; loading: boolean;
list: any[]; value: Item;
list: Item[];
}; };
message: {
loading: boolean;
list: Item[];
pagination: {
page: number;
total: number;
size: number;
};
};
scrollToBottom(): void;
getSession(params?: any): void;
setSession(data: any): void;
getMessage(params?: any): void;
} }
export function useChat() { export function useChat() {

View File

@ -0,0 +1,47 @@
import { BaseService, Service } from "/@/cool";
import Mock from "mockjs";
@Service("im/message")
class ImMessage extends BaseService {
page() {
return new Promise((resolve) => {
const data = Mock.mock({
"list|20": [
{
id: "@id",
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,
avatar() {
return Mock.Random.image(
"40x40",
Mock.Random.color(),
"#FFF",
"png",
this.nickName[0]
);
}
}
]
});
setTimeout(() => {
resolve({
list: data.list,
pagination: {
total: 20,
page: 1,
size: 20
}
});
}, 1000);
});
}
}
export default ImMessage;

View File

@ -0,0 +1,43 @@
import { BaseService, Service } from "/@/cool";
import Mock from "mockjs";
@Service("im/session")
class ImSession extends BaseService {
page() {
return new Promise((resolve) => {
const data = Mock.mock({
"list|20": [
{
id: "@id",
nickName: "@name",
createTime: "@datetime(HH:mm:ss)",
text: "@cparagraph(5)",
"num|0-99": 0,
avatar() {
return Mock.Random.image(
"40x40",
Mock.Random.color(),
"#FFF",
"png",
this.nickName[0]
);
}
}
]
});
setTimeout(() => {
resolve({
list: data.list,
pagination: {
total: 20,
page: 1,
size: 20
}
});
}, 1000);
});
}
}
export default ImSession;

View File

@ -984,10 +984,10 @@
"@babel/helper-validator-identifier" "^7.16.7" "@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@cool-vue/crud@^5.0.10": "@cool-vue/crud@^5.0.11":
version "5.0.10" version "5.0.11"
resolved "https://registry.npmjs.org/@cool-vue/crud/-/crud-5.0.10.tgz#c2d70504fccdf89c907e1d32e62a93dd777fba9c" resolved "https://registry.npmjs.org/@cool-vue/crud/-/crud-5.0.11.tgz#fbca5084c4d1e9e82fba00f17bbbb984bd2ee38e"
integrity sha512-a3jZPS+Y/+7IJTA3iYjD7PK83rwtIDrnE0tcf5LfenZ7JnnlXs+QFMRybGypJfoeswsG97JmWFeSY6o/JyALDw== integrity sha512-0N1w5RCZqDKz5DLmhwXNLxZIbJPt/lkW8DuFzXB/cpeZTYYLc+5VubbD+KTah1YyA6C2js+Wt1MlKhC0gY6ROA==
dependencies: dependencies:
array.prototype.flat "^1.2.4" array.prototype.flat "^1.2.4"
core-js "^3.21.1" core-js "^3.21.1"