添加im聊天功能,添加用户搜索功能

This commit is contained in:
junleea 2024-06-28 18:08:35 +08:00
parent df715f924d
commit 43915ff365
9 changed files with 633 additions and 1 deletions

6
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": {
"axios": "^1.6.5",
"cors": "^2.8.5",
"crypto-js": "^4.2.0",
"element-plus": "^2.4.4",
"js-md5": "^0.8.3",
"qrcode": "^1.5.3",
@ -1104,6 +1105,11 @@
"node": ">= 0.10"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",

View File

@ -11,6 +11,7 @@
"dependencies": {
"axios": "^1.6.5",
"cors": "^2.8.5",
"crypto-js": "^4.2.0",
"element-plus": "^2.4.4",
"js-md5": "^0.8.3",
"qrcode": "^1.5.3",

13
src/api/im.js Normal file
View File

@ -0,0 +1,13 @@
import request from '@/utils/request.js'
export const getImKeyService = (Data) => {
const params = new URLSearchParams();
for (let key in Data) {
params.append(key, Data[key])
}
return request.post('/im/get_imKey', params,{
headers: {
'token': Data.token, // 将 token 替换为您的令牌值
}
})
}

View File

@ -38,6 +38,19 @@ export const getUUIDService = (registerData) => {
return request.post('/user/uuid', params)
}
export const SearchUserService = (Data) => {
const params = new URLSearchParams();
for (let key in Data) {
params.append(key, Data[key])
}
return request.post('/user/search', params,{
headers: {
'token': Data.token, // 将 token 替换为您的令牌值
}
})
}
export const getQRService = (qrData) => {
const params = new URLSearchParams();
//console.log("qrdata=",qrData);

View File

@ -4,6 +4,8 @@ import LoginVue from "@/views/Login.vue";
import VideoVue from "@/views/Video.vue";
import VideoListVue from "@/views/VideoList.vue";
import DeviceListVue from "@/views/DeviceList.vue";
import UserListVue from "@/views/UserList.vue";
import ImVue from "@/views/Im.vue";
const routes = [
{
@ -26,6 +28,16 @@ const routes = [
name: 'Device',
component: DeviceListVue
},
{
path: '/im',
name: 'Im',
component: ImVue
},
{
path: '/user',
name: 'User',
component: UserListVue
},
{
path: '/',
redirect: '/login'

View File

@ -150,6 +150,9 @@ export default {
@click.prevent="handleMenuSelect('/device')"
>设备管理</el-button
>
<el-button type="primary" size="mini" @click.prevent="handleMenuSelect('/User')"
>用户</el-button
>
<el-container style="height: 700px; border: 1px solid #eee">
<el-header style="font-size: 40px; background-color: rgb(238, 241, 246)"
>监控设备列表</el-header

372
src/views/Im.vue Normal file
View File

@ -0,0 +1,372 @@
<template>
<div style="padding: 10px; margin-bottom: 50px">
<el-button
type="primary"
size="mini"
@click.prevent="handleMenuSelect('/videoList')"
>视频列表</el-button
>
<el-button
type="primary"
size="mini"
@click.prevent="handleMenuSelect('/device')"
>设备管理</el-button
>
<el-button
type="primary"
size="mini"
@click.prevent="handleMenuSelect('/User')"
>用户</el-button
>
<el-row>
<el-col :span="16">
<div
style="
width: 800px;
margin: 0 auto;
background-color: white;
border-radius: 5px;
box-shadow: 0 0 10px #ccc;
"
>
<div style="text-align: center; line-height: 50px">
Web聊天室{{ chatUser }}
</div>
<div
style="height: 350px; overflow: auto; border-top: 1px solid #ccc"
v-html="content"
></div>
<div style="height: 200px">
<textarea
v-model="text"
style="
height: 160px;
width: 100%;
padding: 20px;
border: none;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
outline: none;
"
></textarea>
<div style="text-align: right; padding-right: 10px">
<el-button type="primary" size="mini" @click="send"
>发送</el-button
>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { ref, onMounted, inject, onUnmounted } from "vue";
import { getImKeyService } from "@/api/im.js";
import router from "@/router/index.js";
import * as crypto from "crypto";
import CryptoJS from "crypto-js";
export default {
data() {
return {
circleUrl:
"https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png",
user: {},
isCollapse: false,
users: [{ username: "admin" }, { username: "zhang" }],
chatUser: "2002",
text: "",
messages: [],
session: "",
imKey: "testimkey",
socket: null,
content: "",
to_user_id: 0,
to_user_name: "",
cnt: 0,
intervalId: null,
tokenData: {
token: localStorage.getItem("token"),
ip: localStorage.getItem("ip"),
userId: localStorage.getItem("userId"),
username: localStorage.getItem("username"),
to_user_id: this.to_user_id,
},
intervalId: ref(null),
timerId: null, // ID
};
},
methods: {
async send() {
if (!this.text) {
this.$message({ type: "warning", message: "请输入内容" });
} else {
if (typeof WebSocket == "undefined") {
console.log("您的浏览器不支持WebSocket");
} else {
// json
// {"type":"msg","data":"hello","to_user_id":1,"from_user_id":2002,"session":"11917957"}
// let message = {
// type: "msg",
// data: "hello",
// to_user_id: this.to_user_id,
// from_user_id: this.tokenData.userId,
// session: this.session,
// };
// let json=JSON.stringify(message)
// var aesEnc = await this.aesEncrypt(
// this.text,
// this.imKey,
// this.session
// );
var aesEnc = await this.aesEncrypt(
this.text,
this.imKey,
this.imKey
);
let data =
'{"type":"msg","data":"' +
aesEnc +
'","to_user_id":' +
this.to_user_id +
',"from_user_id":' +
this.tokenData.userId +
',"session":"' +
this.session +
'"}';
this.socket.send(data); // json
this.messages.push(JSON.parse(data));
//
this.createContent(null, this.tokenData.username, this.text);
this.text = "";
}
}
},
startInterval() {
this.timerId = setInterval(() => {
this.getIMKey();
}, 1000); // 1000
},
handleMenuSelect(val) {
router.push(val);
},
stopInterval() {
clearInterval(this.timerId);
},
async getIMKey() {
let result = {};
try {
let req = {
to_user_id: this.to_user_id,
token: this.tokenData.token,
};
result = await getImKeyService(req);
} catch (e) {
console.log(e);
}
let data = result.data;
if (result.code != 0) {
this.cnt++;
if (this.cnt > 10) {
//
this.stopInterval();
alert("连接失败,请重试!");
router.push("/user");
}
return;
}
try {
if (data.is_read == 1) {
//
this.imKey = data.im_key;
this.session = data.im_session;
console.log("imKey:",this.imKey)
localStorage.setItem("imkey_" + this.to_user_id, this.imKey);
//
localStorage.setItem(
"imkey_" + this.to_user_id + "_expire",
data.expire
);
this.session = data.im_session;
//
this.stopInterval();
this.connectWebSocket();
} else {
this.cnt++;
if (this.cnt > 30) {
//
this.stopInterval();
alert("连接失败,请重试!");
router.push("/user");
}
}
} catch (e) {
console.log(e);
}
},
createContent(remoteUser, nowUser, text) {
// json html
var html;
//
if (nowUser) {
// nowUser 绿
html =
'<div class="el-row" style="padding: 5px 0">\n' +
' <div class="el-col el-col-22" style="text-align: right; padding-right: 10px">\n' +
' <div class="tip left">' +
text +
"</div>\n" +
" </div>\n" +
' <div class="el-col el-col-2">\n' +
' <span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;">\n' +
' <img src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" style="object-fit: cover;">\n' +
" </span>\n" +
" </div>\n" +
"</div>";
} else if (remoteUser) {
// remoteUser
html =
'<div class="el-row" style="padding: 5px 0">\n' +
' <div class="el-col el-col-2" style="text-align: right">\n' +
' <span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;">\n' +
' <img src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" style="object-fit: cover;">\n' +
" </span>\n" +
" </div>\n" +
' <div class="el-col el-col-22" style="text-align: left; padding-left: 10px">\n' +
' <div class="tip right">' +
text +
"</div>\n" +
" </div>\n" +
"</div>";
}
this.content += html;
},
aesEncrypt(text, password, iv) {
// CryptoJS使WordArrayBufferIVWordArray
const passwordWordArray = CryptoJS.enc.Utf8.parse(password);
const ivWordArray = CryptoJS.enc.Utf8.parse(iv);
// 使CryptoJSAES
const cipher = CryptoJS.AES.encrypt(
CryptoJS.enc.Utf8.parse(text), //
passwordWordArray, //
{
iv: ivWordArray, //
mode: CryptoJS.mode.CBC, //
padding: CryptoJS.pad.Pkcs7, //
}
);
// CryptoJSCipherParamsBase64
const encrypted = cipher.toString();
return encrypted;
},
decryptAES(encryptedData, secretKey, iv) {
// IVWordArray
const key = CryptoJS.enc.Utf8.parse(secretKey);
const ivData = CryptoJS.enc.Utf8.parse(iv);
// Base64
// CryptoJS.enc.Base64.parse WordArrayBase64
// Base64使 CryptoJS.enc.Base64.parse(CryptoJS.enc.Base64.stringify(someWordArray))
// 使encryptedDataBase64
try {
const decrypted = CryptoJS.AES.decrypt(encryptedData, key, {
iv: ivData,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
//
if (decrypted.sigBytes === 0) {
throw new Error("Decryption failed. Invalid data or password.");
}
// WordArray UTF-8
const decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
console.log("decryptedText: " + decryptedText,secretKey, iv,encryptedData);
return decryptedText;
} catch (error) {
//
console.error("Decryption error:", error);
throw error; //
}
},
connectWebSocket() {
// WebSocket
let _this = this;
if (typeof WebSocket == "undefined") {
console.log("您的浏览器不支持WebSocket");
} else {
console.log("您的浏览器支持WebSocket");
let socketUrl =
"wss://gep.ljsea.top/im/ws?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 () {
alert("websocket已打开");
};
this.socket.onerror = (error) => {
console.error("WebSocket Error:", error);
};
//
this.socket.onmessage = async function (msg) {
console.log("收到数据====" + msg.data);
let data = JSON.parse(msg.data); // json
// json users keyjson
data.data=await _this.decryptAES(data.data, _this.imKey, _this.imKey)
_this.messages.push(data);
console.log("收到数据====" + msg.data);
//
_this.createContent(_this.to_user_name, null, data.data);
};
}
},
},
// ,
mounted() {
console.log("chatUser:", this.chatUser);
this.to_user_id = localStorage.getItem("to_user_id");
this.to_user_name = localStorage.getItem("to_user_name");
this.chatUser = this.to_user_name;
//console.log("to_user_id:", this.to_user_id, this.to_user_name);
this.startInterval();
//this.connectWebSocket();
},
// ,
beforeDestroy() {
this.stopInterval();
this.socket.close();
},
};
</script>
<style>
.tip {
color: white;
text-align: center;
border-radius: 10px;
font-family: sans-serif;
padding: 10px;
width: auto;
display: inline-block !important;
display: inline;
}
.right {
background-color: deepskyblue;
}
.left {
background-color: forestgreen;
}
</style>

210
src/views/UserList.vue Normal file
View File

@ -0,0 +1,210 @@
<script>
import axios from "axios";
import { SearchUserService } from "@/api/user.js";
import router from "@/router/index.js";
export default {
data() {
return {
ip: "",
tableData: [],
tokenData: {
token: localStorage.getItem("token"),
ip: localStorage.getItem("ip"),
userId: localStorage.getItem("userId"),
username: localStorage.getItem("username"),
id: 2002,
keyword: "",
},
};
},
// methods
//
methods: {
async getUserList() {
let result = {};
try {
result = await SearchUserService(this.tokenData);
} catch (e) {
console.log(e);
}
let data = result.data;
// for(let d in data){
// let res = JSON.parse(d);
// console.log("res=",res);
// this.tableData.push(res);
// }
this.tableData = data;
},
onSubmit() {
getUserList({ token: token });
},
handleSizeChange() {
alert("每页记录数变化" + val);
},
handleCurrentChange() {
alert("页码发生变化" + val);
},
//
async startChat(index) {
var id = this.tableData[index].ID;
var name = this.tableData[index].Name;
// var user_data = {
// to_user_id: id,
// to_user_name: this.tableData[index].Name,
// };
//
localStorage.setItem("to_user_id", id);
localStorage.setItem("to_user_name", name);
router.push("/im");
},
handleMenuSelect(val) {
router.push(val);
},
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");
}
},
};
</script>
<template>
<div>
<el-button
type="primary"
size="mini"
@click.prevent="handleMenuSelect('/videoList')"
>视频列表</el-button
>
<el-button
type="primary"
size="mini"
@click.prevent="handleMenuSelect('/device')"
>设备管理</el-button
>
<el-button
type="primary"
size="mini"
@click.prevent="handleMenuSelect('/User')"
>用户</el-button
>
<el-container style="height: 700px; border: 1px solid #eee">
<el-header style="font-size: 40px; background-color: rgb(238, 241, 246)"
>用户搜索列表</el-header
>
<el-container>
<el-main>
<el-col :span="8">
<!-- 搜索与添加区域 -->
<el-input
placeholder="请输入ID"
v-model="tokenData.id"
clearable
@clear="getUserList"
>
</el-input>
<el-input
placeholder="请输入关键字"
v-model="tokenData.keyword"
clearable
@clear="getUserList"
>
</el-input>
<template #append>
<el-button @click="getUserList"
><el-icon><search /></el-icon
></el-button>
</template>
</el-col>
<!-- 表单 -->
<el-form :inline="true" :model="tokenData" class="demo-form-inline">
<el-form-item>
<el-button
class="el-button--danger"
type="primary"
@click="getUserList()"
>查询</el-button
>
</el-form-item>
</el-form>
<!-- 表格 :row-style="this.tableRowClassName"-->
<el-table :data="tableData" width="100%" border>
:row-style="this.tableRowClassName"
<el-table-column prop="ID" label="id" width="80"></el-table-column>
<el-table-column
prop="Name"
label="名称"
width="100"
></el-table-column>
<el-table-column
prop="Email"
label="用户邮箱"
width="180"
></el-table-column>
<el-table-column
prop="Age"
label="用户Age"
width="120"
></el-table-column>
<el-table-column
prop="Gender"
label="用户性别"
width="80"
></el-table-column>
<el-table-column label="操作" width="300">
<template #default="scope">
<el-button
type="primary"
size="mini"
@click.prevent="startChat(scope.$index)"
>聊天</el-button
>
<!-- <el-button type="danger" size="mini">删除</el-button> -->
</template>
</el-table-column>
</el-table>
<br />
<!-- 分页条 -->
<!-- Pagination 分页 -->
<!-- <el-pagination
background
layout="total,sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:total="1000"
></el-pagination> -->
</el-main>
</el-container>
</el-container>
</div>
</template>
<style>
.blueRowbg {
background: "#488aff";
}
</style>

View File

@ -1,6 +1,5 @@
<script>
import axios from "axios";
import { inject } from "vue";
import { getVideoListService, quashVideoService } from "@/api/video.js";
import { delayVideoService } from "@/api/video.js";
import router from "@/router/index.js";
@ -226,6 +225,9 @@ export default {
<el-button type="primary" size="mini" @click.prevent="handleMenuSelect('/device')"
>设备管理</el-button
>
<el-button type="primary" size="mini" @click.prevent="handleMenuSelect('/User')"
>用户</el-button
>
<el-container style="height: 700px; border: 1px solid #eee">
<el-header style="font-size: 40px; background-color: rgb(238, 241, 246)"
>监控视频列表</el-header