video_ca/src/views/Chat.vue

801 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<el-dialog
title="图片发送"
v-model="sendImgDialogVisible"
width="60%"
height="60%"
center
>
<!-- 图片输入 -->
<input type="file" @change="handleFileUpload" />
<el-button type="primary" size="default" @click="sendImageOrVideo" >发送</el-button>
</el-dialog>
<el-container
class="layout-container-demo"
style="height: 700px; width: 1000px"
>
<el-aside width="200px">
<el-input
placeholder="请输入用户姓名"
v-model="searchName"
@input="filterUsers"
></el-input>
<el-scrollbar>
<el-menu :default-openeds="['1', '3']">
<el-sub-menu index="1">
<template #title>
<el-icon>
<message /> </el-icon
>联系人
</template>
<el-scrollbar height="300px">
<el-menu-item-group v-for="user in filteredUsers" :key="user.id">
<el-menu-item
:index="String(user.id)"
@click="handleGetMessage(user.id)"
style="height: 40px"
>
{{ user.name }}
<span v-if="hasUnreadMsg[user.id]" class="unread-dot"></span>
</el-menu-item>
</el-menu-item-group>
</el-scrollbar>
</el-sub-menu>
<el-sub-menu index="2">
<template #title>
<el-icon><icon-menu /></el-icon>
群组
</template>
<el-scrollbar height="200px">
<el-menu-item-group v-for="item in groupList" :key="item.ID">
<!-- <template #title>Group 1</template> -->
<el-menu-item
:index="String(item.GroupName)"
@click="handleGetGroupMessage(item.ID)"
>{{ item.GroupName }}</el-menu-item
>
<span
v-if="hasUnreadMsg[`g_${item.ID}`]"
class="unread-dot"
></span>
<!-- <el-menu-item index="2-2">Option 2</el-menu-item> -->
</el-menu-item-group>
</el-scrollbar>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</el-aside>
<el-container width="800px">
<el-header style="text-align: right; font-size: 12px">
<div class="toolbar">
<el-button type="primary" size="mini" @click.prevent="getHistory()"
>历史消息</el-button
>
<el-button
type="primary"
size="mini"
@click.prevent="handleMenuSelect('/user')"
>返回</el-button
>
<el-text class="mx-1" type="primary"
>目前:{{ cur_user_name }}</el-text
>
<div>
<span style="margin-right: 10px; margin-left: 10px">{{
username
}}</span>
<el-avatar size="default" fit="fit"> {{ username }} </el-avatar>
</div>
</div>
</el-header>
<el-main style="margin-top: 1px">
<el-scrollbar
class="chat-room"
id="chat-room"
ref="chatRoom"
height="600px"
style="margin-top: 1px"
always
>
<div
v-for="item in MsgList"
:key="item.ID"
style="margin-top: 10px; margin-bottom: 20px"
>
<!-- 左边 -->
<div
v-if="
uid == item.ToUserID &&
item.FromUserID == cur_user_id &&
cur_group_id == 0
"
style="margin-left: 10px; margin-bottom: 8px"
>
<el-row class="row-bg" type="flex" align="middle">
<el-avatar size="default" fit="fit">{{
cur_user_name
}}</el-avatar>
<span style="margin-left: 10px"
>{{ cur_user_name }} : {{ formatTime(item.CreatedAt) }}</span
>
</el-row>
<el-row>
<el-col :span="1000" :offset="1" class="msg">
<!-- {{ item.Msg }} -->
<div v-html="renderMarkdown(item.Msg)"></div>
</el-col>
</el-row>
</div>
<div
v-if="cur_group_id != 0 && uid != item.FromUserID"
style="margin-left: 10px; margin-bottom: 8px"
>
<el-row class="row-bg" type="flex" align="middle">
<el-avatar size="default" fit="fit">{{ item.name }}</el-avatar>
<span style="margin-left: 10px"
>{{ item.name }} : {{ formatTime(item.CreatedAt) }}</span
>
</el-row>
<el-row>
<el-col :span="1000" :offset="1" class="msg">
<!-- {{ item.Msg }} -->
<div v-html="renderMarkdown(item.Msg)"></div>
</el-col>
</el-row>
</div>
<!-- 右边 -->
<div
v-if="uid == item.FromUserID && item.ToUserID == cur_user_id"
style="margin-right: 10px; margin-bottom: 8px"
>
<el-row class="row-bg" type="flex" justify="end" align="middle">
<span style="margin-right: 10px">
{{ tokenData.username }} :
{{ formatTime(item.CreatedAt) }}</span
>
<el-avatar size="default" fit="fit">
{{ tokenData.username }}
</el-avatar>
</el-row>
<el-row justify="end">
<el-col :span="1000" class="msg2" style="margin-right: 20px">
<!-- {{ item.Msg }} -->
<div v-html="renderMarkdown(item.Msg)"></div>
</el-col>
</el-row>
</div>
</div>
</el-scrollbar>
</el-main>
<div>
</div>
<div>
<el-row class="row-bg" type="flex" justify="space-around" align="middle">
<el-button
type="primary"
size="default"
@click="SendImage"
class="send-image-button-bg"
>
文件发送
</el-button>
<el-col :span="20">
<el-input
type="textarea"
style="width: 100%"
rows="1"
autofocus
@keyup.enter="handleSendBtnClick"
placeholder="请输入消息按Enter发送"
v-model="currentMsg"
></el-input>
</el-col>
<el-col :span="2">
<el-button
class="sendBtn"
@click="handleSendBtnClick"
type="primary"
size="default"
>
发送
</el-button>
</el-col>
</el-row>
</div>
</el-container>
</el-container>
</template>
<script>
import axios from "axios";
import { inject } from "vue";
import { ref } from 'vue';
import { getFriendListService } from "@/api/chat.js";
import { getMessageService } from "@/api/chat.js";
import { sendMessageService } from "@/api/chat.js";
import { UploadFileService } from "@/api/tool.js";
import FileUpload from "@/views/FileUpload.vue";
import {
ElAvatar,
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElIcon,
ElMenu,
ElMenuItem,
ElMenuItemGroup,
ElScrollbar,
ElRow,
ElCol,
ElButton,
} from "element-plus";
import { Menu as IconMenu, Message, Setting } from "@element-plus/icons-vue";
import router from "@/router/index.js";
import { ElMessage } from "element-plus";
import MarkdownIt from 'markdown-it';
export default {
data() {
return {
ip: "",
tableData: [],
sendImgDialogVisible: false,
file : null,
tokenData: {
token: localStorage.getItem("token"),
ip: localStorage.getItem("ip"),
userId: localStorage.getItem("userId"),
username: localStorage.getItem("username"),
},
username: localStorage.getItem("username"),
userList: [],
filteredUsers: [], // 过滤后的用户列表
to_user_id: 0,
cur_user_id: 0,
cur_group_id: 0,
msg_type: 1,
searchName: "", // 搜索框内容
history_cnt: 1,
md: new MarkdownIt(), //md解析器
cur_user_name: "",
eventSource: null, // 事件源
uid: localStorage.getItem("userId"),
currentMsg: "",
MsgList: [],
groupList: [],
hasUnreadMsg: {}, // 未读消息, key为用户idvalue为true/false
};
},
// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件处理器绑定
methods: {
async handleSelectUser() {
let result = {};
try {
result = await getFriendListService(this.tokenData);
} catch (e) {
console.log(e);
}
let data = result.data;
this.userList = data.friends;
for (let i = 0; i < data.friends.length; i++) {
this.hasUnreadMsg[data.friends[i].id] = false;
}
this.groupList = data.groups;
},
filterUsers() {
this.filteredUsers = this.userList.filter((user) => {
return user.name.toLowerCase().includes(this.searchName.toLowerCase());
});
},
getHistory() {
this.history_cnt++;
this.handleGetMessage(this.cur_user_id);
},
async handleGetGroupMessage(id) {
this.cur_group_id = id;
this.cur_user_id = 0;
this.to_user_id = 0;
this.msg_type = 2;
this.hasUnreadMsg["g_" + id] = false;
let result = {};
try {
let req = {
token: localStorage.getItem("token"),
ip: localStorage.getItem("ip"),
userId: localStorage.getItem("userId"),
username: localStorage.getItem("username"),
group_id: id,
index: this.history_cnt,
type: 2,
};
result = await getMessageService(req);
} catch (e) {
console.log(e);
}
let data = result.data;
if (data === undefined || data === null) {
ElMessage({
message: "无消息!",
type: "error",
});
this.MsgList = [];
return;
}
data.sort((a, b) => {
const dateA = new Date(a.CreatedAt);
const dateB = new Date(b.CreatedAt);
// 返回时间差,用于排序
return dateA - dateB;
});
this.MsgList = data;
if (this.history_cnt <= 2) {
this.scrollToBottom();
}
},
async handleGetMessage(id) {
let result = {};
if (this.to_user_id != id) {
this.history_cnt = 2;
}
this.to_user_id = id;
this.cur_user_id = id;
this.cur_group_id = 0;
this.msg_type = 1;
console.log("uid:", this.uid, "\tcur_user_id:", this.cur_user_id);
for (let i = 0; i < this.userList.length; i++) {
if (this.userList[i].id === id) {
this.cur_user_name = this.userList[i].name;
break;
}
}
this.hasUnreadMsg[id] = false;
try {
let req = {
token: localStorage.getItem("token"),
ip: localStorage.getItem("ip"),
userId: localStorage.getItem("userId"),
username: localStorage.getItem("username"),
from_user_id: id,
to_user_id: localStorage.getItem("userId"),
index: this.history_cnt,
type: this.msg_type,
};
result = await getMessageService(req);
} catch (e) {
console.log(e);
}
let data = result.data;
if (data === undefined) {
ElMessage({
message: "消息获取失败!",
type: "error",
});
return;
}
//data sort by created time
// for (let i = 0; i < data.length; i++) {
// for (let j = i + 1; j < data.length; j++) {
// if (data[i].CreatedAt > data[j].CreatedAt) {
// let temp = data[i];
// data[i] = data[j];
// data[j] = temp;
// }
// }
// }
data.sort((a, b) => {
const dateA = new Date(a.CreatedAt);
const dateB = new Date(b.CreatedAt);
// 返回时间差,用于排序
return dateA - dateB;
});
this.MsgList = data;
if (this.history_cnt <= 2) {
this.scrollToBottom();
}
},
formatTime(time) {
let date = new Date(time);
// 提取年、月、日、时、分、秒
const year = date.getFullYear();
const month = ("0" + (date.getMonth() + 1)).slice(-2); // 月份是从0开始的所以要加1
const day = ("0" + date.getDate()).slice(-2);
const hour = ("0" + date.getHours()).slice(-2);
const minute = ("0" + date.getMinutes()).slice(-2);
const second = ("0" + date.getSeconds()).slice(-2);
// 重新组合并返回格式化的日期时间字符串
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
},
renderMarkdown(markdown) {
// 使用 MarkdownIt 解析 Markdown 字符串并返回 HTML
return this.md.render(markdown);
},
SendImage() {
this.sendImgDialogVisible = true;
},
connectWebSocket() {
// 连接WebSocket
let _this = this;
if (typeof WebSocket == "undefined") {
console.log("浏览器不支持WebSocket");
} else {
console.log("浏览器支持WebSocket");
let socketUrl =
"wss://tx.ljsea.top/im/ws_v2?to_user_id=" +
this.to_user_id +
"&token=" +
this.tokenData.token;
// console.log("socketUrl:", socketUrl);
if (this.socket != null) {
this.socket.close();
this.socket = null;
}
// 开启一个websocket服务
this.socket = new WebSocket(socketUrl);
//打开事件
this.socket.onopen = function () {
this.loading = false;
//alert("连接成功");
ElMessage({
message: "连接成功",
type: "success",
});
};
this.socket.onerror = (error) => {
console.error("WebSocket Error:", error);
};
//关闭事件
this.socket.onclose = function () {
//alert("连接已关闭!");
ElMessage({
message: "连接已关闭",
type: "error",
});
};
// 浏览器端收消息,获得从服务端发送过来的文本消息
this.socket.onmessage = async function (msg) {
//console.log("收到数据====" + msg.data);
let data = JSON.parse(msg.data); // 对收到的json数据进行解析 类似这样的:
// console.log("收到数据====" + data);
// 如果服务器端发送过来的json数据
console.log("data.type:", data.type);
if (data.type == "msg") {
// 如果是消息类型,解密消息内容
// console.log("收到数据====" + JSON.stringify(msg.data));
console.log("msg_:", data.data);
let msg_data = JSON.parse(data.data);
_this.MsgList.push(msg_data);
if (
_this.cur_user_id != msg_data.FromUserID &&
_this.cur_group_id != msg_data.GroupID
) {
if (msg_data.GroupID === 0) {
_this.hasUnreadMsg[msg_data.FromUserID] = true;
} else {
_this.hasUnreadMsg["g_" + msg_data.GroupID] = true;
}
} else {
_this.scrollToBottom();
}
//console.log("msglist:", _this.MsgList);
// 构建消息内容
} else if (data.type == "check") {
//alert("对方已下线");
//console.log(data.type);
}
};
}
},
async connectSSE() {
if (!!window.EventSource) {
this.eventSource = new EventSource(
"https://gep.ljsea.xyz/im/sse_msg?token=" + this.tokenData.token
);
let this_ = this;
this.eventSource.onopen = function (event) {
console.log("连接已建立!");
};
this.eventSource.onmessage = function (event) {
console.log(event.data);
let msg = JSON.parse(event.data);
if (msg.type !== "check") {
alert(msg);
this_.MsgList.push(msg);
}
};
this.eventSource.onerror = (error) => {
//重新连接
this.connectSSE();
if (this.eventSource.readyState === EventSource.CLOSED) {
//console.log('Connection was closed by the server.');
ElMessage({
message: "连接已关闭!:" + error,
type: "error",
});
// Optionally, try to reconnect
} else {
//console.error('EventSource failed:', error);
ElMessage({
message: "连接失败!服务器无法实时推送消息!",
type: "error",
});
// Handle error
}
};
} else {
ElMessage({
message: "您的浏览器不支持SSE",
type: "error",
});
}
},
scrollToBottom() {
this.$nextTick(() => {
const scrollbar = this.$refs.chatRoom.$el.querySelector(
".el-scrollbar__wrap"
);
if (scrollbar) {
scrollbar.scrollTop = scrollbar.scrollHeight;
}
});
},
async handleSendBtnClick() {
if (this.currentMsg == "" || this.currentMsg === null) {
ElMessage.error("消息不能为空!");
return;
}
let result = {};
try {
let req = {
token: localStorage.getItem("token"),
ip: localStorage.getItem("ip"),
userId: localStorage.getItem("userId"),
username: localStorage.getItem("username"),
from_user_id: localStorage.getItem("userId"),
to_user_id: this.to_user_id,
group_id: this.cur_group_id,
msg: this.currentMsg,
type: this.msg_type,
};
result = await sendMessageService(req);
if (result.code !== 0) {
ElMessage({
message: "消息发送失败!",
type: "error",
});
}
let msg = {
ID: result.Data,
FromUserID: localStorage.getItem("userId"),
ToUserID: this.to_user_id,
GroupID: this.cur_group_id,
Msg: this.currentMsg,
CreatedAt: new Date(),
};
this.MsgList.push(msg);
this.scrollToBottom();
} catch (e) {
console.log(e);
}
this.currentMsg = "";
},
async getIpClient() {
try {
const response = await axios.get("https://ipinfo.io/json");
this.ip = response.data.ip;
localStorage.setItem("ip", this.ip);
//console.log(response);
} catch (error) {
console.error(error);
}
},
handleMenuSelect(val) {
router.push(val);
//关闭websocket
if (this.socket!= null) {
this.socket.close();
}
},
handleFileUpload(e) {
this.file = e.target.files[0];
console.log("file has been selected:", this.file);
},
async sendImageOrVideo(){
if (this.file == null) {
alert('请先选择要上传的文件');
return;
}
//查看文件是否是图片或视频
try {
let formData = new FormData();
formData.append('file', this.file);
//console.log("add file: " + this.file);
formData.append('upload_type', "1");
formData.append('auth_type', "public");
//console.log("formData:",formData);
let result = await UploadFileService(formData,this.tokenData.token);
if (result.code!== 0) {
ElMessage.error('上传文件失败,请稍后再试');
return;
}
let resp_data = result.data;
//console.log("resp:",resp_data);
let url = "https://gep.ljsea.top/tool/file/"+resp_data.file_store_name;
let msg_ ="";
//markdown 图片格式
let fileType = this.file.name.split('.')[1];
if (!['jpg', 'jpeg', 'png', 'gif', 'mp4'].includes(fileType)) {
//alert('请选择正确的图片或视频格式');
msg_ = `文件:[${resp_data.file_name}](${url})`;
}else{
msg_ = `![${resp_data.file_name}](${url})`;
}
let req = {
token: localStorage.getItem("token"),
ip: localStorage.getItem("ip"),
userId: localStorage.getItem("userId"),
username: localStorage.getItem("username"),
from_user_id: localStorage.getItem("userId"),
to_user_id: this.to_user_id,
group_id: this.cur_group_id,
msg: msg_,
type: this.msg_type,
};
result = await sendMessageService(req);
if (result.code !== 0) {
ElMessage({
message: "消息发送失败!",
type: "error",
});
}
let msg = {
ID: result.data.id,
FromUserID: localStorage.getItem("userId"),
ToUserID: this.to_user_id,
GroupID: this.cur_group_id,
Msg: msg_,
CreatedAt: new Date(),
};
//console.log("msg:",msg);
this.MsgList.push(msg);
this.scrollToBottom();
this.sendImgDialogVisible = false;
} catch (error) {
ElMessage.error('上传文件时出现网络错误,请稍后再试');
console.error(error);
}
},
toVideoList() {
router.push("/videoList");
},
// 修改条纹颜色
tableRowClassName({ row, rowIndex }) {
if (row.human === 1) {
return {
background: "#488aff",
};
} else {
return "";
}
},
},
// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
async mounted() {
let now = new Date();
if (localStorage.getItem("token") === null) {
router.push("/login");
}
// console.log("mounted");
await this.getIpClient();
await this.handleSelectUser();
this.filteredUsers = this.userList;
//await this.connectSSE();
this.connectWebSocket();
this.handleGetMessage(this.userList[0].id);
},
onUnmounted() {
if (this.eventSource) {
this.eventSource.close();
}
console.log("unmounted chat");
},
};
</script>
<style scoped>
.layout-container-demo .el-header {
position: relative;
/* background-color: var(--el-color-primary-light-7); */
color: var(--el-text-color-primary);
}
.layout-container-demo .el-aside {
color: var(--el-text-color-primary);
/* background: var(--el-color-primary-light-8); */
}
.layout-container-demo .el-menu {
border-right: none;
}
.layout-container-demo .el-main {
padding: 0;
}
.layout-container-demo .toolbar {
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%;
right: 5px;
}
.msg {
background-color: #73d1f3;
/* box-shadow: rgba(18, 23, 45, 0.6) 0px 8px 24px; */
border-radius: 4px;
padding: 10px;
font-size: 18px;
line-height: 16px;
/* width: auto; */
/* max-width: 330px; */
margin-top: 20px;
color: white;
}
.msg2 {
background-color: #12b7f5;
/* box-shadow: rgba(18, 23, 45, 0.6) 0px 8px 24px; */
border-radius: 4px;
padding: 10px;
font-size: 18px;
line-height: 16px;
/* width: auto; */
/* max-width: 330px; */
margin-top: 20px;
color: white;
}
.unread-dot {
position: absolute;
top: 0;
right: 0;
width: 8px;
height: 8px;
background-color: red;
border-radius: 50%;
transform: translate(50%, -50%);
}
.send-image-button-bg {
background-image: url(../assets/img.jpg);
background-size: cover;
background-repeat: no-repeat;
}
</style>