mirror of https://gitee.com/godoos/godoos.git
17 changed files with 1364 additions and 574 deletions
@ -1,115 +1,144 @@ |
|||
<script setup lang="ts"> |
|||
import { useChatStore } from '@/stores/chat'; |
|||
import { Search } from '@element-plus/icons-vue'; |
|||
import {getWorkflowUrl} from '@/system/config' |
|||
const store = useChatStore() |
|||
const workUrl = getWorkflowUrl() |
|||
onMounted(() => { |
|||
store.initChat() |
|||
}) |
|||
import { useChatStore } from "@/stores/chat"; |
|||
import { getWorkflowUrl } from "@/system/config"; |
|||
import { Search } from "@element-plus/icons-vue"; |
|||
const store = useChatStore(); |
|||
const workUrl = getWorkflowUrl(); |
|||
onMounted(() => { |
|||
store.initChat(); |
|||
// store.initSSE(); |
|||
}); |
|||
</script> |
|||
<template> |
|||
<el-container class="container"> |
|||
<!--菜单--> |
|||
<el-aside class="menu"> |
|||
<chat-menu /> |
|||
</el-aside> |
|||
<el-container class="side" v-if="store.currentNavId < 3"> |
|||
<!--搜索栏--> |
|||
<el-header class="search" v-if="store.currentNavId < 2"> |
|||
<el-input placeholder="搜索" :prefix-icon="Search" class="search-input" v-model="store.search" /> |
|||
</el-header> |
|||
<!--好友列表--> |
|||
<el-main class="list"> |
|||
<el-scrollbar> |
|||
<chat-msg-list v-if="store.currentNavId == 0" /> |
|||
<chat-user-list v-if="store.currentNavId == 1" /> |
|||
<!-- <chat-work-list v-if="store.currentNavId == 2" /> --> |
|||
</el-scrollbar> |
|||
</el-main> |
|||
</el-container> |
|||
<el-container class="chat-box"> |
|||
<chat-box v-if="store.currentNavId < 2" /> |
|||
</el-container> |
|||
<el-container class="chat-setting" v-if="store.currentNavId == 2"> |
|||
<iframe class="workflow" :src="workUrl"></iframe> |
|||
</el-container> |
|||
<el-container class="chat-setting" v-if="store.currentNavId == 5"> |
|||
<ChatUserSetting /> |
|||
</el-container> |
|||
</el-container> |
|||
|
|||
<el-container class="container"> |
|||
<!--菜单--> |
|||
<el-aside class="menu"> |
|||
<chat-menu /> |
|||
</el-aside> |
|||
<el-container |
|||
class="side" |
|||
v-if="store.currentNavId < 3" |
|||
> |
|||
<!--搜索栏--> |
|||
<el-header |
|||
class="search" |
|||
v-if="store.currentNavId < 2" |
|||
> |
|||
<el-input |
|||
placeholder="搜索" |
|||
:prefix-icon="Search" |
|||
class="search-input" |
|||
v-model="store.search" |
|||
/> |
|||
</el-header> |
|||
<!--好友列表--> |
|||
<el-main class="list"> |
|||
<el-scrollbar> |
|||
<chat-msg-list v-if="store.currentNavId == 0" /> |
|||
<chat-user-list v-if="store.currentNavId == 1" /> |
|||
<!-- <chat-work-list v-if="store.currentNavId == 2" /> --> |
|||
</el-scrollbar> |
|||
</el-main> |
|||
</el-container> |
|||
<el-container class="chat-box"> |
|||
<chat-box v-if="store.currentNavId < 1" /> |
|||
<chat-user-info v-if="store.currentNavId ==1"></chat-user-info> |
|||
</el-container> |
|||
<el-container |
|||
class="chat-setting" |
|||
v-if="store.currentNavId == 2" |
|||
> |
|||
<iframe |
|||
class="workflow" |
|||
:src="workUrl" |
|||
></iframe> |
|||
</el-container> |
|||
<el-container |
|||
class="chat-setting" |
|||
v-if="store.currentNavId == 5" |
|||
> |
|||
<ChatUserSetting /> |
|||
</el-container> |
|||
</el-container> |
|||
</template> |
|||
<style scoped> |
|||
.container { |
|||
display: flex; |
|||
height: 100%; |
|||
width: 100%; |
|||
overflow-y: hidden; |
|||
overflow-x: hidden; |
|||
} |
|||
.container { |
|||
display: flex; |
|||
height: 100%; |
|||
width: 100%; |
|||
overflow-y: hidden; |
|||
overflow-x: hidden; |
|||
} |
|||
|
|||
.menu { |
|||
width: 55px; |
|||
background-color: #2E2E2E; |
|||
overflow-y: hidden; |
|||
overflow-x: hidden; |
|||
-webkit-app-region: drag; |
|||
} |
|||
.menu { |
|||
width: 55px; |
|||
|
|||
.side { |
|||
flex: 1; |
|||
/* 占据剩余宽度 */ |
|||
max-height: max-content; |
|||
border-right: 1px solid #edebeb; |
|||
overflow-y: hidden; |
|||
overflow-x: hidden; |
|||
background-color: #F7F7F7; |
|||
} |
|||
background-color: white; |
|||
overflow-y: hidden; |
|||
overflow-x: hidden; |
|||
-webkit-app-region: drag; |
|||
} |
|||
|
|||
.search { |
|||
width: 100%; |
|||
/* 占据整个宽度 */ |
|||
.side { |
|||
flex: 1; |
|||
/* 占据剩余宽度 */ |
|||
max-height: max-content; |
|||
border-right: 1px solid #edebeb; |
|||
overflow-y: hidden; |
|||
overflow-x: hidden; |
|||
background-color: #f7f7f7; |
|||
} |
|||
|
|||
height: 50px; |
|||
padding: 0; |
|||
-webkit-app-region: drag; |
|||
} |
|||
.search { |
|||
width: 100%; |
|||
/* 占据整个宽度 */ |
|||
height: 50px; |
|||
padding: 0; |
|||
-webkit-app-region: drag; |
|||
} |
|||
|
|||
.search-input { |
|||
width: calc(100% - 20px); |
|||
/* 减去左右边距 */ |
|||
margin: 10px; |
|||
-webkit-app-region: no-drag; |
|||
--el-input-placeholder-color: #818181 !important; |
|||
--el-input-icon-color: #5D5D5D !important; |
|||
} |
|||
.search-input { |
|||
width: calc(100% - 20px); |
|||
/* 减去左右边距 */ |
|||
margin: 10px; |
|||
-webkit-app-region: no-drag; |
|||
--el-input-placeholder-color: #818181 !important; |
|||
--el-input-icon-color: #5d5d5d !important; |
|||
} |
|||
|
|||
.list { |
|||
width: 100%; |
|||
/* 占据整个宽度 */ |
|||
padding: 0; |
|||
overflow-y: hidden; |
|||
overflow-x: hidden; |
|||
} |
|||
.list { |
|||
width: 100%; |
|||
/* 占据整个宽度 */ |
|||
padding: 0; |
|||
overflow-y: hidden; |
|||
overflow-x: hidden; |
|||
} |
|||
|
|||
.chat-box { |
|||
flex: 3; |
|||
/* 占据剩余宽度的三倍 */ |
|||
max-height: max-content; |
|||
background-color: #F5F5F5; |
|||
} |
|||
.chat-box { |
|||
flex: 3; |
|||
/* 占据剩余宽度的三倍 */ |
|||
max-height: max-content; |
|||
background-color: #f5f5f5; |
|||
} |
|||
|
|||
.chat-setting { |
|||
width: calc(100% - 65px); |
|||
/* 占据整个宽度 */ |
|||
height: 100%; |
|||
overflow: hidden; |
|||
} |
|||
.workflow { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: contain; |
|||
border: none; |
|||
.chat-setting { |
|||
width: calc(100% - 65px); |
|||
/* 占据整个宽度 */ |
|||
height: 100%; |
|||
overflow: hidden; |
|||
} |
|||
.workflow { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: contain; |
|||
border: none; |
|||
} |
|||
|
|||
.no-message-container { |
|||
height: 100%; |
|||
margin: 120px auto; |
|||
text-align: center; |
|||
font-size:14px; |
|||
justify-content: center; |
|||
} |
|||
</style> |
|||
</style> |
|||
|
@ -1,173 +1,204 @@ |
|||
<script setup lang="ts"> |
|||
import { useChatStore } from '@/stores/chat'; |
|||
const store = useChatStore() |
|||
import { useChatStore } from "@/stores/chat"; |
|||
const store = useChatStore(); |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="chatbox-main" v-if="store.targetUserId > 0"> |
|||
<!--聊天顶部区--> |
|||
<el-header class="chat-header"> |
|||
<div class="header-title">{{ store.targetUserInfo.username }}</div> |
|||
</el-header> |
|||
<!--聊天主体区--> |
|||
<el-main class="msg-main"> |
|||
<el-scrollbar ref="store.scrollbarRef"> |
|||
<div ref="store.innerRef"> |
|||
<ChatMessage /> |
|||
</div> |
|||
</el-scrollbar> |
|||
</el-main> |
|||
<el-footer class="msg-footer"> |
|||
<!--聊天输入选项--> |
|||
<div class="input-option"> |
|||
<el-icon :size="20" class="input-option-icon"> |
|||
<Picture /> |
|||
</el-icon> |
|||
<el-icon :size="20" class="input-option-icon"> |
|||
<Link /> |
|||
</el-icon> |
|||
<el-icon :size="20" class="input-option-icon"> |
|||
<Delete /> |
|||
</el-icon> |
|||
</div> |
|||
<!--聊天输入区--> |
|||
<div class="input-textarea"> |
|||
<el-input type="textarea" maxlength="1000" resize="none" class="textarea" |
|||
@keyup.enter.exact="store.sendMessage" v-model="store.message" /> |
|||
</div> |
|||
<!--聊天发送按钮--> |
|||
<el-tooltip placement="top" content="按enter键发送,按ctrl+enter键换行"> |
|||
<el-icon :size="22" class="input-button"> |
|||
<Promotion /> |
|||
</el-icon> |
|||
</el-tooltip> |
|||
</el-footer> |
|||
</div> |
|||
<div class="no-message-container" v-else> |
|||
<el-icon :size="180" color="#0078d7"> |
|||
<ScaleToOriginal /> |
|||
</el-icon> |
|||
<p>暂无内容</p> |
|||
</div> |
|||
<div |
|||
class="chatbox-main" |
|||
v-if="store.targetUserId > 0" |
|||
> |
|||
<!--聊天顶部区--> |
|||
<el-header class="chat-header"> |
|||
<div class="header-title">{{ store.targetUserInfo.username }}</div> |
|||
</el-header> |
|||
<!--聊天主体区--> |
|||
<el-main class="msg-main"> |
|||
<el-scrollbar ref="store.scrollbarRef"> |
|||
<div ref="store.innerRef"> |
|||
<ChatMessage /> |
|||
</div> |
|||
</el-scrollbar> |
|||
</el-main> |
|||
<el-footer class="msg-footer"> |
|||
<!--聊天输入选项--> |
|||
<div class="input-option"> |
|||
<el-icon |
|||
:size="20" |
|||
class="input-option-icon" |
|||
> |
|||
<Picture /> |
|||
</el-icon> |
|||
<el-icon |
|||
:size="20" |
|||
class="input-option-icon" |
|||
> |
|||
<Link /> |
|||
</el-icon> |
|||
<el-icon |
|||
:size="20" |
|||
class="input-option-icon" |
|||
> |
|||
<Delete /> |
|||
</el-icon> |
|||
</div> |
|||
<!--聊天输入区--> |
|||
<div class="input-textarea"> |
|||
<el-input |
|||
type="textarea" |
|||
maxlength="1000" |
|||
resize="none" |
|||
class="textarea" |
|||
@keyup.enter.exact="store.sendMessage" |
|||
v-model="store.message" |
|||
/> |
|||
</div> |
|||
<!--聊天发送按钮--> |
|||
<el-tooltip |
|||
placement="top" |
|||
content="按enter键发送,按ctrl+enter键换行" |
|||
> |
|||
<el-icon |
|||
@click="store.sendMessage" |
|||
:size="22" |
|||
class="input-button" |
|||
> |
|||
<Promotion /> |
|||
</el-icon> |
|||
</el-tooltip> |
|||
</el-footer> |
|||
</div> |
|||
<div |
|||
class="no-message-container" |
|||
v-else |
|||
> |
|||
<el-icon |
|||
:size="180" |
|||
color="#0078d7" |
|||
> |
|||
<ChatDotSquare /> |
|||
</el-icon> |
|||
<p>欢迎使用GodoOS</p> |
|||
</div> |
|||
</template> |
|||
<style scoped> |
|||
.chatbox-main { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
.chatbox-main { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.chat-header { |
|||
width: 100%; |
|||
/* 占据整个宽度 */ |
|||
height: 50px; |
|||
line-height: 50px; |
|||
padding: 0; |
|||
-webkit-app-region: drag; |
|||
} |
|||
.chat-header { |
|||
width: 100%; |
|||
/* 占据整个宽度 */ |
|||
height: 50px; |
|||
line-height: 50px; |
|||
padding: 0; |
|||
-webkit-app-region: drag; |
|||
} |
|||
|
|||
header-title { |
|||
font-size: 20px; |
|||
text-align: left; |
|||
margin-left: 15px; |
|||
} |
|||
header-title { |
|||
font-size: 20px; |
|||
text-align: left; |
|||
margin-left: 15px; |
|||
} |
|||
|
|||
.msg-main { |
|||
width: 100%; |
|||
/* 占据整个宽度 */ |
|||
height: calc(70% - 50px); |
|||
padding: 0; |
|||
border-top-width: 1px; |
|||
border-bottom-width: 1px; |
|||
border-left-width: 0; |
|||
border-right-width: 0; |
|||
border-color: #D6D6D6; |
|||
border-style: solid; |
|||
} |
|||
.msg-main { |
|||
width: 100%; |
|||
/* 占据整个宽度 */ |
|||
height: calc(70% - 50px); |
|||
padding: 0; |
|||
border-top-width: 1px; |
|||
border-bottom-width: 1px; |
|||
border-left-width: 0; |
|||
border-right-width: 0; |
|||
border-color: #d6d6d6; |
|||
border-style: solid; |
|||
} |
|||
|
|||
.msg-footer { |
|||
width: 100%; |
|||
/* 占据整个宽度 */ |
|||
height: 30%; |
|||
padding: 0; |
|||
} |
|||
.msg-footer { |
|||
width: 100%; |
|||
/* 占据整个宽度 */ |
|||
height: 30%; |
|||
padding: 0; |
|||
} |
|||
|
|||
.input-option { |
|||
height: 20px; |
|||
padding: 5px; |
|||
} |
|||
.input-option { |
|||
height: 20px; |
|||
padding: 5px; |
|||
} |
|||
|
|||
.input-option-icon { |
|||
color: #494949; |
|||
margin-left: 20px; |
|||
margin-top: 5px; |
|||
cursor: pointer; |
|||
} |
|||
.input-option-icon { |
|||
color: #494949; |
|||
margin-left: 20px; |
|||
margin-top: 5px; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.input-textarea { |
|||
height: calc(100% - 50px); |
|||
width: calc(100% - 20px); |
|||
/* 减去左右边距 */ |
|||
margin: 10px; |
|||
} |
|||
.input-textarea { |
|||
height: calc(100% - 50px); |
|||
width: calc(100% - 20px); |
|||
/* 减去左右边距 */ |
|||
margin: 10px; |
|||
} |
|||
|
|||
.textarea { |
|||
font-size: 16px; |
|||
font-family: Arial, sans-serif; |
|||
line-height: 1.5; |
|||
width: 100%; |
|||
height: 100%; |
|||
overflow-y: hidden; |
|||
overflow-x: hidden; |
|||
--el-input-border-radius: 0; |
|||
--el-input-border-color: transparent; |
|||
--el-input-hover-border-color: transparent; |
|||
--el-input-clear-hover-color: transparent; |
|||
--el-input-focus-border-color: transparent; |
|||
} |
|||
.textarea { |
|||
font-size: 16px; |
|||
font-family: Arial, sans-serif; |
|||
line-height: 1.5; |
|||
width: 100%; |
|||
height: 100%; |
|||
overflow-y: hidden; |
|||
overflow-x: hidden; |
|||
--el-input-border-radius: 0; |
|||
--el-input-border-color: transparent; |
|||
--el-input-hover-border-color: transparent; |
|||
--el-input-clear-hover-color: transparent; |
|||
--el-input-focus-border-color: transparent; |
|||
} |
|||
|
|||
.input-button { |
|||
position: absolute; |
|||
bottom: 12px; |
|||
right: 12px; |
|||
width: 30px; |
|||
/* 缩小宽度 */ |
|||
height: 30px; |
|||
/* 减小高度 */ |
|||
border-radius: 50%; |
|||
/* 较小的圆角 */ |
|||
background-color: #E8F0FE; |
|||
/* 浅蓝色,符合Win11的轻量风格 */ |
|||
color: #0078D4; |
|||
/* 使用Win11的强调色作为文字颜色 */ |
|||
font-weight: bold; |
|||
border: 1px solid #B3D4FC; |
|||
/* 添加边框,保持简洁风格 */ |
|||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); |
|||
/* 轻微阴影 */ |
|||
transition: all 0.2s ease; |
|||
/* 快速过渡效果 */ |
|||
} |
|||
.input-button { |
|||
position: absolute; |
|||
bottom: 12px; |
|||
right: 12px; |
|||
width: 30px; |
|||
/* 缩小宽度 */ |
|||
height: 30px; |
|||
/* 减小高度 */ |
|||
border-radius: 50%; |
|||
/* 较小的圆角 */ |
|||
background-color: #e8f0fe; |
|||
/* 浅蓝色,符合Win11的轻量风格 */ |
|||
color: #0078d4; |
|||
/* 使用Win11的强调色作为文字颜色 */ |
|||
font-weight: bold; |
|||
border: 1px solid #b3d4fc; |
|||
/* 添加边框,保持简洁风格 */ |
|||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); |
|||
/* 轻微阴影 */ |
|||
transition: all 0.2s ease; |
|||
/* 快速过渡效果 */ |
|||
} |
|||
|
|||
.input-button:hover { |
|||
background-color: #D1E4FF; |
|||
/* 悬浮时颜色略深,保持浅色调 */ |
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
|||
/* 稍微增强阴影 */ |
|||
} |
|||
.input-button:hover { |
|||
background-color: #d1e4ff; |
|||
/* 悬浮时颜色略深,保持浅色调 */ |
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
|||
/* 稍微增强阴影 */ |
|||
} |
|||
|
|||
.input-button:active { |
|||
background-color: #B3D4FC; |
|||
/* 按下时颜色更深,但依然保持清新 */ |
|||
box-shadow: 0 1px 2px rgba(0, 0, 0.1); |
|||
/* 回复初始阴影 */ |
|||
transform: translateY(1px); |
|||
/* 微小下移,模拟按下 */ |
|||
} |
|||
.input-button:active { |
|||
background-color: #b3d4fc; |
|||
/* 按下时颜色更深,但依然保持清新 */ |
|||
box-shadow: 0 1px 2px rgba(0, 0, 0.1); |
|||
/* 回复初始阴影 */ |
|||
transform: translateY(1px); |
|||
/* 微小下移,模拟按下 */ |
|||
} |
|||
|
|||
.no-message-container { |
|||
height: 100%; |
|||
margin: 120px auto; |
|||
text-align: center; |
|||
justify-content: center; |
|||
} |
|||
</style> |
|||
.no-message-container { |
|||
height: 100%; |
|||
margin: 120px auto; |
|||
text-align: center; |
|||
justify-content: center; |
|||
} |
|||
</style> |
|||
|
@ -1,71 +1,158 @@ |
|||
<script setup> |
|||
import { ref } from "vue"; |
|||
import { useChatStore } from "@/stores/chat"; |
|||
const store = useChatStore() |
|||
|
|||
</script> |
|||
|
|||
<template> |
|||
<div v-for="item in store.chatList"> |
|||
<div class="list-item" @click="store.changeChatList(item)" |
|||
:style="{ backgroundColor: item.id === store.targetUserId ? '#C4C4C4' : '' }"> |
|||
<div v-if="store.chatList.length > 0" v-for="item in store.chatList" :key="item.id"> |
|||
<div |
|||
class="list-item" |
|||
@click="store.changeChatList(item.id)" |
|||
:style="{ |
|||
backgroundColor: item.id === store.targetUserId ? '#C4C4C4' : '', |
|||
}" |
|||
> |
|||
<el-row> |
|||
<el-col :span="6"> |
|||
<el-avatar shape="square" :size="40" class="avatar" :src="item.avatar" /> |
|||
<el-avatar |
|||
shape="square" |
|||
:size="40" |
|||
class="avatar" |
|||
:src="item.avatar" |
|||
/> |
|||
</el-col> |
|||
<el-col :span="18"> |
|||
<el-row> |
|||
<el-col :span="18"> |
|||
<div class="previewName">{{ item.name }}</div> |
|||
<el-col :span="18" class="preview"> |
|||
<el-row class="preview-content"> |
|||
<el-col :span="18" class="preview-left"> |
|||
<div class="previewName">{{ item.nickname }}</div> |
|||
<div class="previewChat"> |
|||
<span v-if="item.previewType === 0">{{ item.previewMessage }}</span> |
|||
<span v-if="item.previewType === 1"> |
|||
{{ |
|||
item.targetUserId === id |
|||
? "你" |
|||
: '"' + item.nickname + '"' |
|||
}}撤回了一条消息 |
|||
</span> |
|||
</div> |
|||
</el-col> |
|||
<el-col :span="6"> |
|||
<div class="previewTime">{{ item.previewTimeFormat }}</div> |
|||
<el-col :span="6" class="preview-right"> |
|||
<div class="previewTime"> |
|||
{{ item.previewTimeFormat }} |
|||
</div> |
|||
</el-col> |
|||
</el-row> |
|||
<el-row> |
|||
<div v-if="item.previewType === 0" class="previewChat">{{ item.previewMessage }}</div> |
|||
<div v-if="item.previewType === 1" class="previewChat">{{ item.userId === id ? "你" : "\"" + item.name + |
|||
"\""}}撤回了一条消息</div> |
|||
</el-row> |
|||
</el-col> |
|||
</el-row> |
|||
</div> |
|||
</div> |
|||
<div v-else class="emptyChat"> |
|||
<el-icon :size="60" class="chat-icon"> |
|||
<ChatSquare /> |
|||
</el-icon> |
|||
<p class="empty-message">暂无数据</p> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { useChatStore } from "@/stores/chat"; |
|||
import { ref } from "vue"; |
|||
|
|||
const store = useChatStore(); |
|||
const id = ref("1"); |
|||
|
|||
</script> |
|||
|
|||
<style scoped> |
|||
.list-item { |
|||
width: 100%; |
|||
height: 60px; |
|||
} |
|||
|
|||
.list-item:hover { |
|||
background-color: #D0D0D0; |
|||
} |
|||
|
|||
.avatar { |
|||
margin: 10px |
|||
} |
|||
|
|||
.previewName { |
|||
margin-top: 10px; |
|||
font-size: 15px; |
|||
font-family: Arial, sans-serif; |
|||
line-height: 1.5; |
|||
} |
|||
|
|||
.previewChat { |
|||
font-size: 14px; |
|||
font-family: Arial, sans-serif; |
|||
color: #999999; |
|||
} |
|||
|
|||
.previewTime { |
|||
float: right; |
|||
margin-top: 10px; |
|||
margin-right: 10px; |
|||
font-size: 12px; |
|||
font-family: Arial, sans-serif; |
|||
color: #999999; |
|||
} |
|||
.list-item { |
|||
width: 100%; |
|||
height: 60px; |
|||
display: flex; |
|||
} |
|||
|
|||
.list-item:hover { |
|||
background-color: #d0d0d0; |
|||
} |
|||
|
|||
.avatar { |
|||
margin: 10px; |
|||
} |
|||
|
|||
.preview { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
height: 100%; |
|||
} |
|||
|
|||
.preview-content { |
|||
height: 100%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
width: 100%; |
|||
} |
|||
|
|||
.preview-left { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
height: 100%; |
|||
} |
|||
|
|||
.previewName { |
|||
margin-left: 10px; |
|||
font-size: 12px; |
|||
font-family: Arial, sans-serif; |
|||
line-height: 1.5; |
|||
overflow: hidden; /* 隐藏超出部分 */ |
|||
text-overflow: ellipsis; /* 显示为省略号 */ |
|||
white-space: nowrap; /* 不换行 */ |
|||
min-width: 100px; /* 最小宽度 */ |
|||
max-width: 100%; /* 最大宽度 */ |
|||
} |
|||
|
|||
.previewChat { |
|||
height: 20px; |
|||
margin-left: 10px; |
|||
font-size: 10px; |
|||
font-family: Arial, sans-serif; |
|||
color: #999999; |
|||
overflow: hidden; /* 隐藏超出部分 */ |
|||
text-overflow: ellipsis; /* 显示为省略号 */ |
|||
white-space: nowrap; /* 不换行 */ |
|||
min-width: 90px; /* 最小宽度 */ |
|||
max-width: 100%; /* 最大宽度 */ |
|||
} |
|||
|
|||
.preview-right { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
height: 100%; |
|||
} |
|||
|
|||
.previewTime { |
|||
font-size: 12px; |
|||
font-family: Arial, sans-serif; |
|||
color: #999999; |
|||
min-width: 30px; /* 最小宽度 */ |
|||
max-width: 100%; /* 最大宽度 */ |
|||
} |
|||
|
|||
.emptyChat { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
height: 100%; |
|||
margin: 120px auto; |
|||
text-align: center; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.chat-icon { |
|||
color: #0078d4; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.empty-message { |
|||
font-size: 16px; |
|||
color: #666666; |
|||
} |
|||
</style> |
|||
|
@ -1,71 +1,221 @@ |
|||
<script setup> |
|||
import { ref } from "vue"; |
|||
import { useChatStore } from "@/stores/chat"; |
|||
const store = useChatStore() |
|||
|
|||
import { useChatStore } from "@/stores/chat"; |
|||
import { |
|||
ElAvatar, |
|||
ElCol, |
|||
ElCollapse, |
|||
ElCollapseItem, |
|||
ElRow, |
|||
} from "element-plus"; |
|||
const store = useChatStore(); |
|||
</script> |
|||
|
|||
<template> |
|||
<div v-for="item in store.chatList"> |
|||
<div class="list-item" @click="store.changeChatList(item)" |
|||
:style="{ backgroundColor: item.id === store.targetUserId ? '#C4C4C4' : '' }"> |
|||
<el-row> |
|||
<el-col :span="6"> |
|||
<el-avatar shape="square" :size="40" class="avatar" :src="item.avatar" /> |
|||
</el-col> |
|||
<el-col :span="18"> |
|||
<el-row> |
|||
<el-col :span="18"> |
|||
<div class="previewName">{{ item.name }}</div> |
|||
</el-col> |
|||
<el-col :span="6"> |
|||
<div class="previewTime">{{ item.previewTimeFormat }}</div> |
|||
</el-col> |
|||
</el-row> |
|||
<el-row> |
|||
<div v-if="item.previewType === 0" class="previewChat">{{ item.previewMessage }}</div> |
|||
<div v-if="item.previewType === 1" class="previewChat">{{ item.userId === id ? "你" : "\"" + item.name + |
|||
"\""}}撤回了一条消息</div> |
|||
</el-row> |
|||
</el-col> |
|||
</el-row> |
|||
</div> |
|||
</div> |
|||
<el-collapse |
|||
v-model="store.activeNames" |
|||
@change="handleChange" |
|||
> |
|||
<el-collapse-item name="1"> |
|||
<template #title> |
|||
<span v-if="store.userList.length > 0" class="title">同事({{store.userList.length}})</span> |
|||
<span v-else class="title">同事</span> |
|||
</template> |
|||
<div v-if="store.userList.length > 0"> |
|||
<div |
|||
v-for="item in store.userList" |
|||
:key="item.id" |
|||
> |
|||
<div |
|||
class="list-item" |
|||
@click="store.changeChatList(item.id)" |
|||
:style="{ |
|||
backgroundColor: |
|||
item.id === store.targetUserId ? '#C4C4C4' : '', |
|||
}" |
|||
> |
|||
<el-row> |
|||
<el-col :span="6"> |
|||
<el-avatar |
|||
shape="square" |
|||
:size="40" |
|||
class="avatar" |
|||
:src="item.avatar" |
|||
/> |
|||
</el-col> |
|||
<el-col :span="18"> |
|||
<el-row> |
|||
<el-col :span="18"> |
|||
<div class="previewName"> |
|||
{{ item.nickname }} |
|||
<el-icon class="online-icon" v-if="item.isOnline"> |
|||
<CircleCheckFilled /> |
|||
</el-icon> |
|||
</div> |
|||
</el-col> |
|||
<el-col :span="6"> |
|||
<!-- 空白占位 --> |
|||
</el-col> |
|||
</el-row> |
|||
<el-row> |
|||
<div class="previewIP"> |
|||
{{ item.ip }} |
|||
</div> |
|||
</el-row> |
|||
</el-col> |
|||
</el-row> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div v-else> |
|||
<p class="no-data">暂无数据</p> |
|||
</div> |
|||
</el-collapse-item> |
|||
<el-collapse-item name="2"> |
|||
<template #title> |
|||
<span v-if="store.groupList.length > 0" class="title">部门({{store.groupList.length}})</span> |
|||
<span v-else class="title">部门</span> |
|||
</template> |
|||
<div v-if="store.groupList.length > 0"> |
|||
<div |
|||
v-for="group in store.groupList" |
|||
:key="group.id" |
|||
> |
|||
<div |
|||
class="list-item" |
|||
@click="store.changeGroupList(group)" |
|||
:style="{ |
|||
backgroundColor: |
|||
group.id === store.targetGroupId |
|||
? '#C4C4C4' |
|||
: '', |
|||
}" |
|||
> |
|||
<el-row> |
|||
<el-col :span="6"> |
|||
<el-avatar |
|||
shape="square" |
|||
:size="40" |
|||
class="avatar" |
|||
:src="group.avatar" |
|||
/> |
|||
</el-col> |
|||
<el-col :span="18"> |
|||
<el-row> |
|||
<el-col :span="18"> |
|||
<div class="previewName"> |
|||
{{ group.name }} |
|||
</div> |
|||
</el-col> |
|||
<el-col :span="6"> |
|||
<div class="previewTime"> |
|||
{{ group.previewTimeFormat }} |
|||
</div> |
|||
</el-col> |
|||
</el-row> |
|||
<el-row> |
|||
<div |
|||
v-if="group.previewType === 0" |
|||
class="previewChat" |
|||
> |
|||
{{ group.previewMessage }} |
|||
</div> |
|||
<div |
|||
v-if="group.previewType === 1" |
|||
class="previewChat" |
|||
> |
|||
{{ |
|||
group.userId === id |
|||
? "你" |
|||
: '"' + group.name + '"' |
|||
}}撤回了一条消息 |
|||
</div> |
|||
</el-row> |
|||
</el-col> |
|||
</el-row> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div v-else> |
|||
<p class="no-data">暂无数据</p> |
|||
</div> |
|||
</el-collapse-item> |
|||
</el-collapse> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.list-item { |
|||
width: 240px; |
|||
height: 60px; |
|||
} |
|||
.title { |
|||
padding-left: 10px; |
|||
} |
|||
.list-item { |
|||
width: 100%; |
|||
height: 60px; |
|||
border: 0.5px solid #d0d0d0; |
|||
} |
|||
|
|||
.list-item:hover { |
|||
background-color: #d0d0d0; |
|||
} |
|||
|
|||
.avatar { |
|||
margin: 10px; |
|||
} |
|||
|
|||
.previewName { |
|||
margin-left: 10px; |
|||
font-size: 14px; |
|||
font-family: Arial, sans-serif; |
|||
line-height: 1.5; |
|||
overflow: hidden; /* 隐藏超出部分 */ |
|||
text-overflow: ellipsis; /* 显示为省略号 */ |
|||
white-space: nowrap; /* 不换行 */ |
|||
max-width: 100%; /* 最大宽度 */ |
|||
display: flex; /* 使用 Flexbox */ |
|||
align-items: center; /* 垂直居中 */ |
|||
} |
|||
|
|||
/* 为了使内部的 el-row 垂直居中,我们也需要设置其父级元素 */ |
|||
.el-row { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.list-item:hover { |
|||
background-color: #D0D0D0; |
|||
} |
|||
.previewIP { |
|||
margin-left: 10px; |
|||
font-size: 12px; |
|||
font-family: Arial, sans-serif; |
|||
color: #999999; |
|||
display: flex; /* 使用 Flexbox */ |
|||
align-items: center; /* 垂直居中 */ |
|||
} |
|||
|
|||
.avatar { |
|||
margin: 10px |
|||
} |
|||
.online-icon { |
|||
font-size: 16px; /* 调整图标大小 */ |
|||
color: green; /* 在线状态颜色 */ |
|||
margin-left: 5px; /* 与用户名之间的间距 */ |
|||
} |
|||
|
|||
.previewName { |
|||
margin-top: 10px; |
|||
font-size: 15px; |
|||
font-family: Arial, sans-serif; |
|||
line-height: 1.5; |
|||
} |
|||
.previewChat { |
|||
margin-left: 10px; |
|||
font-size: 12px; /* 调整字体大小 */ |
|||
font-family: Arial, sans-serif; |
|||
color: #999999; |
|||
overflow: hidden; /* 隐藏超出部分 */ |
|||
text-overflow: ellipsis; /* 显示为省略号 */ |
|||
white-space: nowrap; /* 不换行 */ |
|||
max-width: 100%; /* 最大宽度 */ |
|||
} |
|||
|
|||
.previewChat { |
|||
font-size: 14px; |
|||
font-family: Arial, sans-serif; |
|||
color: #999999; |
|||
} |
|||
.previewTime { |
|||
float: right; |
|||
margin-top: 10px; |
|||
margin-right: 10px; |
|||
font-size: 12px; |
|||
font-family: Arial, sans-serif; |
|||
color: #999999; |
|||
} |
|||
|
|||
.previewTime { |
|||
float: right; |
|||
margin-top: 10px; |
|||
margin-right: 10px; |
|||
font-size: 12px; |
|||
font-family: Arial, sans-serif; |
|||
color: #999999; |
|||
} |
|||
</style> |
|||
.no-data { |
|||
text-align: center; |
|||
color: #999999; |
|||
} |
|||
</style> |
@ -0,0 +1,79 @@ |
|||
<template> |
|||
<div |
|||
v-if="store.targetUserId == null" |
|||
class="no-message-container" |
|||
> |
|||
<el-icon |
|||
:size="180" |
|||
color="#0078d7" |
|||
> |
|||
<ChatDotSquare /> |
|||
</el-icon> |
|||
<p>欢迎使用GodoOS</p> |
|||
</div> |
|||
<div |
|||
v-else |
|||
class="user-info-container" |
|||
> |
|||
<div class="user-details"> |
|||
<p>昵称: {{ store.targetUserInfo.nickname }}</p> |
|||
<p>邮箱: {{ store.targetUserInfo.email }}</p> |
|||
<p>电话: {{ store.targetUserInfo.phone }}</p> |
|||
<p>描述: {{ store.targetUserInfo.desc }}</p> |
|||
<p>工号: {{ store.targetUserInfo.jobNumber }}</p> |
|||
<p>工作地点: {{ store.targetUserInfo.workPlace }}</p> |
|||
<p>入职日期: {{ store.targetUserInfo.hiredDate }}</p> |
|||
</div> |
|||
<el-button |
|||
type="primary" |
|||
@click="sendMessage(store.targetUserId)" |
|||
>发送消息</el-button |
|||
> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { useChatStore } from "@/stores/chat"; |
|||
|
|||
const store = useChatStore(); |
|||
|
|||
const sendMessage = (id:number) => { |
|||
store.currentNavId = 0; |
|||
store.updateConversationList(id) |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.no-message-container { |
|||
height: 100%; |
|||
margin: 0px auto; |
|||
text-align: center; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.user-info-container { |
|||
height: 100%; |
|||
margin: 0px auto; |
|||
text-align: center; |
|||
font-size: 14px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.user-details { |
|||
margin-bottom: 20px; |
|||
text-align: left; |
|||
max-width: 300px; |
|||
} |
|||
|
|||
.user-details p { |
|||
margin: 5px 0; |
|||
} |
|||
|
|||
.el-button { |
|||
width: 100%; |
|||
max-width: 300px; |
|||
} |
|||
</style> |
@ -1,86 +1,367 @@ |
|||
import { defineStore } from 'pinia' |
|||
import emojiList from "@/assets/emoji.json" |
|||
import { getSystemConfig } from '@/system/config' |
|||
import emojiList from "@/assets/emoji.json"; |
|||
import { fetchGet, fetchPost, getSystemConfig } from '@/system/config'; |
|||
import { notifyError } from "@/util/msg"; |
|||
import { defineStore } from 'pinia'; |
|||
import { db } from "./db"; |
|||
import { useMessageStore } from "./message"; |
|||
|
|||
export const useChatStore = defineStore('chatStore', () => { |
|||
const userList: any = ref([]) // 用户列表
|
|||
const chatList: any = ref([]) // 消息列表
|
|||
const msgList: any = ref([]) //聊天消息列表
|
|||
const userInfo: any = ref({}) |
|||
const showChooseFile = ref(false) |
|||
const currentPage = ref(1) |
|||
const pageSize = ref(50) |
|||
const scrollbarRef = ref() |
|||
const innerRef = ref() |
|||
const config = getSystemConfig() |
|||
const currentNavId = ref(0) |
|||
const message: any = ref() |
|||
const targetUserInfo:any = ref({}) |
|||
const targetUserId = ref(0) |
|||
const search = ref('') |
|||
const contextMenu = ref({ |
|||
visible: false, |
|||
chatMessageId: 0, |
|||
list: [ |
|||
{ |
|||
id: 2, |
|||
label: '撤回', |
|||
} |
|||
], |
|||
x: 0, |
|||
y: 0 |
|||
}) |
|||
const initChat = () => { |
|||
if(config.userInfo.avatar == ''){ |
|||
config.userInfo.avatar = '/logo.png' |
|||
} |
|||
userInfo.value = config.userInfo |
|||
// 用户列表
|
|||
const userList: any = ref([]); |
|||
|
|||
// 定义聊天列表项的接口
|
|||
interface ChatListItem { |
|||
id: number; |
|||
nickname: string; |
|||
avatar: string; |
|||
previewTimeFormat: string; |
|||
previewType: 0 | 1; // 消息类型,0表示正常消息,1表示撤回消息
|
|||
previewMessage: string; |
|||
} |
|||
|
|||
// 模拟数据 - 聊天列表
|
|||
const chatList = ref<ChatListItem[]>([ |
|||
{ |
|||
id: 2, |
|||
nickname: '朋友2', |
|||
avatar: '/logo.png', |
|||
previewTimeFormat: "昨天", |
|||
previewType: 1, |
|||
previewMessage: "测试消息", |
|||
}, |
|||
]); |
|||
|
|||
// 模拟数据 - 聊天消息列表
|
|||
const chatHistory = ref([]); |
|||
|
|||
// 群组数据
|
|||
const groupList = ref([ |
|||
{ |
|||
id: 1, |
|||
name: '群组1', |
|||
avatar: '/logo.png', |
|||
previewTimeFormat: "今天", |
|||
previewType: 0, |
|||
previewMessage: "这是一个示例消息。", |
|||
}, |
|||
{ |
|||
id: 2, |
|||
name: '群组2', |
|||
avatar: '/logo.png', |
|||
previewTimeFormat: "今天", |
|||
previewType: 0, |
|||
previewMessage: "这是一个示例消息。", |
|||
}, |
|||
{ |
|||
id: 3, |
|||
name: '群组3', |
|||
avatar: '/logo.png', |
|||
previewTimeFormat: "今天", |
|||
previewType: 0, |
|||
previewMessage: "这是一个示例消息。", |
|||
} |
|||
const setCurrentNavId = (id: number) => { |
|||
currentNavId.value = id |
|||
]); |
|||
|
|||
const activeNames = ref([]); |
|||
const userInfo: any = ref({}); |
|||
const showChooseFile = ref(false); |
|||
const currentPage = ref(1); |
|||
const pageSize = ref(50); |
|||
const innerRef = ref(null); |
|||
const scrollbarRef = ref(null); |
|||
const config = getSystemConfig(); |
|||
const currentNavId = ref(0); |
|||
const message: any = ref(''); |
|||
const targetUserInfo: any = ref({}); |
|||
const targetUserId = ref(); |
|||
const search = ref(''); |
|||
const messageStore = useMessageStore(); |
|||
const apiUrl = "http://192.168.1.10:8816"; |
|||
|
|||
const contextMenu = ref({ |
|||
visible: false, |
|||
chatMessageId: 0, |
|||
list: [ |
|||
{ |
|||
id: 2, |
|||
label: '撤回', |
|||
} |
|||
], |
|||
x: 0, |
|||
y: 0 |
|||
}); |
|||
|
|||
const initChat = () => { |
|||
if (config.userInfo.avatar == '') { |
|||
config.userInfo.avatar = '/logo.png'; |
|||
} |
|||
const sendMessage = async () => { |
|||
await setScrollToBottom() |
|||
userInfo.value = config.userInfo; |
|||
}; |
|||
|
|||
const setCurrentNavId = (id: number) => { |
|||
currentNavId.value = id; |
|||
}; |
|||
|
|||
const sendMessage = async () => { |
|||
const chatSendUrl = apiUrl + '/chat/send'; |
|||
const messageHistory = { |
|||
type: 'text', |
|||
createdAt: Date.now(), |
|||
content: message.value, |
|||
targetUserId: targetUserId.value, |
|||
previewType: 0, // 消息类型,0表示正常消息,1表示撤回消息
|
|||
previewMessage: message.value, |
|||
isMe: true, |
|||
isRead: false, |
|||
userInfo: { |
|||
id: config.userInfo.id, |
|||
username: config.userInfo.username, |
|||
avatar: config.userInfo.avatar, |
|||
}, |
|||
}; |
|||
|
|||
const res = await fetchPost(chatSendUrl, messageHistory); |
|||
|
|||
if (res.ok) { |
|||
// 本地存储一份聊天记录
|
|||
await db.addOne('chatRecord', messageHistory); |
|||
|
|||
// 更新聊天历史
|
|||
chatHistory.value.push(messageHistory); |
|||
|
|||
// 更新 chatList 和 conversationList
|
|||
await updateConversationList(targetUserId.value); |
|||
|
|||
// 清空输入框
|
|||
clearMessage(); |
|||
|
|||
// 聊天框滚动到底部
|
|||
await setScrollToBottom(); |
|||
return; |
|||
} |
|||
const setScrollToBottom = async () => { |
|||
await nextTick() |
|||
const max = innerRef.value.clientHeight |
|||
scrollbarRef.value.setScrollTop(max) |
|||
notifyError("消息发送失败"); |
|||
}; |
|||
|
|||
const updateConversationList = async (id: number) => { |
|||
// 先判断是否已经存在该会话
|
|||
const res = await db.getRow('conversationList', 'id', id); |
|||
|
|||
if (res) { |
|||
// 更新现有会话
|
|||
const updatedConversation = { |
|||
...res, |
|||
previewMessage: message.value, |
|||
previewTimeFormat: formatTime(Date.now()), |
|||
previewType: 0, |
|||
}; |
|||
await db.update('conversationList', id, updatedConversation); |
|||
|
|||
// 更新 chatList
|
|||
const existingConversationIndex = chatList.value.findIndex(conversation => conversation.id === id); |
|||
if (existingConversationIndex !== -1) { |
|||
chatList.value[existingConversationIndex] = updatedConversation; |
|||
} else { |
|||
chatList.value.push(updatedConversation); |
|||
} |
|||
} else { |
|||
const targetUser = await db.getOne('workbenchusers', id); |
|||
const lastMessage = messageHistory; |
|||
|
|||
const targetUserInfo = { |
|||
id: targetUser.id, |
|||
nickname: targetUser.nickname, |
|||
avatar: targetUser.avatar, |
|||
}; |
|||
|
|||
// 计算时间差
|
|||
const now = new Date(); |
|||
const createdAt = new Date(lastMessage.createdAt); |
|||
const diffTime = Math.abs(now.getTime() - createdAt.getTime()); |
|||
|
|||
// 根据时间差格式化时间
|
|||
const previewTimeFormat = formatTime(Date.now()); |
|||
|
|||
const newConversation = { |
|||
...targetUserInfo, |
|||
previewTimeFormat, |
|||
previewMessage: lastMessage.content, |
|||
previewType: lastMessage.type, |
|||
}; |
|||
|
|||
// 添加到 conversationList
|
|||
await db.addOne('conversationList', newConversation); |
|||
|
|||
// 添加到 chatList
|
|||
chatList.value.push(newConversation); |
|||
} |
|||
const changeChatList = async () => { |
|||
// const res = await getChatList(id)
|
|||
// chatList.value = res.data
|
|||
}; |
|||
|
|||
const formatTime = (timestamp: number): string => { |
|||
const now = new Date(); |
|||
const createdAt = new Date(timestamp); |
|||
const diffTime = Math.abs(now.getTime() - createdAt.getTime()); |
|||
|
|||
const minutes = Math.floor(diffTime / (1000 * 60)); |
|||
const hours = Math.floor(minutes / 60); |
|||
const days = Math.floor(hours / 24); |
|||
|
|||
if (minutes < 1) { |
|||
return '刚刚'; |
|||
} else if (minutes < 60) { |
|||
return `${minutes}分钟前`; |
|||
} else if (hours < 24) { |
|||
return `${hours}小时前`; |
|||
} else { |
|||
return `${days}天前`; |
|||
} |
|||
const handleContextMenu = async () => { |
|||
contextMenu.value.visible = false; |
|||
}; |
|||
|
|||
const clearMessage = () => { |
|||
message.value = ''; |
|||
}; |
|||
|
|||
const initSSE = async () => { |
|||
console.log('initSSE'); |
|||
const source = new EventSource(`${apiUrl}/chat/message`); |
|||
|
|||
console.log(source); |
|||
source.onmessage = function (event) { |
|||
const data = JSON.parse(event.data); |
|||
console.log(data); |
|||
messageStore.handleMessage(data); |
|||
}; |
|||
|
|||
source.onerror = function (event) { |
|||
console.error('EventSource error:', event); |
|||
}; |
|||
}; |
|||
|
|||
const setScrollToBottom = async () => { |
|||
await nextTick(); // 确保 DOM 已经更新完毕
|
|||
|
|||
// 检查 innerRef 是否存在
|
|||
if (!innerRef.value) { |
|||
console.warn('innerRef is not defined.'); |
|||
return; |
|||
} |
|||
const showContextMenu = (event: any, id: number) => { |
|||
contextMenu.value.visible = true; |
|||
contextMenu.value.chatMessageId = id; |
|||
contextMenu.value.x = event.x; |
|||
contextMenu.value.y = event.y; |
|||
|
|||
// 设置滚动条到底部
|
|||
const max = innerRef.value.clientHeight; |
|||
if (scrollbarRef.value) { |
|||
scrollbarRef.value.setScrollTop(max); |
|||
} else { |
|||
console.warn('scrollbarRef is not defined.'); |
|||
} |
|||
return { |
|||
emojiList, |
|||
userList, |
|||
chatList, |
|||
msgList, |
|||
userInfo, |
|||
search, |
|||
showChooseFile, |
|||
currentPage, |
|||
pageSize, |
|||
scrollbarRef, |
|||
innerRef, |
|||
currentNavId, |
|||
targetUserInfo, |
|||
targetUserId, |
|||
message, |
|||
contextMenu, |
|||
initChat, |
|||
setCurrentNavId, |
|||
sendMessage, |
|||
changeChatList, |
|||
handleContextMenu, |
|||
showContextMenu |
|||
}; |
|||
|
|||
const changeChatList = async (id: number) => { |
|||
// 设置 targetUserId
|
|||
targetUserId.value = id; |
|||
|
|||
// 获取当前用户和目标用户的聊天记录
|
|||
const messagesList = await db.getByField('chatRecord', 'targetUserId', id); |
|||
chatHistory.value = messagesList; |
|||
|
|||
// 设置目标用户的信息
|
|||
setTargetUserInfo(id); |
|||
}; |
|||
|
|||
const setTargetUserInfo = async (id: number) => { |
|||
targetUserInfo.value = await db.getOne('workbenchusers', id); |
|||
}; |
|||
|
|||
const handleContextMenu = async () => { |
|||
contextMenu.value.visible = false; |
|||
}; |
|||
|
|||
const getOnlineUsers = async () => { |
|||
const res = await fetchGet(apiUrl + '/chat/online?page=1'); |
|||
|
|||
if (!res.ok) { |
|||
notifyError("获取在线用户失败"); |
|||
return; |
|||
} |
|||
}) |
|||
|
|||
const data = await res.json(); |
|||
console.log(data); |
|||
|
|||
const onlineUsers = data.data.list.map((item: any) => ({ |
|||
id: item.id, |
|||
username: item.username, |
|||
nickname: item.nickname, |
|||
email: item.email, |
|||
phone: item.phone, |
|||
desc: item.desc, |
|||
jobNumber: item.job_number, |
|||
workPlace: item.work_place, |
|||
hiredDate: item.hired_date, |
|||
avatar: item.avatar || '/logo.png', |
|||
isOnline: true, |
|||
ip: item.login_ip, |
|||
updatedAt: item.updated_at, |
|||
createdAt: item.add_time, |
|||
})); |
|||
|
|||
// 更新或添加用户
|
|||
const existingUserIds = await db.getAll('workbenchusers').then(users => users.map(user => user.id)); |
|||
const newUserIds = onlineUsers.filter(user => !existingUserIds.includes(user.id)).map(user => user.id); |
|||
|
|||
// 更新现有用户的状态
|
|||
const updatePromises = onlineUsers.filter(user => existingUserIds.includes(user.id)).map(user => { |
|||
return db.update('workbenchusers', user.id, { |
|||
isOnline: true, |
|||
updatedAt: user.updatedAt, |
|||
ip: user.ip, |
|||
}); |
|||
}); |
|||
|
|||
// 添加新用户
|
|||
const addPromises = onlineUsers.filter((user: { id: any }) => newUserIds.includes(user.id)).map((user: any) => { |
|||
return db.addOne('workbenchusers', user); |
|||
}); |
|||
|
|||
await Promise.all([...updatePromises, ...addPromises]); |
|||
|
|||
// 更新 userList
|
|||
userList.value = await db.getAll('workbenchusers'); |
|||
|
|||
console.log(userList.value); |
|||
}; |
|||
|
|||
const showContextMenu = (event: any, id: number) => { |
|||
contextMenu.value.visible = true; |
|||
contextMenu.value.chatMessageId = id; |
|||
contextMenu.value.x = event.x; |
|||
contextMenu.value.y = event.y; |
|||
}; |
|||
|
|||
return { |
|||
emojiList, |
|||
userList, |
|||
chatList, |
|||
groupList, |
|||
chatHistory, |
|||
userInfo, |
|||
search, |
|||
showChooseFile, |
|||
currentPage, |
|||
pageSize, |
|||
scrollbarRef, |
|||
innerRef, |
|||
currentNavId, |
|||
targetUserInfo, |
|||
targetUserId, |
|||
message, |
|||
contextMenu, |
|||
activeNames, |
|||
initChat, |
|||
initSSE, |
|||
setCurrentNavId, |
|||
sendMessage, |
|||
changeChatList, |
|||
handleContextMenu, |
|||
showContextMenu, |
|||
getOnlineUsers, |
|||
updateConversationList |
|||
}; |
|||
}); |
@ -0,0 +1,106 @@ |
|||
package files |
|||
|
|||
import ( |
|||
"encoding/base64" |
|||
"encoding/json" |
|||
"godo/libs" |
|||
"net/http" |
|||
"strings" |
|||
) |
|||
|
|||
// 带加密读
|
|||
func HandleReadFile(w http.ResponseWriter, r *http.Request) { |
|||
|
|||
path := r.URL.Query().Get("path") |
|||
fPwd := r.Header.Get("fPwd") |
|||
hasPwd := IsHavePwd(fPwd) |
|||
|
|||
// 获取salt值
|
|||
salt := GetSalt(r) |
|||
|
|||
// 校验文件路径
|
|||
if err := validateFilePath(path); err != nil { |
|||
libs.HTTPError(w, http.StatusBadRequest, err.Error()) |
|||
return |
|||
} |
|||
|
|||
// 有密码校验密码
|
|||
if hasPwd { |
|||
if !CheckFilePwd(fPwd, salt) { |
|||
libs.HTTPError(w, http.StatusBadRequest, "密码错误") |
|||
return |
|||
} |
|||
} |
|||
|
|||
// 获取文件路径
|
|||
basePath, err := libs.GetOsDir() |
|||
if err != nil { |
|||
libs.HTTPError(w, http.StatusInternalServerError, err.Error()) |
|||
return |
|||
} |
|||
// 读取内容
|
|||
fileContent, err := ReadFile(basePath, path) |
|||
if err != nil { |
|||
libs.HTTPError(w, http.StatusNotFound, err.Error()) |
|||
return |
|||
} |
|||
|
|||
// 解密
|
|||
data, err := libs.DecryptData(fileContent, libs.EncryptionKey) |
|||
if err != nil { |
|||
libs.HTTPError(w, http.StatusInternalServerError, err.Error()) |
|||
return |
|||
} |
|||
|
|||
content := string(data) |
|||
// 检查文件内容是否以"link::"开头
|
|||
if !strings.HasPrefix(content, "link::") { |
|||
content = base64.StdEncoding.EncodeToString(data) |
|||
} |
|||
|
|||
// 初始响应
|
|||
res := libs.APIResponse{Code: 0, Message: "success", Data: content} |
|||
|
|||
json.NewEncoder(w).Encode(res) |
|||
} |
|||
|
|||
// 设置文件密码
|
|||
func HandleSetFilePwd(w http.ResponseWriter, r *http.Request) { |
|||
fPwd := r.Header.Get("filepPwd") |
|||
salt := r.Header.Get("salt") |
|||
// 服务端再hash加密
|
|||
hashPwd := libs.HashPassword(fPwd, salt) |
|||
|
|||
// 服务端存储
|
|||
req := libs.ReqBody{ |
|||
Name: "filePwd", |
|||
Value: hashPwd, |
|||
} |
|||
libs.SetConfig(req) |
|||
|
|||
// salt值存储
|
|||
reqSalt := libs.ReqBody{ |
|||
Name: "salt", |
|||
Value: salt, |
|||
} |
|||
libs.SetConfig(reqSalt) |
|||
res := libs.APIResponse{Message: "密码设置成功"} |
|||
json.NewEncoder(w).Encode(res) |
|||
} |
|||
|
|||
// 更改文件密码
|
|||
func HandleChangeFilePwd(w http.ResponseWriter, r *http.Request) { |
|||
filePwd := r.Header.Get("filePwd") |
|||
salt := r.Header.Get("salt") |
|||
if filePwd == "" || salt == "" { |
|||
libs.ErrorMsg(w, "密码为空") |
|||
return |
|||
} |
|||
newPwd := libs.HashPassword(filePwd, salt) |
|||
pwdReq := libs.ReqBody{ |
|||
Name: "filePwd", |
|||
Value: newPwd, |
|||
} |
|||
libs.SetConfig(pwdReq) |
|||
libs.SuccessMsg(w, "success", "The file password change success!") |
|||
} |
Loading…
Reference in new issue