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"> |
<script setup lang="ts"> |
||||
import { useChatStore } from '@/stores/chat'; |
import { useChatStore } from "@/stores/chat"; |
||||
import { Search } from '@element-plus/icons-vue'; |
import { getWorkflowUrl } from "@/system/config"; |
||||
import {getWorkflowUrl} from '@/system/config' |
import { Search } from "@element-plus/icons-vue"; |
||||
const store = useChatStore() |
const store = useChatStore(); |
||||
const workUrl = getWorkflowUrl() |
const workUrl = getWorkflowUrl(); |
||||
onMounted(() => { |
onMounted(() => { |
||||
store.initChat() |
store.initChat(); |
||||
}) |
// store.initSSE(); |
||||
|
}); |
||||
</script> |
</script> |
||||
<template> |
<template> |
||||
<el-container class="container"> |
<el-container class="container"> |
||||
<!--菜单--> |
<!--菜单--> |
||||
<el-aside class="menu"> |
<el-aside class="menu"> |
||||
<chat-menu /> |
<chat-menu /> |
||||
</el-aside> |
</el-aside> |
||||
<el-container class="side" v-if="store.currentNavId < 3"> |
<el-container |
||||
<!--搜索栏--> |
class="side" |
||||
<el-header class="search" v-if="store.currentNavId < 2"> |
v-if="store.currentNavId < 3" |
||||
<el-input placeholder="搜索" :prefix-icon="Search" class="search-input" v-model="store.search" /> |
> |
||||
</el-header> |
<!--搜索栏--> |
||||
<!--好友列表--> |
<el-header |
||||
<el-main class="list"> |
class="search" |
||||
<el-scrollbar> |
v-if="store.currentNavId < 2" |
||||
<chat-msg-list v-if="store.currentNavId == 0" /> |
> |
||||
<chat-user-list v-if="store.currentNavId == 1" /> |
<el-input |
||||
<!-- <chat-work-list v-if="store.currentNavId == 2" /> --> |
placeholder="搜索" |
||||
</el-scrollbar> |
:prefix-icon="Search" |
||||
</el-main> |
class="search-input" |
||||
</el-container> |
v-model="store.search" |
||||
<el-container class="chat-box"> |
/> |
||||
<chat-box v-if="store.currentNavId < 2" /> |
</el-header> |
||||
</el-container> |
<!--好友列表--> |
||||
<el-container class="chat-setting" v-if="store.currentNavId == 2"> |
<el-main class="list"> |
||||
<iframe class="workflow" :src="workUrl"></iframe> |
<el-scrollbar> |
||||
</el-container> |
<chat-msg-list v-if="store.currentNavId == 0" /> |
||||
<el-container class="chat-setting" v-if="store.currentNavId == 5"> |
<chat-user-list v-if="store.currentNavId == 1" /> |
||||
<ChatUserSetting /> |
<!-- <chat-work-list v-if="store.currentNavId == 2" /> --> |
||||
</el-container> |
</el-scrollbar> |
||||
</el-container> |
</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> |
</template> |
||||
<style scoped> |
<style scoped> |
||||
.container { |
.container { |
||||
display: flex; |
display: flex; |
||||
height: 100%; |
height: 100%; |
||||
width: 100%; |
width: 100%; |
||||
overflow-y: hidden; |
overflow-y: hidden; |
||||
overflow-x: hidden; |
overflow-x: hidden; |
||||
} |
} |
||||
|
|
||||
.menu { |
.menu { |
||||
width: 55px; |
width: 55px; |
||||
background-color: #2E2E2E; |
|
||||
overflow-y: hidden; |
|
||||
overflow-x: hidden; |
|
||||
-webkit-app-region: drag; |
|
||||
} |
|
||||
|
|
||||
.side { |
background-color: white; |
||||
flex: 1; |
overflow-y: hidden; |
||||
/* 占据剩余宽度 */ |
overflow-x: hidden; |
||||
max-height: max-content; |
-webkit-app-region: drag; |
||||
border-right: 1px solid #edebeb; |
} |
||||
overflow-y: hidden; |
|
||||
overflow-x: hidden; |
|
||||
background-color: #F7F7F7; |
|
||||
} |
|
||||
|
|
||||
.search { |
.side { |
||||
width: 100%; |
flex: 1; |
||||
/* 占据整个宽度 */ |
/* 占据剩余宽度 */ |
||||
|
max-height: max-content; |
||||
|
border-right: 1px solid #edebeb; |
||||
|
overflow-y: hidden; |
||||
|
overflow-x: hidden; |
||||
|
background-color: #f7f7f7; |
||||
|
} |
||||
|
|
||||
height: 50px; |
.search { |
||||
padding: 0; |
width: 100%; |
||||
-webkit-app-region: drag; |
/* 占据整个宽度 */ |
||||
} |
height: 50px; |
||||
|
padding: 0; |
||||
|
-webkit-app-region: drag; |
||||
|
} |
||||
|
|
||||
.search-input { |
.search-input { |
||||
width: calc(100% - 20px); |
width: calc(100% - 20px); |
||||
/* 减去左右边距 */ |
/* 减去左右边距 */ |
||||
margin: 10px; |
margin: 10px; |
||||
-webkit-app-region: no-drag; |
-webkit-app-region: no-drag; |
||||
--el-input-placeholder-color: #818181 !important; |
--el-input-placeholder-color: #818181 !important; |
||||
--el-input-icon-color: #5D5D5D !important; |
--el-input-icon-color: #5d5d5d !important; |
||||
} |
} |
||||
|
|
||||
.list { |
.list { |
||||
width: 100%; |
width: 100%; |
||||
/* 占据整个宽度 */ |
/* 占据整个宽度 */ |
||||
padding: 0; |
padding: 0; |
||||
overflow-y: hidden; |
overflow-y: hidden; |
||||
overflow-x: hidden; |
overflow-x: hidden; |
||||
} |
} |
||||
|
|
||||
.chat-box { |
.chat-box { |
||||
flex: 3; |
flex: 3; |
||||
/* 占据剩余宽度的三倍 */ |
/* 占据剩余宽度的三倍 */ |
||||
max-height: max-content; |
max-height: max-content; |
||||
background-color: #F5F5F5; |
background-color: #f5f5f5; |
||||
} |
} |
||||
|
|
||||
.chat-setting { |
.chat-setting { |
||||
width: calc(100% - 65px); |
width: calc(100% - 65px); |
||||
/* 占据整个宽度 */ |
/* 占据整个宽度 */ |
||||
height: 100%; |
height: 100%; |
||||
overflow: hidden; |
overflow: hidden; |
||||
} |
} |
||||
.workflow { |
.workflow { |
||||
width: 100%; |
width: 100%; |
||||
height: 100%; |
height: 100%; |
||||
object-fit: contain; |
object-fit: contain; |
||||
border: none; |
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"> |
<script setup lang="ts"> |
||||
import { useChatStore } from '@/stores/chat'; |
import { useChatStore } from "@/stores/chat"; |
||||
const store = useChatStore() |
const store = useChatStore(); |
||||
</script> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<div class="chatbox-main" v-if="store.targetUserId > 0"> |
<div |
||||
<!--聊天顶部区--> |
class="chatbox-main" |
||||
<el-header class="chat-header"> |
v-if="store.targetUserId > 0" |
||||
<div class="header-title">{{ store.targetUserInfo.username }}</div> |
> |
||||
</el-header> |
<!--聊天顶部区--> |
||||
<!--聊天主体区--> |
<el-header class="chat-header"> |
||||
<el-main class="msg-main"> |
<div class="header-title">{{ store.targetUserInfo.username }}</div> |
||||
<el-scrollbar ref="store.scrollbarRef"> |
</el-header> |
||||
<div ref="store.innerRef"> |
<!--聊天主体区--> |
||||
<ChatMessage /> |
<el-main class="msg-main"> |
||||
</div> |
<el-scrollbar ref="store.scrollbarRef"> |
||||
</el-scrollbar> |
<div ref="store.innerRef"> |
||||
</el-main> |
<ChatMessage /> |
||||
<el-footer class="msg-footer"> |
</div> |
||||
<!--聊天输入选项--> |
</el-scrollbar> |
||||
<div class="input-option"> |
</el-main> |
||||
<el-icon :size="20" class="input-option-icon"> |
<el-footer class="msg-footer"> |
||||
<Picture /> |
<!--聊天输入选项--> |
||||
</el-icon> |
<div class="input-option"> |
||||
<el-icon :size="20" class="input-option-icon"> |
<el-icon |
||||
<Link /> |
:size="20" |
||||
</el-icon> |
class="input-option-icon" |
||||
<el-icon :size="20" class="input-option-icon"> |
> |
||||
<Delete /> |
<Picture /> |
||||
</el-icon> |
</el-icon> |
||||
</div> |
<el-icon |
||||
<!--聊天输入区--> |
:size="20" |
||||
<div class="input-textarea"> |
class="input-option-icon" |
||||
<el-input type="textarea" maxlength="1000" resize="none" class="textarea" |
> |
||||
@keyup.enter.exact="store.sendMessage" v-model="store.message" /> |
<Link /> |
||||
</div> |
</el-icon> |
||||
<!--聊天发送按钮--> |
<el-icon |
||||
<el-tooltip placement="top" content="按enter键发送,按ctrl+enter键换行"> |
:size="20" |
||||
<el-icon :size="22" class="input-button"> |
class="input-option-icon" |
||||
<Promotion /> |
> |
||||
</el-icon> |
<Delete /> |
||||
</el-tooltip> |
</el-icon> |
||||
</el-footer> |
</div> |
||||
</div> |
<!--聊天输入区--> |
||||
<div class="no-message-container" v-else> |
<div class="input-textarea"> |
||||
<el-icon :size="180" color="#0078d7"> |
<el-input |
||||
<ScaleToOriginal /> |
type="textarea" |
||||
</el-icon> |
maxlength="1000" |
||||
<p>暂无内容</p> |
resize="none" |
||||
</div> |
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> |
</template> |
||||
<style scoped> |
<style scoped> |
||||
.chatbox-main { |
.chatbox-main { |
||||
width: 100%; |
width: 100%; |
||||
height: 100%; |
height: 100%; |
||||
} |
} |
||||
|
|
||||
.chat-header { |
.chat-header { |
||||
width: 100%; |
width: 100%; |
||||
/* 占据整个宽度 */ |
/* 占据整个宽度 */ |
||||
height: 50px; |
height: 50px; |
||||
line-height: 50px; |
line-height: 50px; |
||||
padding: 0; |
padding: 0; |
||||
-webkit-app-region: drag; |
-webkit-app-region: drag; |
||||
} |
} |
||||
|
|
||||
header-title { |
header-title { |
||||
font-size: 20px; |
font-size: 20px; |
||||
text-align: left; |
text-align: left; |
||||
margin-left: 15px; |
margin-left: 15px; |
||||
} |
} |
||||
|
|
||||
.msg-main { |
.msg-main { |
||||
width: 100%; |
width: 100%; |
||||
/* 占据整个宽度 */ |
/* 占据整个宽度 */ |
||||
height: calc(70% - 50px); |
height: calc(70% - 50px); |
||||
padding: 0; |
padding: 0; |
||||
border-top-width: 1px; |
border-top-width: 1px; |
||||
border-bottom-width: 1px; |
border-bottom-width: 1px; |
||||
border-left-width: 0; |
border-left-width: 0; |
||||
border-right-width: 0; |
border-right-width: 0; |
||||
border-color: #D6D6D6; |
border-color: #d6d6d6; |
||||
border-style: solid; |
border-style: solid; |
||||
} |
} |
||||
|
|
||||
.msg-footer { |
.msg-footer { |
||||
width: 100%; |
width: 100%; |
||||
/* 占据整个宽度 */ |
/* 占据整个宽度 */ |
||||
height: 30%; |
height: 30%; |
||||
padding: 0; |
padding: 0; |
||||
} |
} |
||||
|
|
||||
.input-option { |
.input-option { |
||||
height: 20px; |
height: 20px; |
||||
padding: 5px; |
padding: 5px; |
||||
} |
} |
||||
|
|
||||
.input-option-icon { |
.input-option-icon { |
||||
color: #494949; |
color: #494949; |
||||
margin-left: 20px; |
margin-left: 20px; |
||||
margin-top: 5px; |
margin-top: 5px; |
||||
cursor: pointer; |
cursor: pointer; |
||||
} |
} |
||||
|
|
||||
.input-textarea { |
.input-textarea { |
||||
height: calc(100% - 50px); |
height: calc(100% - 50px); |
||||
width: calc(100% - 20px); |
width: calc(100% - 20px); |
||||
/* 减去左右边距 */ |
/* 减去左右边距 */ |
||||
margin: 10px; |
margin: 10px; |
||||
} |
} |
||||
|
|
||||
.textarea { |
.textarea { |
||||
font-size: 16px; |
font-size: 16px; |
||||
font-family: Arial, sans-serif; |
font-family: Arial, sans-serif; |
||||
line-height: 1.5; |
line-height: 1.5; |
||||
width: 100%; |
width: 100%; |
||||
height: 100%; |
height: 100%; |
||||
overflow-y: hidden; |
overflow-y: hidden; |
||||
overflow-x: hidden; |
overflow-x: hidden; |
||||
--el-input-border-radius: 0; |
--el-input-border-radius: 0; |
||||
--el-input-border-color: transparent; |
--el-input-border-color: transparent; |
||||
--el-input-hover-border-color: transparent; |
--el-input-hover-border-color: transparent; |
||||
--el-input-clear-hover-color: transparent; |
--el-input-clear-hover-color: transparent; |
||||
--el-input-focus-border-color: transparent; |
--el-input-focus-border-color: transparent; |
||||
} |
} |
||||
|
|
||||
.input-button { |
.input-button { |
||||
position: absolute; |
position: absolute; |
||||
bottom: 12px; |
bottom: 12px; |
||||
right: 12px; |
right: 12px; |
||||
width: 30px; |
width: 30px; |
||||
/* 缩小宽度 */ |
/* 缩小宽度 */ |
||||
height: 30px; |
height: 30px; |
||||
/* 减小高度 */ |
/* 减小高度 */ |
||||
border-radius: 50%; |
border-radius: 50%; |
||||
/* 较小的圆角 */ |
/* 较小的圆角 */ |
||||
background-color: #E8F0FE; |
background-color: #e8f0fe; |
||||
/* 浅蓝色,符合Win11的轻量风格 */ |
/* 浅蓝色,符合Win11的轻量风格 */ |
||||
color: #0078D4; |
color: #0078d4; |
||||
/* 使用Win11的强调色作为文字颜色 */ |
/* 使用Win11的强调色作为文字颜色 */ |
||||
font-weight: bold; |
font-weight: bold; |
||||
border: 1px solid #B3D4FC; |
border: 1px solid #b3d4fc; |
||||
/* 添加边框,保持简洁风格 */ |
/* 添加边框,保持简洁风格 */ |
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); |
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); |
||||
/* 轻微阴影 */ |
/* 轻微阴影 */ |
||||
transition: all 0.2s ease; |
transition: all 0.2s ease; |
||||
/* 快速过渡效果 */ |
/* 快速过渡效果 */ |
||||
} |
} |
||||
|
|
||||
.input-button:hover { |
.input-button:hover { |
||||
background-color: #D1E4FF; |
background-color: #d1e4ff; |
||||
/* 悬浮时颜色略深,保持浅色调 */ |
/* 悬浮时颜色略深,保持浅色调 */ |
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
||||
/* 稍微增强阴影 */ |
/* 稍微增强阴影 */ |
||||
} |
} |
||||
|
|
||||
.input-button:active { |
.input-button:active { |
||||
background-color: #B3D4FC; |
background-color: #b3d4fc; |
||||
/* 按下时颜色更深,但依然保持清新 */ |
/* 按下时颜色更深,但依然保持清新 */ |
||||
box-shadow: 0 1px 2px rgba(0, 0, 0.1); |
box-shadow: 0 1px 2px rgba(0, 0, 0.1); |
||||
/* 回复初始阴影 */ |
/* 回复初始阴影 */ |
||||
transform: translateY(1px); |
transform: translateY(1px); |
||||
/* 微小下移,模拟按下 */ |
/* 微小下移,模拟按下 */ |
||||
} |
} |
||||
|
|
||||
.no-message-container { |
.no-message-container { |
||||
height: 100%; |
height: 100%; |
||||
margin: 120px auto; |
margin: 120px auto; |
||||
text-align: center; |
text-align: center; |
||||
justify-content: center; |
justify-content: center; |
||||
} |
} |
||||
</style> |
</style> |
@ -1,71 +1,158 @@ |
|||||
<script setup> |
|
||||
import { ref } from "vue"; |
|
||||
import { useChatStore } from "@/stores/chat"; |
|
||||
const store = useChatStore() |
|
||||
|
|
||||
</script> |
|
||||
|
|
||||
<template> |
<template> |
||||
<div v-for="item in store.chatList"> |
<div v-if="store.chatList.length > 0" v-for="item in store.chatList" :key="item.id"> |
||||
<div class="list-item" @click="store.changeChatList(item)" |
<div |
||||
:style="{ backgroundColor: item.id === store.targetUserId ? '#C4C4C4' : '' }"> |
class="list-item" |
||||
|
@click="store.changeChatList(item.id)" |
||||
|
:style="{ |
||||
|
backgroundColor: item.id === store.targetUserId ? '#C4C4C4' : '', |
||||
|
}" |
||||
|
> |
||||
<el-row> |
<el-row> |
||||
<el-col :span="6"> |
<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> |
||||
<el-col :span="18"> |
<el-col :span="18" class="preview"> |
||||
<el-row> |
<el-row class="preview-content"> |
||||
<el-col :span="18"> |
<el-col :span="18" class="preview-left"> |
||||
<div class="previewName">{{ item.name }}</div> |
<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> |
||||
<el-col :span="6"> |
<el-col :span="6" class="preview-right"> |
||||
<div class="previewTime">{{ item.previewTimeFormat }}</div> |
<div class="previewTime"> |
||||
|
{{ item.previewTimeFormat }} |
||||
|
</div> |
||||
</el-col> |
</el-col> |
||||
</el-row> |
</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-col> |
||||
</el-row> |
</el-row> |
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
|
<div v-else class="emptyChat"> |
||||
|
<el-icon :size="60" class="chat-icon"> |
||||
|
<ChatSquare /> |
||||
|
</el-icon> |
||||
|
<p class="empty-message">暂无数据</p> |
||||
|
</div> |
||||
</template> |
</template> |
||||
|
|
||||
|
<script setup> |
||||
|
import { useChatStore } from "@/stores/chat"; |
||||
|
import { ref } from "vue"; |
||||
|
|
||||
|
const store = useChatStore(); |
||||
|
const id = ref("1"); |
||||
|
|
||||
|
</script> |
||||
|
|
||||
<style scoped> |
<style scoped> |
||||
.list-item { |
.list-item { |
||||
width: 100%; |
width: 100%; |
||||
height: 60px; |
height: 60px; |
||||
} |
display: flex; |
||||
|
} |
||||
.list-item:hover { |
|
||||
background-color: #D0D0D0; |
.list-item:hover { |
||||
} |
background-color: #d0d0d0; |
||||
|
} |
||||
.avatar { |
|
||||
margin: 10px |
.avatar { |
||||
} |
margin: 10px; |
||||
|
} |
||||
.previewName { |
|
||||
margin-top: 10px; |
.preview { |
||||
font-size: 15px; |
display: flex; |
||||
font-family: Arial, sans-serif; |
align-items: center; |
||||
line-height: 1.5; |
justify-content: space-between; |
||||
} |
height: 100%; |
||||
|
} |
||||
.previewChat { |
|
||||
font-size: 14px; |
.preview-content { |
||||
font-family: Arial, sans-serif; |
height: 100%; |
||||
color: #999999; |
display: flex; |
||||
} |
align-items: center; |
||||
|
justify-content: space-between; |
||||
.previewTime { |
width: 100%; |
||||
float: right; |
} |
||||
margin-top: 10px; |
|
||||
margin-right: 10px; |
.preview-left { |
||||
font-size: 12px; |
display: flex; |
||||
font-family: Arial, sans-serif; |
flex-direction: column; |
||||
color: #999999; |
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> |
</style> |
||||
|
@ -1,71 +1,221 @@ |
|||||
<script setup> |
<script setup> |
||||
import { ref } from "vue"; |
import { useChatStore } from "@/stores/chat"; |
||||
import { useChatStore } from "@/stores/chat"; |
import { |
||||
const store = useChatStore() |
ElAvatar, |
||||
|
ElCol, |
||||
|
ElCollapse, |
||||
|
ElCollapseItem, |
||||
|
ElRow, |
||||
|
} from "element-plus"; |
||||
|
const store = useChatStore(); |
||||
</script> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<div v-for="item in store.chatList"> |
<el-collapse |
||||
<div class="list-item" @click="store.changeChatList(item)" |
v-model="store.activeNames" |
||||
:style="{ backgroundColor: item.id === store.targetUserId ? '#C4C4C4' : '' }"> |
@change="handleChange" |
||||
<el-row> |
> |
||||
<el-col :span="6"> |
<el-collapse-item name="1"> |
||||
<el-avatar shape="square" :size="40" class="avatar" :src="item.avatar" /> |
<template #title> |
||||
</el-col> |
<span v-if="store.userList.length > 0" class="title">同事({{store.userList.length}})</span> |
||||
<el-col :span="18"> |
<span v-else class="title">同事</span> |
||||
<el-row> |
</template> |
||||
<el-col :span="18"> |
<div v-if="store.userList.length > 0"> |
||||
<div class="previewName">{{ item.name }}</div> |
<div |
||||
</el-col> |
v-for="item in store.userList" |
||||
<el-col :span="6"> |
:key="item.id" |
||||
<div class="previewTime">{{ item.previewTimeFormat }}</div> |
> |
||||
</el-col> |
<div |
||||
</el-row> |
class="list-item" |
||||
<el-row> |
@click="store.changeChatList(item.id)" |
||||
<div v-if="item.previewType === 0" class="previewChat">{{ item.previewMessage }}</div> |
:style="{ |
||||
<div v-if="item.previewType === 1" class="previewChat">{{ item.userId === id ? "你" : "\"" + item.name + |
backgroundColor: |
||||
"\""}}撤回了一条消息</div> |
item.id === store.targetUserId ? '#C4C4C4' : '', |
||||
</el-row> |
}" |
||||
</el-col> |
> |
||||
</el-row> |
<el-row> |
||||
</div> |
<el-col :span="6"> |
||||
</div> |
<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> |
</template> |
||||
|
|
||||
<style scoped> |
<style scoped> |
||||
.list-item { |
.title { |
||||
width: 240px; |
padding-left: 10px; |
||||
height: 60px; |
} |
||||
} |
.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 { |
.previewIP { |
||||
background-color: #D0D0D0; |
margin-left: 10px; |
||||
} |
font-size: 12px; |
||||
|
font-family: Arial, sans-serif; |
||||
|
color: #999999; |
||||
|
display: flex; /* 使用 Flexbox */ |
||||
|
align-items: center; /* 垂直居中 */ |
||||
|
} |
||||
|
|
||||
.avatar { |
.online-icon { |
||||
margin: 10px |
font-size: 16px; /* 调整图标大小 */ |
||||
} |
color: green; /* 在线状态颜色 */ |
||||
|
margin-left: 5px; /* 与用户名之间的间距 */ |
||||
|
} |
||||
|
|
||||
.previewName { |
.previewChat { |
||||
margin-top: 10px; |
margin-left: 10px; |
||||
font-size: 15px; |
font-size: 12px; /* 调整字体大小 */ |
||||
font-family: Arial, sans-serif; |
font-family: Arial, sans-serif; |
||||
line-height: 1.5; |
color: #999999; |
||||
} |
overflow: hidden; /* 隐藏超出部分 */ |
||||
|
text-overflow: ellipsis; /* 显示为省略号 */ |
||||
|
white-space: nowrap; /* 不换行 */ |
||||
|
max-width: 100%; /* 最大宽度 */ |
||||
|
} |
||||
|
|
||||
.previewChat { |
.previewTime { |
||||
font-size: 14px; |
float: right; |
||||
font-family: Arial, sans-serif; |
margin-top: 10px; |
||||
color: #999999; |
margin-right: 10px; |
||||
} |
font-size: 12px; |
||||
|
font-family: Arial, sans-serif; |
||||
|
color: #999999; |
||||
|
} |
||||
|
|
||||
.previewTime { |
.no-data { |
||||
float: right; |
text-align: center; |
||||
margin-top: 10px; |
color: #999999; |
||||
margin-right: 10px; |
} |
||||
font-size: 12px; |
|
||||
font-family: Arial, sans-serif; |
|
||||
color: #999999; |
|
||||
} |
|
||||
</style> |
</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 emojiList from "@/assets/emoji.json" |
import { fetchGet, fetchPost, getSystemConfig } from '@/system/config'; |
||||
import { 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', () => { |
export const useChatStore = defineStore('chatStore', () => { |
||||
const userList: any = ref([]) // 用户列表
|
// 用户列表
|
||||
const chatList: any = ref([]) // 消息列表
|
const userList: any = ref([]); |
||||
const msgList: any = ref([]) //聊天消息列表
|
|
||||
const userInfo: any = ref({}) |
// 定义聊天列表项的接口
|
||||
const showChooseFile = ref(false) |
interface ChatListItem { |
||||
const currentPage = ref(1) |
id: number; |
||||
const pageSize = ref(50) |
nickname: string; |
||||
const scrollbarRef = ref() |
avatar: string; |
||||
const innerRef = ref() |
previewTimeFormat: string; |
||||
const config = getSystemConfig() |
previewType: 0 | 1; // 消息类型,0表示正常消息,1表示撤回消息
|
||||
const currentNavId = ref(0) |
previewMessage: string; |
||||
const message: any = ref() |
} |
||||
const targetUserInfo:any = ref({}) |
|
||||
const targetUserId = ref(0) |
// 模拟数据 - 聊天列表
|
||||
const search = ref('') |
const chatList = ref<ChatListItem[]>([ |
||||
const contextMenu = ref({ |
{ |
||||
visible: false, |
id: 2, |
||||
chatMessageId: 0, |
nickname: '朋友2', |
||||
list: [ |
avatar: '/logo.png', |
||||
{ |
previewTimeFormat: "昨天", |
||||
id: 2, |
previewType: 1, |
||||
label: '撤回', |
previewMessage: "测试消息", |
||||
} |
}, |
||||
], |
]); |
||||
x: 0, |
|
||||
y: 0 |
// 模拟数据 - 聊天消息列表
|
||||
}) |
const chatHistory = ref([]); |
||||
const initChat = () => { |
|
||||
if(config.userInfo.avatar == ''){ |
// 群组数据
|
||||
config.userInfo.avatar = '/logo.png' |
const groupList = ref([ |
||||
} |
{ |
||||
userInfo.value = config.userInfo |
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 () => { |
userInfo.value = config.userInfo; |
||||
await setScrollToBottom() |
}; |
||||
|
|
||||
|
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 () => { |
notifyError("消息发送失败"); |
||||
await nextTick() |
}; |
||||
const max = innerRef.value.clientHeight |
|
||||
scrollbarRef.value.setScrollTop(max) |
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; |
const max = innerRef.value.clientHeight; |
||||
contextMenu.value.x = event.x; |
if (scrollbarRef.value) { |
||||
contextMenu.value.y = event.y; |
scrollbarRef.value.setScrollTop(max); |
||||
|
} else { |
||||
|
console.warn('scrollbarRef is not defined.'); |
||||
} |
} |
||||
return { |
}; |
||||
emojiList, |
|
||||
userList, |
const changeChatList = async (id: number) => { |
||||
chatList, |
// 设置 targetUserId
|
||||
msgList, |
targetUserId.value = id; |
||||
userInfo, |
|
||||
search, |
// 获取当前用户和目标用户的聊天记录
|
||||
showChooseFile, |
const messagesList = await db.getByField('chatRecord', 'targetUserId', id); |
||||
currentPage, |
chatHistory.value = messagesList; |
||||
pageSize, |
|
||||
scrollbarRef, |
// 设置目标用户的信息
|
||||
innerRef, |
setTargetUserInfo(id); |
||||
currentNavId, |
}; |
||||
targetUserInfo, |
|
||||
targetUserId, |
const setTargetUserInfo = async (id: number) => { |
||||
message, |
targetUserInfo.value = await db.getOne('workbenchusers', id); |
||||
contextMenu, |
}; |
||||
initChat, |
|
||||
setCurrentNavId, |
const handleContextMenu = async () => { |
||||
sendMessage, |
contextMenu.value.visible = false; |
||||
changeChatList, |
}; |
||||
handleContextMenu, |
|
||||
showContextMenu |
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