prr 7 months ago
parent
commit
5c8028d928
  1. 3
      frontend/auto-imports.d.ts
  2. 1
      frontend/components.d.ts
  3. 106
      frontend/src/components/chat/Chat.vue
  4. 62
      frontend/src/components/chat/ChatBox.vue
  5. 127
      frontend/src/components/chat/ChatMessage.vue
  6. 14
      frontend/src/components/chat/ChatMsgList.vue
  7. 23
      frontend/src/components/chat/ChatUserList.vue
  8. 4
      frontend/src/components/chat/chatUserInfo.vue
  9. 419
      frontend/src/stores/chat.ts
  10. 23
      frontend/src/stores/db.ts
  11. 18
      frontend/src/stores/upgrade.ts
  12. 5
      frontend/src/system/index.ts
  13. 28
      godo/files/pwdfile.go

3
frontend/auto-imports.d.ts

@ -3,6 +3,7 @@
// @ts-nocheck // @ts-nocheck
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import // Generated by unplugin-auto-import
// biome-ignore lint: disable
export {} export {}
declare global { declare global {
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
@ -70,6 +71,6 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue') import('vue')
} }

1
frontend/components.d.ts

@ -53,7 +53,6 @@ declare module 'vue' {
ElAside: typeof import('element-plus/es')['ElAside'] ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBadge: typeof import('element-plus/es')['ElBadge'] ElBadge: typeof import('element-plus/es')['ElBadge']
ElBu: typeof import('element-plus/es')['ElBu']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCarousel: typeof import('element-plus/es')['ElCarousel'] ElCarousel: typeof import('element-plus/es')['ElCarousel']

106
frontend/src/components/chat/Chat.vue

@ -7,21 +7,37 @@
onMounted(() => { onMounted(() => {
store.initChat(); store.initChat();
}); });
const userList = ref([
{
id: 2,
nickname: "朋友2",
avatar: "/logo.png",
previewTimeFormat: "昨天",
previewType: 1,
previewMessage: "测试消息",
},
{
id: 3,
nickname: "朋友3",
avatar: "/logo.png",
previewTimeFormat: "昨天",
previewType: 1,
previewMessage: "测试消息",
},
]);
// el-transfer
const generateData = () => { const generateData = () => {
const data = []; return userList.value.map((user) => ({
for (let i = 1; i <= 15; i++) { key: user.id,
data.push({ label: user.nickname,
key: i, avatar: user.avatar, //
label: ` ${i}`, }));
disabled: i % 4 === 0,
});
}
return data;
}; };
const data = generateData(); const data = ref(generateData());
const value = ref([]); const users = ref([]);
</script> </script>
<template> <template>
<el-container class="container"> <el-container class="container">
@ -47,7 +63,7 @@
<!-- 邀请群聊 --> <!-- 邀请群聊 -->
<button <button
class="inviteGroupChats" class="inviteGroupChats"
@click="store.setGroupChatDialogVisible(true)" @click="store.setGroupChatInvitedDialogVisible(true)"
> >
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
</button> </button>
@ -57,7 +73,6 @@
<el-scrollbar> <el-scrollbar>
<chat-msg-list v-if="store.currentNavId == 0" /> <chat-msg-list v-if="store.currentNavId == 0" />
<chat-user-list v-if="store.currentNavId == 1" /> <chat-user-list v-if="store.currentNavId == 1" />
<!-- <chat-work-list v-if="store.currentNavId == 2" /> -->
</el-scrollbar> </el-scrollbar>
</el-main> </el-main>
</el-container> </el-container>
@ -81,29 +96,44 @@
<ChatUserSetting /> <ChatUserSetting />
</el-container> </el-container>
</el-container> </el-container>
<!-- 群聊弹窗 -->
<!-- 邀请群聊弹窗 -->
<el-dialog <el-dialog
v-model="store.groupChatDialogVisible" v-model="store.groupChatInvitedDialogVisible"
title="发起群聊" title="发起群聊"
width="600px" width="80%"
> >
<div class="transfer"> <div class="dialog-body">
<el-transfer class="transfer-box" v-model="value" :data="data" /> <el-transfer
</div> v-model="users"
:data="data"
>
<!-- 义穿梭框 -->
<template #default="{ option }">
<div class="user-item">
<el-avatar
:size="20"
:src="option.avatar"
class="avatar"
/>
<span>{{ option.label }}</span>
</div>
</template>
</el-transfer>
</div>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="store.groupChatDialogVisible = false" <el-button @click="store.groupChatInvitedDialogVisible = false"
>取消</el-button >取消</el-button
> >
<el-button <el-button @click="store.setGroupInfoDrawerVisible(true)"
type="primary"
@click="createGroupChat"
>确定</el-button >确定</el-button
> >
</span> </span>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<style scoped> <style scoped>
.container { .container {
display: flex; display: flex;
@ -149,13 +179,12 @@
background-color: #f0f0f0; background-color: #f0f0f0;
} }
.user-item {
.transfer-box { width: 100%;
height: 220px; height: 30px;
display: flex; display: flex;
justify-content: center; align-items: center;
align-items: center; }
}
.search-input { .search-input {
width: calc(100% - 20px); width: calc(100% - 20px);
@ -174,6 +203,21 @@
overflow-y: hidden; overflow-y: hidden;
overflow-x: hidden; overflow-x: hidden;
} }
.dialog-body {
width: 100%;
}
.el-transfer {
display: flex;
align-items: center;
justify-content: center;
}
/* .el-transfer >>> .el-transfer-panel >>> .el-transfer__buttons {
width: 50px;
} */
/* .el-transfer__button {
width: 20px;;
} */
.chat-box { .chat-box {
flex: 3; flex: 3;

62
frontend/src/components/chat/ChatBox.vue

@ -4,23 +4,29 @@
</script> </script>
<template> <template>
<div <div
class="chatbox-main" class="chatbox-main"
v-if="store.targetUserId > 0" v-if="store.targetUserId > 0"
> >
<!--聊天顶部区--> <!--聊天顶部区-->
<el-header class="chat-header"> <el-header class="chat-header">
<div class="header-title">{{ store.targetUserInfo.username }}</div> <div class="header-title" >
</el-header> {{ store.targetUserInfo.nickname }}
<!--聊天主体区--> </div>
<el-main class="msg-main">
<el-scrollbar ref="store.scrollbarRef"> </el-header>
<div ref="store.innerRef">
<ChatMessage /> <!--聊天主体区-->
</div> <el-main class="msg-main">
</el-scrollbar> <el-scrollbar ref="store.scrollbarRef">
</el-main> <div ref="store.innerRef">
<el-footer class="msg-footer"> <ChatMessage />
</div>
</el-scrollbar>
</el-main>
<!--聊天输入区和发送按钮等-->
<el-footer class="msg-footer">
<!--聊天输入选项--> <!--聊天输入选项-->
<div class="input-option"> <div class="input-option">
<el-icon <el-icon
@ -67,20 +73,16 @@
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
</el-footer> </el-footer>
</div> </div>
<div
class="no-message-container" <div class="no-message-container" v-else>
v-else <el-icon :size="180" color="#0078d7">
> <ChatDotSquare />
<el-icon </el-icon>
:size="180" <p>欢迎使用GodoOS</p>
color="#0078d7" </div>
>
<ChatDotSquare />
</el-icon>
<p>欢迎使用GodoOS</p>
</div>
</template> </template>
<style scoped> <style scoped>
.chatbox-main { .chatbox-main {
width: 100%; width: 100%;

127
frontend/src/components/chat/ChatMessage.vue

@ -1,19 +1,11 @@
<script setup lang="ts">
import { useChatStore } from "@/stores/chat";
const chatHistory = computed(() => store.chatHistory as any);
const store = useChatStore();
</script>
<template> <template>
<div <div
v-for="item in chatHistory" v-if="store.chatHistory.length > 0"
:key="item.id" v-for="item in store.chatHistory"
:key="item.time"
> >
<div <div
v-if="!item.isme" v-if="item.userId == store.userInfo.id"
class="chat-item" class="chat-item"
> >
<el-row> <el-row>
@ -22,20 +14,22 @@
<el-row> <el-row>
<el-col :span="24"> <el-col :span="24">
<div class="chat-name-me"> <div class="chat-name-me">
{{ item.userInfo.username }} {{ item.userInfo.nickname }}
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<div <div
class="bubble-me" class="bubble-me"
@contextmenu.prevent=" @contextmenu.prevent="
store.showContextMenu($event, item.id) store.showContextMenu($event, item.userId)
" "
> >
<div class="chat-font"> <div class="chat-font">
{{ item.content }} {{ item.message }}
</div> </div>
</div> </div>
<!-- 时间显示在消息框外 -->
<div class="chat-time">{{ formatTime(item.time) }}</div>
</el-col> </el-col>
<el-col :span="2"> <el-col :span="2">
<div class="chat-avatar"> <div class="chat-avatar">
@ -70,31 +64,24 @@
<el-row> <el-row>
<el-col :span="24"> <el-col :span="24">
<div class="chat-name-other"> <div class="chat-name-other">
{{ item.userInfo.username }} {{ item.userInfo.nickname }}
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<div class="bubble-other"> <div class="bubble-other">
<div class="chat-font"> <div class="chat-font">
{{ item.content }} {{ item.message }}
</div> </div>
</div> </div>
<!-- 时间显示在消息框外 -->
<div class="chat-time">{{ formatTime(item.time) }}</div>
</el-col> </el-col>
<el-col :span="8" /> <el-col :span="8" />
</el-row> </el-row>
</div> </div>
<div
v-if="item.type === 1"
class="withdraw"
>
{{
item.userInfo.id === store.targetUserId
? "你"
: item.userInfo.username
}}撤回了一条消息
</div>
</div> </div>
<!--悬浮菜单-->
<!-- 悬浮菜单 -->
<div <div
class="context-menu" class="context-menu"
v-if="store.contextMenu.visible" v-if="store.contextMenu.visible"
@ -118,6 +105,21 @@
</div> </div>
</template> </template>
<script setup lang="ts">
import { useChatStore } from "@/stores/chat";
const store = useChatStore();
const formatTime = (timestamp: number) => {
const date = new Date(timestamp);
const options: Intl.DateTimeFormatOptions = {
hour: "numeric",
minute: "numeric",
hour12: false,
};
return date.toLocaleString("default", options);
};
</script>
<style scoped> <style scoped>
.bubble-me { .bubble-me {
background-color: #95ec69; background-color: #95ec69;
@ -125,19 +127,7 @@
border-radius: 4px; border-radius: 4px;
margin-right: 5px; margin-right: 5px;
margin-top: 5px; margin-top: 5px;
} padding: 5px;
.bubble-me:hover {
background-color: #89d961;
}
.chat-name-me {
font-size: 14px;
font-family: Arial, sans-serif;
line-height: 1.5;
color: #b2b2b2;
float: right;
margin-right: 5px;
} }
.bubble-other { .bubble-other {
@ -146,19 +136,15 @@
border-radius: 4px; border-radius: 4px;
margin-left: 5px; margin-left: 5px;
margin-top: 5px; margin-top: 5px;
padding: 5px;
} }
.bubble-other:hover { .chat-name-me,
background-color: #ebebeb;
}
.chat-name-other { .chat-name-other {
font-size: 14px; font-size: 14px;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
line-height: 1.5; line-height: 1.5;
color: #b2b2b2; color: #b2b2b2;
float: left;
margin-left: 5px;
} }
.chat-font { .chat-font {
@ -168,43 +154,14 @@
line-height: 1.5; line-height: 1.5;
} }
.chat-avatar { .chat-time {
margin: 5px; font-size: 12px;
} color: #999;
height: 50px;
.chat-item { display: flex;
margin: 5px; align-items: self-end;
} justify-content: start;
padding-left: 10px;
.withdraw { text-align: left;
text-align: center;
font-size: 13px;
font-family: Arial, sans-serif;
color: #999999;
line-height: 3.2;
}
.context-menu {
position: fixed;
background-color: white;
z-index: 9999;
border: 1px solid #cccc;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.context-menu-item {
width: 80px;
height: 30px;
}
.context-menu-item:hover {
background-color: #e2e2e2;
}
.context-menu-item-font {
font-size: 14px;
text-align: center;
font-family: Arial, sans-serif;
line-height: 2.2;
} }
</style> </style>

14
frontend/src/components/chat/ChatMsgList.vue

@ -2,9 +2,9 @@
<div v-if="store.chatList.length > 0" v-for="item in store.chatList" :key="item.id"> <div v-if="store.chatList.length > 0" v-for="item in store.chatList" :key="item.id">
<div <div
class="list-item" class="list-item"
@click="store.changeChatList(item.id)" @click="store.changeChatList(item.userId)"
:style="{ :style="{
backgroundColor: item.id === store.targetUserId ? '#bae7ff' : '', backgroundColor: item.userId == store.targetUserId ? '#bae7ff' : '',
}" }"
> >
<el-row> <el-row>
@ -21,14 +21,8 @@
<el-col :span="18" class="preview-left"> <el-col :span="18" class="preview-left">
<div class="previewName">{{ item.nickname }}</div> <div class="previewName">{{ item.nickname }}</div>
<div class="previewChat"> <div class="previewChat">
<span v-if="item.previewType === 0">{{ item.previewMessage }}</span> <span>{{ item.previewMessage }}</span>
<span v-if="item.previewType === 1">
{{
item.targetUserId === id
? "你"
: '"' + item.nickname + '"'
}}撤回了一条消息
</span>
</div> </div>
</el-col> </el-col>
<el-col :span="6" class="preview-right"> <el-col :span="6" class="preview-right">

23
frontend/src/components/chat/ChatUserList.vue

@ -122,29 +122,6 @@
{{ group.name }} {{ group.name }}
</div> </div>
</el-col> </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-row>
</el-col> </el-col>
</el-row> </el-row>

4
frontend/src/components/chat/chatUserInfo.vue

@ -37,9 +37,9 @@
const store = useChatStore(); const store = useChatStore();
const sendMessage = (id:number) => { const sendMessage = (userId:number) => {
store.currentNavId = 0; store.currentNavId = 0;
store.updateConversationList(id) store.changeChatListAndGetChatHistory(userId)
}; };
</script> </script>

419
frontend/src/stores/chat.ts

@ -3,71 +3,64 @@ import { fetchPost, getSystemConfig } from '@/system/config';
import { notifyError } from "@/util/msg"; import { notifyError } from "@/util/msg";
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { db } from "./db"; import { db } from "./db";
import { useMessageStore } from "./message";
interface ChatMessage {
type: string;
createdAt: number;
content: any;
targetUserId: any;
previewType: 0 | 1; // 消息类型,0表示正常消息,1表示撤回消息
previewMessage: any;
isMe: boolean;
isRead: boolean;
userInfo: {
id: any;
username: any;
avatar: any;
};
}
// 发起群聊对话框显示
const groupChatDialogVisible = ref(false);
// 设置发起群聊对话框状态
const setGroupChatDialogVisible = (visible: boolean) => {
groupChatDialogVisible.value = visible;
};
export const useChatStore = defineStore('chatStore', () => { export const useChatStore = defineStore('chatStore', () => {
// 用户列表
const userList: any = ref([]);
// 定义聊天列表项的接口 interface ChatMessage {
interface ChatListItem { type: string; // 消息类型,0表示文字消息,1表示图片消息,2表示文件消息
time: number; // 消息发送时间
message: any; // 消息内容
userId: any; // 发送者id
toUserId: any; // 接收者id
// receiver: any; // 消息接收者
// to_groupid: any; // 群组id
userInfo: { // 发送者信息
}
};
// 发起群聊对话框显示
const groupChatInvitedDialogVisible = ref(false);
// 群信息设置抽屉
const groupInfoSettingDrawerVisible = ref(false);
// 设置群聊邀请对话框状态
const setGroupChatInvitedDialogVisible = (visible: boolean) => {
groupChatInvitedDialogVisible.value = visible;
};
// 设置群信息抽屉状态
const setGroupInfoDrawerVisible = (visible: boolean) => {
groupInfoSettingDrawerVisible.value = visible
}
// 定义用户类型
type User = {
id: number; id: number;
nickname: string; ip: string;
isOnline: boolean;
avatar: string; avatar: string;
previewTimeFormat: string; nickname: string;
previewType: 0 | 1; // 消息类型,0表示正常消息,1表示撤回消息 username: string;
previewMessage: string; updatedAt?: number;
} };
// 模拟数据 - 聊天列表 // 将 userList 的类型设置为 User[]
const chatList = ref<ChatListItem[]>([ const userList = ref<User[]>([]);
{
id: 2,
nickname: '朋友2',
avatar: '/logo.png',
previewTimeFormat: "昨天",
previewType: 1,
previewMessage: "测试消息",
},
{
id: 3,
nickname: '朋友2',
avatar: '/logo.png',
previewTimeFormat: "昨天",
previewType: 1,
previewMessage: "测试消息",
},
]); // 聊天列表
const chatList: any = ref([]);
// 聊天消息记录列表
const chatHistory: any = ref([]);
const targetNickname: any = ref('');
// 模拟数据 - 聊天消息列表
const chatHistory = ref<ChatMessage[]>([]);
// 群组数据 // 群组数据
const groupList = ref([ const groupList: any = ref([
{ {
id: 1, id: 1,
name: '群组1', name: '群组1',
@ -107,8 +100,6 @@ export const useChatStore = defineStore('chatStore', () => {
const targetUserInfo: any = ref({}); const targetUserInfo: any = ref({});
const targetUserId = ref(); const targetUserId = ref();
const search = ref(''); const search = ref('');
const messageStore = useMessageStore();
const apiUrl = "http://192.168.1.10:8816";
const contextMenu = ref({ const contextMenu = ref({
visible: false, visible: false,
@ -127,35 +118,40 @@ export const useChatStore = defineStore('chatStore', () => {
} }
userInfo.value = config.userInfo; userInfo.value = config.userInfo;
getUserList() getUserList()
initUserList() // initUserList()
initOnlineUserList() initChatList()
// initOnlineUserList()
console.log(userList.value); console.log(userList.value);
}; };
// 初始化用户列表
const initChatList = async () => {
chatList.value = await db.getAll('conversationList');
console.log(chatList.value);
};
const setCurrentNavId = (id: number) => { const setCurrentNavId = (id: number) => {
currentNavId.value = id; currentNavId.value = id;
}; };
const sendMessage = async () => { const sendMessage = async () => {
const chatSendUrl = apiUrl + '/chat/send'; const chatSendUrl = config.userInfo.url + '/chat/send';
// 封装成消息历史记录
const messageHistory: ChatMessage = { const messageHistory: ChatMessage = {
type: 'text', type: 'user',
createdAt: Date.now(), time: Date.now(),
content: message.value, message: message.value,
targetUserId: targetUserId.value, userId: userInfo.value.userId,
previewType: 0, // 消息类型,0表示正常消息,1表示撤回消息 toUserId: targetUserId.value,
previewMessage: message.value, // receiver: targetUserId.value,
isMe: true,
isRead: false,
userInfo: { userInfo: {
id: config.userInfo.id,
username: config.userInfo.username,
avatar: config.userInfo.avatar,
}, },
}; };
// 发送消息
const res = await fetchPost(chatSendUrl, messageHistory); const res = await fetchPost(chatSendUrl, messageHistory);
console.log(res);
if (res.ok) { if (res.ok) {
// 本地存储一份聊天记录 // 本地存储一份聊天记录
await db.addOne('chatRecord', messageHistory); await db.addOne('chatRecord', messageHistory);
@ -163,8 +159,10 @@ export const useChatStore = defineStore('chatStore', () => {
// 更新聊天历史 // 更新聊天历史
chatHistory.value.push(messageHistory); chatHistory.value.push(messageHistory);
console.log(chatHistory.value);
// 更新 chatList 和 conversationList // 更新 chatList 和 conversationList
await updateConversationList(targetUserId.value); // await changeChatListAndGetChatHistory(targetUserId.value, messageHistory);
// 清空输入框 // 清空输入框
clearMessage(); clearMessage();
@ -176,57 +174,97 @@ export const useChatStore = defineStore('chatStore', () => {
notifyError("消息发送失败"); notifyError("消息发送失败");
}; };
const updateConversationList = async (id: number) => { // 更新聊天和聊天记录
// 先判断是否已经存在该会话 const changeChatListAndChatHistory = async (data: any) => {
const res = await db.getRow('conversationList', 'id', id); try {
// 从 conversationList 数据库中查找是否存在对应的会话
const conversation = await db.getByField('conversationList', 'userId', data.userId);
if (res) { // 准备会话更新数据
// 更新现有会话
const updatedConversation = { const updatedConversation = {
...res, userId: data.userId,
previewMessage: message.value, avatar: data.userInfo.avatar || "logo.png", // 如果没有头像使用默认图片
previewTimeFormat: formatTime(Date.now()), toUserId: data.toUserId,
previewType: 0, messages: data.message,
nickname: data.userInfo.nickname,
time: data.time || Date.now(),
previewMessage: data.message,
previewTimeFormat: formatTime(Date.now()), // 时间格式化函数
createdAt: Date.now()
}; };
await db.update('conversationList', id, updatedConversation);
// 更新 chatList if (conversation.length === 0) {
const existingConversationIndex = chatList.value.findIndex(conversation => conversation.id === id); // 如果会话不存在,则添加到数据库和 chatList
if (existingConversationIndex !== -1) { await db.addOne('conversationList', updatedConversation);
chatList.value[existingConversationIndex] = updatedConversation;
} else {
chatList.value.push(updatedConversation); chatList.value.push(updatedConversation);
} else {
// 如果会话存在,则更新数据库和 chatList
// 只更新变化的字段,而非全部覆盖,以减少写入数据的量
await db.update('conversationList', conversation[0].id, {
avatar: data.userInfo.avatar || "logo.png",
nickname: data.userInfo.nickname,
previewMessage: data.message,
time: data.time || Date.now(),
previewTimeFormat: formatTime(Date.now())
});
// 更新 chatList 中的对应项
const existingConversationIndex = chatList.value.findIndex(
(conv: any) => conv.userId === data.userId
);
if (existingConversationIndex !== -1) {
chatList.value[existingConversationIndex] = updatedConversation;
} else {
chatList.value.push(updatedConversation);
}
} }
} else { } catch (error) {
const targetUser = await db.getOne('workbenchusers', id); console.error("更新聊天和聊天记录时出错:", error);
const lastMessage: any = messageStore; }
};
const targetUserInfo = {
id: targetUser.id,
nickname: targetUser.nickname,
avatar: targetUser.avatar,
};
// 计算时间差 const changeChatListAndGetChatHistory = async (userId: number) => {
const now = new Date(); // 将当前的chatlist存到map中id为key,如果userId能在map中找到就不添加。
const createdAt = new Date(lastMessage.createdAt);
const diffTime = Math.abs(now.getTime() - createdAt.getTime());
console.log(diffTime);
// 根据时间差格式化时间
const previewTimeFormat = formatTime(Date.now());
const newConversation = {
...targetUserInfo,
previewTimeFormat,
previewMessage: lastMessage.content,
previewType: lastMessage.type,
};
// 添加到 conversationList // 将当前的 chatList 存到 map 中,id 为 key
await db.addOne('conversationList', newConversation); const chatListMap = new Map<number, any>();
// value可设置为空
chatList.value.forEach((chat: { userId: number; }) => {
chatListMap.set(chat.userId, "");
});
// 检查 userId 是否在 map 中
if (chatListMap.has(userId)) {
// 如果存在获取聊天记录添加到historyList
const data = await db.getByField("chatRecord", "userId", userId)
chatHistory.value = data
return;
} else {
// 如果不在chatlist中表示没有聊天记录。那么去用户表中获取该用户的基本信息
const user = await db.getOne("workbenchusers", userId)
chatList.value.push({
id: user.id,
nickname: user.nickname,
avatar: user.avatar,
previewTimeFormat: formatTime(Date.now()),
previewMessage: "",
})
// 持久化
await db.addOne("conversationList", {
userId: user.id,
username: user.username,
nickname: user.nickname,
avatar: user.avatar,
toUserId: userInfo.value.id,
time: Date.now(),
previewMessage: "",
createdAt: Date.now()
})
// 添加到 chatList
chatList.value.push(newConversation);
} }
}; };
@ -255,6 +293,66 @@ export const useChatStore = defineStore('chatStore', () => {
}; };
// 创建群聊
const createGroupChat = async (groupName?: string, userIds?: number[]) => {
try {
const data = {
name: groupName,
user_ids: userIds
}
const url = config.userInfo.url + "/chat/group";
const res = await fetchPost(url, JSON.stringify(data));
if (!res.ok) {
return false;
}
console.log(res)
const groupData = await res.json();
console.log(groupData)
} catch (error) {
console.log(error);
}
};
// 处理用户消息
const userChatMessage = async (data: any) => {
// 先判断数据库是否有该用户
// 更新聊天记录表
// 更新会话列表数据库
// 更新chatlist
const isPresence = await db.getByField('workbenchusers', 'id', data.userId)
if (isPresence[0].id !== data.userId) {
return
}
// 添加消息记录
const addMessageHistory = {
type: data.type,
time: data.time,
userId: data.userId,
message: data.message,
toUserId: data.toUserId,
createdAt: Date.now(),
// 用户信息
userInfo: {
id: data.userId,
nickname: data.userInfo.nickname || "未知用户",
avatar: data.userInfo.avatar || "logo.png", // 使用默认头像。
email: data.userInfo.email,
phone: data.userInfo.phone,
remark: data.userInfo.remark,
role_id: data.userInfo.role_id,
}
}
await db.addOne('chatRecord', addMessageHistory)
// 更新 chatList 和 conversationList表
changeChatListAndChatHistory(data)
};
const setScrollToBottom = async () => { const setScrollToBottom = async () => {
// await nextTick(); // 确保 DOM 已经更新完毕 // await nextTick(); // 确保 DOM 已经更新完毕
@ -274,11 +372,8 @@ export const useChatStore = defineStore('chatStore', () => {
}; };
const handleUserData = async (data: any[]) => { const handleUserData = async (data: any[]) => {
;
// 创建一个用户数组,将所有在线的用户提取出来 // 创建一个用户数组,将所有在线的用户提取出来
const users: any[] = []; const users: any[] = [];
// 遍历每个数据项 // 遍历每个数据项
data.forEach((item: any) => { data.forEach((item: any) => {
if (item.id && item.login_ip) { if (item.id && item.login_ip) {
@ -292,7 +387,6 @@ export const useChatStore = defineStore('chatStore', () => {
} }
}); });
console.log(users);
// 将提取到的用户数据传递给 setUserList // 将提取到的用户数据传递给 setUserList
if (users.length > 0) { if (users.length > 0) {
@ -301,6 +395,7 @@ export const useChatStore = defineStore('chatStore', () => {
}; };
const setUserList = async (data: any[]) => { const setUserList = async (data: any[]) => {
if (data.length < 1) { if (data.length < 1) {
return; return;
@ -308,8 +403,8 @@ export const useChatStore = defineStore('chatStore', () => {
// 从当前用户列表中获取已有用户的 IP 和完整用户映射 // 从当前用户列表中获取已有用户的 IP 和完整用户映射
const existingIps = new Set(userList.value.map((d: any) => d.ip)); const existingIps = new Set(userList.value.map((d: any) => d.ip));
const userMap = new Map( const userMap = new Map<string, User>(
userList.value.map((user: any) => [user.ip, user]) userList.value.map((user: User) => [user.ip, user])
); );
const updates: any[] = []; const updates: any[] = [];
@ -324,8 +419,9 @@ export const useChatStore = defineStore('chatStore', () => {
key: existingUser.id, key: existingUser.id,
changes: { changes: {
isOnline: true, isOnline: true,
avatar: d.avatar,
nickname: d.nickname, nickname: d.nickname,
username: d.usernmae, username: d.username,
updatedAt: Date.now() updatedAt: Date.now()
} }
}); });
@ -335,6 +431,7 @@ export const useChatStore = defineStore('chatStore', () => {
id: d.id, id: d.id,
ip: d.ip, ip: d.ip,
isOnline: true, isOnline: true,
avatar: d.avatar,
nickname: d.nickname, nickname: d.nickname,
username: d.usernmae, username: d.usernmae,
createdAt: Date.now(), createdAt: Date.now(),
@ -343,8 +440,6 @@ export const useChatStore = defineStore('chatStore', () => {
} }
}); });
console.log(updates);
console.log(newEntries);
// 批量更新和添加用户数据 // 批量更新和添加用户数据
if (updates.length > 0) { if (updates.length > 0) {
@ -363,21 +458,20 @@ export const useChatStore = defineStore('chatStore', () => {
try { try {
// 从数据库中获取所有用户信息 // 从数据库中获取所有用户信息
const list = await db.getAll("workbenchusers"); const list = await db.getAll("workbenchusers");
console.log(list);
// 创建一个 Map,用于存储每个用户的唯一 ID 地址 // 创建一个 Map,用于存储每个用户的唯一 ID 地址
let uniqueIdMap = new Map<string, any>(); let uniqueIdMap = new Map<string, any>();
// 遍历用户列表,将每个用户添加到 Map 中(基于 ID 去重) // 遍历用户列表,将每个用户添加到 Map 中(基于 ID 去重)
list.forEach((item: any) => { list.forEach((item: any) => {
uniqueIdMap.set(item.id, item); // 使用 ID 作为键,用户对象作为值 uniqueIdMap.set(item.userId, item); // 使用 ID 作为键,用户对象作为值
}); });
// 将 Map 的值转换为数组(去重后的用户列表) // 将 Map 的值转换为数组(去重后的用户列表)
const uniqueIdList = Array.from(uniqueIdMap.values()); const uniqueIdList = Array.from(uniqueIdMap.values());
// 按照 updatedAt 时间进行升序排序 // 按照 updatedAt 时间进行升序排序
uniqueIdList.sort((a: any, b: any) => a.updatedAt - b.updatedAt); uniqueIdList.sort((a: any, b: any) => a.time - b.time);
// 更新用户列表 // 更新用户列表
userList.value = uniqueIdList; userList.value = uniqueIdList;
} catch (error) { } catch (error) {
@ -387,25 +481,29 @@ export const useChatStore = defineStore('chatStore', () => {
// 初始化统一用户列表状态 // 初始化统一用户列表状态
const initUserList = async () => { const initUserList = async () => {
// 检查用户列表是否为空 console.log("初始化用户列表状态");
if (userList.value.length > 0) { console.log(userList.value);
// 收集需要更新的用户数据 if (!userList.value.length) return;
const updates = userList.value // 获取需要更新的用户数据(只选取在线的用户并设为离线状态)
.filter((d: any) => d.isOnline) // 过滤出在线的用户 const updates = userList.value.reduce((acc: any[], user: any) => {
.map((d: any) => ({ console.log(user);
key: d.id, if (user.isOnline) {
changes: { console.log(user);
isOnline: false acc.push({
} key: user.id,
})); changes: { isOnline: false }
});
// 批量更新用户状态
if (updates.length > 0) {
await db.table('workbenchusers').bulkUpdate(updates);
} }
return acc;
}, []);
// 如果有需要更新的用户,批量更新数据库状态
if (updates.length) {
await db.table('workbenchusers').bulkUpdate(updates);
} }
}; };
const initOnlineUserList = async () => { const initOnlineUserList = async () => {
const msgAll = await db.getAll('workbenchusers'); const msgAll = await db.getAll('workbenchusers');
@ -438,20 +536,37 @@ export const useChatStore = defineStore('chatStore', () => {
}; };
const changeChatList = async (userId: number) => {
const changeChatList = async (id: number) => {
// 设置 targetUserId // 设置 targetUserId
targetUserId.value = id; targetUserId.value = userId;
// 获取当前用户和目标用户的聊天记录 // 获取当前用户和目标用户的聊天记录
const messagesList = await db.getByField('chatRecord', 'targetUserId', id); const messagesList = await db.getByField('chatRecord', 'userId', userId);
// // 表示与该用户没有聊天记录,但是也需要初始化一些信息
// if (messagesList.length == 0) {
// const userData:any = await db.getOne("workbenchusers", userId)
// console.log(userData)
// const data: any = {
// userId: userData.id,
// userInfo: {
// id: userData.id,
// avatar: userData.avatar,
// nickname: userData.nickname,
// }
// }
// chatHistory.value.push(data)
// } else {
chatHistory.value = messagesList; chatHistory.value = messagesList;
// }
// 设置目标用户的信息 // 设置目标用户的信息
setTargetUserInfo(id); await setTargetUserInfo(userId);
}; };
const setTargetUserInfo = async (id: number) => { const setTargetUserInfo = async (id: number) => {
targetUserInfo.value = await db.getOne('workbenchusers', id); targetUserInfo.value = await db.getOne('workbenchusers', id);
}; };
@ -487,16 +602,22 @@ export const useChatStore = defineStore('chatStore', () => {
message, message,
contextMenu, contextMenu,
activeNames, activeNames,
groupChatDialogVisible, groupChatInvitedDialogVisible,
groupInfoSettingDrawerVisible,
targetNickname,
initChat, initChat,
showContextMenu, showContextMenu,
setCurrentNavId, setCurrentNavId,
sendMessage, sendMessage,
changeChatList, changeChatList,
handleContextMenu, handleContextMenu,
updateConversationList, changeChatListAndGetChatHistory,
handleUserData, handleUserData,
initUserList, initUserList,
setGroupChatDialogVisible setGroupChatInvitedDialogVisible,
setGroupInfoDrawerVisible,
createGroupChat,
userChatMessage,
initOnlineUserList
}; };
}); });

23
frontend/src/stores/db.ts

@ -1,22 +1,31 @@
import Dexie from 'dexie'; import Dexie from 'dexie';
export type ChatTable = 'chatuser' | 'chatmsg' | 'chatmessage' | 'groupmessage' | 'chatRecord' | 'workbenchusers' | 'conversationList'; export type ChatTable = 'chatuser' | 'chatmsg' | 'chatmessage' | 'groupmessage' | 'chatRecord' | 'workbenchusers' | 'conversationList' | 'group';
export const dbInit: any = new Dexie('GodoOSDatabase'); export const dbInit: any = new Dexie('GodoOSDatabase');
dbInit.version(1).stores({ dbInit.version(1).stores({
// 用户列表 // 用户列表
workbenchusers: '++id,ip,userName,avatar,mobile,nickName,isOnline,updatedAt,createdAt', workbenchusers: '++id,ip,userName,avatar,mobile,nickName,isOnline,updatedAt,createdAt',
// 聊天记录 // 聊天记录
chatRecord: '++id,userId,targetUserId,senderInfo,previewType,previewMessage,isMe,isRead,content,type,readAt,createdAt', chatRecord: '++id,toUserId,messages,time,createdAt,userInfo',
// 会话列表 // 会话列表
conversationList: '++id,userId,targetUserId,targetIp,senderInfo,previewMessage,previewType,isMe,isRead,content,type,createdAt', conversationList: '++id,avatar,username,nickname,userId,toUserId,previewMessage,messages,time,createdAt',
chatuser: '++id,ip,hostname,userName,avatar,mobile,nickName,isOnline,updatedAt,createdAt', chatuser: '++id,ip,hostname,userName,avatar,mobile,nickName,isOnline,updatedAt,createdAt',
// chatmsg: '++id,targetUserId,targetIp,senderInfo,reciperInfo,previewMessage,previewType,content,type,status,isRead,isMe,readAt,createdAt', // chatmsg: '++id,toUserId,targetIp,senderInfo,reciperInfo,previewMessage,content,type,status,isRead,isMe,readAt,createdAt',
chatmessage: '++id,userId,targetUserId,senderInfo,isMe,isRead,content,type,readAt,createdAt', chatmessage: '++id,userId,toUserId,senderInfo,isMe,isRead,content,type,readAt,createdAt',
groupmessage: '++id,userId,groupId,senderInfo,isMe,isRead,content,type,readAt,createdAt', groupmessage: '++id,userId,groupId,senderInfo,isMe,isRead,content,type,readAt,createdAt',
}).upgrade((tx: { chatRecord: { addIndex: (arg0: string, arg1: (obj: { targetUserId: any; }) => any) => void; }; }) => { // 群组表
group: '++id,name,description,owner_id,avatar_url,created_at,updated_at,member_count,max_members,is_public,join_policy,group_type,status'
}).upgrade((tx: {
conversationList: any;
group: any;
chatRecord: { addIndex: (arg0: string, arg1: (obj: { toUserId: any; }) => any) => void; };
}) => {
// 手动添加索引 // 手动添加索引
tx.chatRecord.addIndex('targetUserId', (obj: { targetUserId: any; }) => obj.targetUserId); tx.conversationList.addIndex('userId', (obj: { userId: any; }) => obj.userId);
tx.chatRecord.addIndex('toUserId', (obj: { toUserId: any; }) => obj.toUserId);
tx.group.addIndex('owner_id', (obj: { owner_id: any }) => obj.owner_id); // 添加索引: 群主 ID
}); });
export const db = { export const db = {

18
frontend/src/stores/upgrade.ts

@ -67,6 +67,20 @@ export const useUpgradeStore = defineStore('upgradeStore', () => {
}; };
} }
// 获取用户聊天消息
function userChatMessage() {
const url = getUrl('/chat/message', false)
const source = new EventSource(url);
source.onmessage = function (event) {
const data = JSON.parse(event.data);
handleMessage(data);
};
source.onerror = function (event) {
console.error('EventSource error:', event);
};
}
async function handleMessage(message: any) { async function handleMessage(message: any) {
switch (message.type) { switch (message.type) {
@ -79,6 +93,9 @@ export const useUpgradeStore = defineStore('upgradeStore', () => {
case 'online': case 'online':
chatChatStore.handleUserData(message.data) chatChatStore.handleUserData(message.data)
break; break;
case 'user':
chatChatStore.userChatMessage
break
default: default:
console.warn('Unknown message type:', message.type); console.warn('Unknown message type:', message.type);
} }
@ -180,6 +197,7 @@ export const useUpgradeStore = defineStore('upgradeStore', () => {
checkUpdate, checkUpdate,
systemMessage, systemMessage,
onlineMessage, onlineMessage,
userChatMessage,
update update
} }
}) })

5
frontend/src/system/index.ts

@ -14,7 +14,6 @@ import {
} from './type/type'; } from './type/type';
import { BrowserWindow, BrowserWindowOption } from './window/BrowserWindow'; import { BrowserWindow, BrowserWindowOption } from './window/BrowserWindow';
import { useMessageStore } from '@/stores/message';
import { useUpgradeStore } from '@/stores/upgrade'; import { useUpgradeStore } from '@/stores/upgrade';
import { RestartApp } from '@/util/goutil'; import { RestartApp } from '@/util/goutil';
import { notifyError } from '@/util/msg'; import { notifyError } from '@/util/msg';
@ -435,7 +434,7 @@ export class System {
const fileContent = await this.fs.readShareFile(path) const fileContent = await this.fs.readShareFile(path)
// console.log('阅读:', fileContent); // console.log('阅读:', fileContent);
if (fileContent !== false) { if (fileContent !== false) {
const fileName = extname(arr[arr.length-1] || '') || 'link' const fileName = extname(arr[arr.length - 1] || '') || 'link'
this._flieOpenerMap this._flieOpenerMap
.get(fileName) .get(fileName)
?.func.call(this, path, fileContent || ''); ?.func.call(this, path, fileContent || '');
@ -461,7 +460,7 @@ export class System {
?.func.call(this, path, fileContent || ''); ?.func.call(this, path, fileContent || '');
} }
} }
} }
// 插件系统 // 插件系统
use(func: OsPlugin): void { use(func: OsPlugin): void {

28
godo/files/pwdfile.go

@ -6,14 +6,14 @@ import (
"godo/libs" "godo/libs"
"net/http" "net/http"
"strconv" "strconv"
"strings"
) )
// 加密读
func HandleReadFile(w http.ResponseWriter, r *http.Request) { func HandleReadFile(w http.ResponseWriter, r *http.Request) {
// 初始值 // 初始值
path := r.URL.Query().Get("path") path := r.URL.Query().Get("path")
fPwd := r.Header.Get("fPwd") fPwd := r.Header.Get("filePwd")
salt := GetSalt(r) salt := GetSalt(r)
hasPwd, err := GetPwdFlag() hasPwd, err := GetPwdFlag()
if err != nil { if err != nil {
@ -38,12 +38,14 @@ func HandleReadFile(w http.ResponseWriter, r *http.Request) {
libs.HTTPError(w, http.StatusNotFound, err.Error()) libs.HTTPError(w, http.StatusNotFound, err.Error())
return return
} }
fileData := string(fileContent)
// 无加密情况 // 无加密情况
if !hasPwd { if !hasPwd {
// 直接base64编码原文返回 // 判断文件开头是否以link:开头
data := base64.StdEncoding.EncodeToString(fileContent) if !strings.HasPrefix(fileData, "link::") {
resp := libs.APIResponse{Message: "success", Data: data} fileData = base64.StdEncoding.EncodeToString(fileContent)
}
resp := libs.APIResponse{Message: "success", Data: fileData}
json.NewEncoder(w).Encode(resp) json.NewEncoder(w).Encode(resp)
return return
} }
@ -63,10 +65,14 @@ func HandleReadFile(w http.ResponseWriter, r *http.Request) {
} }
// 3. base64编码后返回 // 3. base64编码后返回
content := base64.StdEncoding.EncodeToString(fileContent) // 判断文件开头是否以link:开头
fileData = string(fileContent)
if !strings.HasPrefix(fileData, "link::") {
fileData = base64.StdEncoding.EncodeToString(fileContent)
}
// 初始响应 // 初始响应
res := libs.APIResponse{Code: 0, Message: "success", Data: content} res := libs.APIResponse{Code: 0, Message: "success", Data: fileData}
json.NewEncoder(w).Encode(res) json.NewEncoder(w).Encode(res)
} }
@ -116,7 +122,11 @@ func HandleChangeFilePwd(w http.ResponseWriter, r *http.Request) {
func HandleSetIsPwd(w http.ResponseWriter, r *http.Request) { func HandleSetIsPwd(w http.ResponseWriter, r *http.Request) {
isPwd := r.URL.Query().Get("ispwd") isPwd := r.URL.Query().Get("ispwd")
// 0非加密机器 1加密机器 // 0非加密机器 1加密机器
isPwdValue, _ := strconv.Atoi(isPwd) isPwdValue, err := strconv.Atoi(isPwd)
if err != nil {
libs.HTTPError(w, http.StatusBadRequest, err.Error())
return
}
var isPwdBool bool var isPwdBool bool
if isPwdValue == 0 { if isPwdValue == 0 {
isPwdBool = false isPwdBool = false

Loading…
Cancel
Save