im页面完成

This commit is contained in:
THK3121 2023-10-11 16:59:50 +08:00
parent 4dfbb67c65
commit 8c4c26f554
7 changed files with 852 additions and 7 deletions

View File

@ -13,8 +13,8 @@ TypeScript cannot handle type information for `.vue` imports by default, so we r
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
@ -44,5 +44,3 @@ npm run build
```sh
npm run lint
```
zmj 分支

22
src/api/talk.ts Normal file
View File

@ -0,0 +1,22 @@
import request from "@/utils/request/indexs";
/**
* scoket
*/
export const bindScoket = (data: any) =>
request.post({ url: "/common/im/doBindUid", data });
// 发送消息
export const sendMsgApi = (data: any) =>
request.post({ url: "/common/im/sendTextMsg", data });
// 发送文件
export const sendFileApi = (data: any) =>
request.post({ url: "/common/im/sendFileMsg", data });
// 消息列表
export const getMsgListApi = (data: any) =>
request.post({ url: "/common/im/msgList", data });
// 片区经理id
export const getAreaManagerApi = (data: any) =>
request.post({ url: "/common/im/getAreaManager", data });
// 联系人列表
export const getContactListApi = (data: any) =>
request.post({ url: "/common/im/contactList", data });

BIN
src/assets/images/SP.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
src/assets/images/XC.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -2,11 +2,19 @@ const config = {
terminal: 1, //终端
title: "后台管理系统", //网站默认标题
version: "1.6.0", //版本号
baseUrl: `${
import.meta.env.VITE_APP_BASE_URL
}/`, //请求接口域名
baseUrl: `${import.meta.env.VITE_APP_BASE_URL}/`, //请求接口域名
urlPrefix: "adminapi", //请求默认前缀
timeout: 20 * 1000, //请求超时时长
};
export default config;
// const config = {
// terminal: 1, //终端
// title: "后台管理系统", //网站默认标题
// version: "1.6.0", //版本号
// baseUrl: `https://worker-task.lihaink.cn`, //请求接口域名
// urlPrefix: "adminapi", //请求默认前缀
// timeout: 20 * 1000, //请求超时时长
// };
// export default config;

131
src/utils/request/indexs.ts Normal file
View File

@ -0,0 +1,131 @@
import { merge } from "lodash";
import configs from "@/config";
import { Axios } from "./axios";
import {
ContentTypeEnum,
RequestCodeEnum,
RequestMethodsEnum,
} from "@/enums/requestEnums";
import type { AxiosHooks } from "./type";
import { clearAuthInfo, getToken } from "../auth";
import feedback from "../feedback";
import NProgress from "nprogress";
import { AxiosError, type AxiosRequestConfig } from "axios";
import router from "@/router";
import { PageEnum } from "@/enums/pageEnum";
// 处理axios的钩子函数
const axiosHooks: AxiosHooks = {
requestInterceptorsHook(config) {
NProgress.start();
const { withToken, isParamsToData } = config.requestOptions;
const params = config.params || {};
const headers = config.headers || {};
// 添加token
if (withToken) {
const token = getToken();
headers.token = token;
}
// POST请求下如果无data则将params视为data
if (
isParamsToData &&
!Reflect.has(config, "data") &&
config.method?.toUpperCase() === RequestMethodsEnum.POST
) {
config.data = params;
config.params = {};
}
config.headers = headers;
return config;
},
requestInterceptorsCatchHook(err) {
NProgress.done();
return err;
},
async responseInterceptorsHook(response) {
NProgress.done();
const { isTransformResponse, isReturnDefaultResponse } =
response.config.requestOptions;
//返回默认响应,当需要获取响应头及其他数据时可使用
if (isReturnDefaultResponse) {
return response;
}
// 是否需要对数据进行处理
if (!isTransformResponse) {
return response.data;
}
const { code, data, show, msg } = response.data;
switch (code) {
case RequestCodeEnum.SUCCESS:
if (show) {
msg && feedback.msgSuccess(msg);
}
return data;
case RequestCodeEnum.FAIL:
if (show) {
msg && feedback.msgError(msg);
}
return Promise.reject(data);
case RequestCodeEnum.LOGIN_FAILURE:
clearAuthInfo();
router.push(PageEnum.LOGIN);
return Promise.reject();
case RequestCodeEnum.OPEN_NEW_PAGE:
window.location.href = data.url;
return data;
default:
return data;
}
},
responseInterceptorsCatchHook(error) {
NProgress.done();
if (error.code !== AxiosError.ERR_CANCELED) {
error.message && feedback.msgError(error.message);
}
return Promise.reject(error);
},
};
const defaultOptions: AxiosRequestConfig = {
//接口超时时间
timeout: configs.timeout,
// 基础接口地
baseURL: configs.baseUrl,
// baseURL: "http://192.168.1.11:8081/",
//请求头
headers: { "Content-Type": ContentTypeEnum.JSON, version: configs.version },
// 处理 axios的钩子函数
axiosHooks: axiosHooks,
// 每个接口可以单独配置
requestOptions: {
// 是否将params视为data参数仅限post请求
isParamsToData: true,
//是否返回默认的响应
isReturnDefaultResponse: false,
// 需要对返回数据进行处理
isTransformResponse: true,
// 接口拼接地址
urlPrefix: "",
// 忽略重复请求
ignoreCancelToken: false,
// 是否携带token
withToken: true,
// 开启请求超时重新发起请求请求机制
isOpenRetry: true,
// 重新请求次数
retryCount: 2,
},
};
function createAxios(opt?: Partial<AxiosRequestConfig>) {
return new Axios(
// 深度合并
merge(defaultOptions, opt || {})
);
}
const request = createAxios();
export default request;

View File

@ -0,0 +1,686 @@
<template>
<div class="box">
<div class="talk-list">
<div class="mt-4 serch">
<el-input v-model="queryUserName" placeholder="搜索联系人" class="input-with-select">
<template #append>
<el-button :icon="Search" @click="serchUserFn" />
</template>
</el-input>
</div>
<div style="height: 5vh;"></div>
<div class="contacts" v-for="(item, index) in userList" :key="index" @click="choseTalkFn(item, index)">
<div class="l">
<div style="position: relative;">
<el-image style="width: 40px; height: 40px" :src="item.avatar" />
<div class="brage" v-if="item.no_read_num">{{ item.no_read_num }}</div>
</div>
<div class="tit-a">
<div>{{ item.name }}</div>
<div v-if="item.last_msg_type == 'text'" class="value">{{ item.last_msg_content }}</div>
<div v-if="item.last_msg_type == 'image'" class="value">[图片]</div>
<div v-if="item.last_msg_type == 'video'" class="value">[视频]</div>
</div>
</div>
<div class="r">
{{ timeFn(item.last_msg_time) }}
</div>
</div>
</div>
<div class="talk-detail" id="content">
<div class="top">
{{ talkDetail.name }}
</div>
<div style="height: 4vh"></div>
<div class="center" id="center" @scroll="handleScroll">
<div class="talk" v-for="(item, index) in talkDetail.talkList" :key="index">
<div style="text-align: center;" v-show="index % 5 == 0">{{ timeFn(item.create_time) }}</div>
<!-- 我的消息 -->
<div class="my_task" v-if="item.from_user_id == user_id">
<div class="content">
<!-- 文本 -->
<div v-if="item.type == 'text'">
<a :href="item.content" v-if="isLink(item.content)" target="_blank"
style="color: #576B95;">{{
item.content }}
</a>
<div v-else> {{ item.content }}</div>
</div>
<!-- 图片 -->
<div v-if="item.type == 'image'">
<el-image style="width: 100px;height: auto;" :src="item.content" :preview-src-list="srcList"
@click="PreviewImgFn(item)"></el-image>
</div>
<!-- 视频 -->
<div v-if="item.type == 'video'">
<video controls :src="item.content"></video>
</div>
</div>
<el-image style="width: 40px; height: 40px" :src="userStore.userInfo.avatar" />
</div>
<!-- 对方消息 -->
<div class="to_task" v-else>
<el-image style="width: 40px; height: 40px;margin-right: 5px;" :src="item.from_user_avatar" />
<div class="content">
<!-- 文本 -->
<div v-if="item.type == 'text'">
<a :href="item.content" target="_blank" v-if="isLink(item.content)"
style="color: #576B95;">{{
item.content }}
</a>
<div v-else> {{ item.content }}</div>
</div>
<!-- 图片 -->
<div v-if="item.type == 'image'">
<el-image style="width: 100px;height: auto;" :src="item.content" :preview-src-list="srcList"
@click="PreviewImgFn(item)"></el-image>
</div>
<!-- 视频 -->
<div v-if="item.type == 'video'">
<video controls :src="item.content"></video>
</div>
</div>
</div>
</div>
<!-- 本地 -->
<div v-show="item.status" class="my_task" v-for="item, index in local">
<!-- <div></div> -->
<!-- <div >上传中</div> -->
<div class="content" v-loading="true">
<div style="width: 120px;margin-right: 10px;">
<video controls :src="item.localSrc" v-if="item.type == 'video'"></video>
<img controls :src="item.localSrc" v-else />
</div>
<!-- <el-progress :text-inside="true" :stroke-width="14" :percentage="item.percentage" /> -->
</div>
<el-image style="width: 40px; height: 40px;margin-right: 10px;" :src="url" />
</div>
</div>
<div class="bottom">
<div style="display: flex;">
<el-upload :action="base_url + '/common/im/sendFileMsg'" :data="{
from_user_id: user_id,
to_user_id: talkDetail.to_user_id,
scene: 1,
type: 'image',
msg_id: msg_id
}" multiple :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="changeFile">
<img class="img_cls" src="@/assets/images/XC.png" style="width: 50px;height: 50px;" />
</el-upload>
<el-upload :action="base_url + '/common/im/sendFileMsg'" :data="{
from_user_id: user_id,
to_user_id: talkDetail.to_user_id,
scene: 1,
type: 'video',
msg_id: msg_id
}" multiple :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="changeFile1">
<img style="width: 50px; height: 50px" src="@/assets/images/SP.png" />
</el-upload>
</div>
<el-input v-model="textarea" style="border: none;outline: none;" placeholder="请输入..."
@keydown="keyup_submit(event)" />
<div class="send-btn" @click="sendMsgFn">发送</div>
</div>
<!-- <div style="height: 30vh;"></div> -->
</div>
</div>
</template>
<script lang="ts" scoped setup>
import { reactive, ref, onBeforeUnmount } from 'vue';
import { Search } from '@element-plus/icons-vue'
import feedback from '@/utils/feedback'
import {
sendMsgApi, getMsgListApi, bindScoket, sendFileApi, getContactListApi
} from "@/api/talk"
import { ElMessage } from 'element-plus'
import useUserStore from "@/stores/modules/user";
import { inject } from "vue";
const base_url: any = inject("base_url").replace('adminapi', '');
const userStore = useUserStore();
const user_id = userStore.userInfo.id
let page_no = 1
let to_id: any = null
//
const srcList: any = reactive(
[]
)
const PreviewImgFn = (item: any) => {
srcList[0] = item.content
}
//
const handleScroll = (e) => {
let top = e.target.scrollTop
if (top <= 0) {
page_no++
getMsgListApi({
from_user_id: user_id,
to_user_id: to_id,
scene: 1,
page_no: page_no,
page_size: 10
}).then(res2 => {
if (res2.length <= 0) return
talkDetail.talkList = res2.reverse().concat(talkDetail.talkList)
e.target.scrollTop = 100
})
}
}
let bind = false
//
const userList = reactive([])
let queryList = reactive([])
const talkDetail = reactive({
name: "",
to_user_id: "",
talkList: [],
index: 0,
})
getContactListApi({ user_id: user_id }).then(res => {
res.forEach((element: any) => {
userList.push(element)
queryList.push(element)
});
// queryList = userList
console.log(queryList, 6666)
talkDetail.name = userList[0].name
talkDetail.to_user_id = userList[0].id
to_id = userList[0].id
getMsgListApi({
from_user_id: user_id,
to_user_id: userList[0].id,
scene: 1,
page_no: 1,
page_size: 10
}).then(res2 => {
talkDetail.talkList = res2.reverse()
// console.log(talkDetail)
})
})
const url = 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'
//
const queryUserName = ref('')
const serchUserFn = () => {
getContactListApi({ user_id: user_id, keywords: queryUserName.value }).then(res => {
userList.splice(0, userList.length);
res.forEach((element: any) => {
// @ts-ignore
userList.push(element)
// queryList.push(element)
});
})
}
//
const isLink = (text: string) => {
var pattern = /^(https?:\/\/|www\.|.*\.com).*$/i;
return pattern.test(text);
}
const textarea = ref("")
const sendMsgFn = () => {
if (!textarea.value) return
sendMsgApi({
from_user_id: user_id,
to_user_id: talkDetail.to_user_id,
type: "text",
content: textarea.value,
scene: 1,
msg_id: msg_id.value
}).then(res => {
talkDetail.talkList.push(res)
userList[talkDetail.index].last_msg_content = textarea.value
userList[talkDetail.index].last_msg_time = res.create_time
userList[talkDetail.index].last_msg_type = 'text'
// userList[talkDetail.index].last_msg_content = textarea.value
scrollFn()
textarea.value = ""
generateRandId()
})
}
const keyup_submit = (evt: any) => {
var evt = window.event || evt;
if (evt.keyCode == 13) {
sendMsgFn()
}
}
//
let msg_id: any = ref('')
const generateRandId = () => {
var d = new Date().getTime();
var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == "x" ? r : r & 0x3 | 0x8).toString(16);
});
// return uuid;
msg_id.value = uuid
}
generateRandId()
const local = reactive([])
let localData = () => {
return {
status: false,
localType: null,
localSrc: null,
type: "",
percentage: 0,
msg_id: msg_id.value
}
}
const changeFile = (res) => {
console.log(res.type)
if (!res.type.includes('image')) {
ElMessage.error('请选择正确的图片格式!!')
return false
}
local.push(localData())
let type = 'image'
var localIndex = local.findIndex(function (element) {
return element.msg_id === msg_id.value;
});
// userList[talkDetail.index].last_msg_type = type
let file = res
local[localIndex].type = type
const blob = file;
var reader = new FileReader();
reader.readAsDataURL(blob);
//
reader.onloadend = function (e) {
local[localIndex].status = true
local[localIndex].localSrc = e.target?.result
scrollFn()
generateRandId()
};
}
const changeFile1 = (res) => {
if (!res.type.includes('video')) {
ElMessage.error('请选择正确的视频格式!!')
return false
}
local.push(localData())
let type = 'video'
var localIndex = local.findIndex(function (element) {
return element.msg_id === msg_id.value;
});
// userList[talkDetail.index].last_msg_type = type
let file = res
local[localIndex].type = type
const blob = file;
var reader = new FileReader();
reader.readAsDataURL(blob);
//
reader.onloadend = function (e) {
local[localIndex].status = true
local[localIndex].localSrc = e.target?.result
scrollFn()
generateRandId()
};
}
const handleAvatarSuccess: any = (
res: any
) => {
if (res.show == 0) {
talkDetail.talkList.push(res.data)
}
else {
feedback.msgError(res.msg)
}
var localIndex = local.findIndex(function (element) {
return element.msg_id === res.data.msg_id;
});
local[localIndex].status = false
userList[talkDetail.index].last_msg_type = res.data.type
console.log(res.data)
}
//
const choseTalkFn = (item: any, index: number) => {
userList[index].no_read_num = 0
talkDetail.name = item.name
talkDetail.to_user_id = item.id
talkDetail.index = index
to_id = item.id
getMsgListApi({
from_user_id: user_id,
to_user_id: item.id,
scene: 1,
page_no: 1,
page_size: 10
}).then(res2 => {
talkDetail.talkList = res2.reverse()
scrollFn()
})
}
//
let scrollTop: any = null
onMounted(() => {
scrollTop = document.getElementById('center')
})
const scrollFn = () => {
setTimeout(() => {
scrollTop.scrollTop = 9999999
}, 100)
}
//
const timeFn = (time) => {
const currentDate = new Date(); //
const targetDate = new Date(time * 1000);
if (isSameDay(currentDate, targetDate)) {
const hours = targetDate.getHours(); //
const minutes = targetDate.getMinutes(); //
return hours + ':' + (minutes < 10 ? '0' + minutes : minutes);
} else if (isYesterday(currentDate, targetDate)) {
return '昨天';
}
const month = targetDate.getMonth() + 1; // 01
const day = targetDate.getDate(); //
return month + '月' + day + '日';
}
const isSameDay = (date1, date2) => {
return (
date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate()
);
}
const isYesterday = (currentDate, targetDate) => {
const yesterday = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1);
return isSameDay(yesterday, targetDate);
}
// scoket
const socket = new WebSocket('wss://worker-task.lihaink.cn/wss');
const onSocketOpen = (event: any) => {
console.log('WebSocket连接已打开');
}
const onSocketMessage = (event: any) => {
let data = JSON.parse(event.data)
if (!bind) {
let client_id = (JSON.parse(event.data).client_id)
bindScoket({
client_id: client_id,
user_id: user_id,
scene: 1,
}).then(res => {
bind = true
console.log('绑定成功')
})
}
else if (data.id) {
var index = userList.findIndex(function (element) {
return element.id == data.from_user_id;
});
// talkDetail.talkList.push(data)
if (data.from_user_id == talkDetail.to_user_id) {
console.log(data, "收到scoket消息")
talkDetail.talkList.push(data)
userList[index].last_msg_content = data.content
userList[index].last_msg_time = data.create_time
userList[index].last_msg_type = data.type
scrollFn()
} else {
userList[index].no_read_num++
userList[index].last_msg_content = data.content
userList[index].last_msg_time = data.create_time
userList[index].last_msg_type = data.type
// userList.forEach
}
}
console.log(data)
}
const onSocketClose = (event: any) => {
console.log('WebSocket连接已关闭');
}
const onSocketError = (event: any) => {
console.error('WebSocket连接发生错误');
}
socket.addEventListener('open', onSocketOpen);
socket.addEventListener('message', onSocketMessage);
socket.addEventListener('close', onSocketClose);
socket.addEventListener('error', onSocketError);
onBeforeUnmount(() => {
socket.close()
})
</script>
<style lang="scss" scoped>
:deep(.el-input__wrapper) {
box-shadow: none !important;
// border-bottom: 1px solid black;
border-radius: 0;
}
.box {
display: flex;
.serch {
background-color: #F6F6F6;
position: fixed;
top: 9vh;
width: 17vw;
z-index: 99;
padding-left: 10px;
padding-top: 10px;
}
.talk-list {
width: 20vw;
overflow: hidden;
height: 85vh;
position: relative;
// background-color: #fff;
cursor: pointer;
.contacts {
// margin-bottom: 10px;
// background-color: red;
padding: 10px;
display: flex;
justify-content: space-between;
.l {
display: flex;
.tit-a {
margin-left: 10px;
display: flex;
flex-direction: column;
justify-content: space-around;
.value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 8vw;
}
}
}
}
}
.contacts:active {
background-color: #fff;
}
.contacts:hover {
background-color: #fff;
}
.talk-list::-webkit-scrollbar {
width: 2px;
/* 滚动条宽度 */
}
.talk-list::-webkit-scrollbar-thumb {
background-color: #888;
/* 滚动条颜色 */
border-radius: 1px;
/* 滚动条圆角 */
}
.img_cls {
margin-right: 10PX;
margin-bottom: 10PX;
cursor: pointer;
}
.talk-list:hover {
// background-color: #888;
overflow: scroll;
}
.talk-detail {
width: 80vw;
height: 85vh;
.top {
position: fixed;
height: 4vh;
width: 70vw;
box-sizing: border-box;
padding: 10px;
background-color: #fff;
z-index: 9;
border-bottom: 1px solid #EAE8E7;
}
.center {
height: 65vh;
overflow-y: scroll;
background-color: #fff;
}
.bottom {
position: fixed;
background-color: white;
height: 20vh;
border-top: 1px solid #EAE8E7;
width: 70vw;
box-sizing: border-box;
// padding-top: 10px;
padding: 10px;
}
// overflow: scroll;
}
.center::-webkit-scrollbar {
width: 2px;
/* 滚动条宽度 */
}
.center::-webkit-scrollbar-thumb {
background-color: #888;
/* 滚动条颜色 */
border-radius: 5px;
/* 滚动条圆角 */
}
.talk {
// height: 4vh;
// background-color: #fff;
padding: 10px;
margin-bottom: 10px;
}
.send-btn {
width: 50px;
height: 30px;
background-color: green;
color: white;
line-height: 30px;
text-align: center;
border-radius: 5px;
float: right;
margin-top: 5px;
cursor: pointer;
}
.send-btn:active {
background-color: yellowgreen;
}
.my_task {
display: flex;
justify-content: flex-end;
.content {
background-color: #95EC69;
border-radius: 5px;
// height: 30px;
padding: 5px;
// height: 40px;
// min-height: 40px;
margin-right: 5px;
line-height: 30px;
max-width: 20vw;
// overflow: hidden;
word-break: break-all;
overflow-wrap: break-word;
}
}
.to_task {
display: flex;
// justify-content: flex-end;
.content {
background-color: #95EC69;
border-radius: 5px;
// height: 30px;
padding: 5px 10px;
// height: 40px;
// min-height: 40px;
margin-right: 5px;
line-height: 30px;
max-width: 20vw;
word-break: break-all;
overflow-wrap: break-word;
}
}
.brage {
background-color: red;
color: white;
width: 16px;
height: 16px;
border-radius: 16px;
text-align: center;
line-height: 16px;
position: absolute;
font-size: 12px;
top: -8px;
right: -5px;
}
}
</style>