From 5cbb35f659665615efe555d668d92c4d72b417bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=8B=E7=94=B0=E5=BC=98?= <1240092443@qq.com> Date: Tue, 29 Oct 2024 09:21:52 +0800 Subject: [PATCH 1/4] =?UTF-8?q?add:=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/auto-imports.d.ts | 3 +- frontend/components.d.ts | 1 + frontend/src/components/chat/Chat.vue | 231 ++++++---- frontend/src/components/chat/ChatBox.vue | 341 +++++++------- frontend/src/components/chat/ChatMenu.vue | 8 +- frontend/src/components/chat/ChatMessage.vue | 26 +- frontend/src/components/chat/ChatMsgList.vue | 199 +++++--- frontend/src/components/chat/ChatUserList.vue | 268 ++++++++--- .../src/components/chat/ChatUserSetting.vue | 1 - frontend/src/components/chat/chatUserInfo.vue | 79 ++++ frontend/src/stores/chat.ts | 435 ++++++++++++++---- frontend/src/stores/db.ts | 39 +- 12 files changed, 1150 insertions(+), 481 deletions(-) create mode 100644 frontend/src/components/chat/chatUserInfo.vue diff --git a/frontend/auto-imports.d.ts b/frontend/auto-imports.d.ts index eab6be6..d0c217f 100644 --- a/frontend/auto-imports.d.ts +++ b/frontend/auto-imports.d.ts @@ -3,6 +3,7 @@ // @ts-nocheck // noinspection JSUnusedGlobalSymbols // Generated by unplugin-auto-import +// biome-ignore lint: disable export {} declare global { const EffectScope: typeof import('vue')['EffectScope'] @@ -70,6 +71,6 @@ declare global { // for type re-export declare global { // @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') } diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 35ff343..819d793 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -26,6 +26,7 @@ declare module 'vue' { ChatMessage: typeof import('./src/components/chat/ChatMessage.vue')['default'] ChatMsgList: typeof import('./src/components/chat/ChatMsgList.vue')['default'] ChatNav: typeof import('./src/components/localchat/ChatNav.vue')['default'] + ChatUserInfo: typeof import('./src/components/chat/chatUserInfo.vue')['default'] ChatUserList: typeof import('./src/components/chat/ChatUserList.vue')['default'] ChatUserSetting: typeof import('./src/components/chat/ChatUserSetting.vue')['default'] ChatWorkList: typeof import('./src/components/chat/ChatWorkList.vue')['default'] diff --git a/frontend/src/components/chat/Chat.vue b/frontend/src/components/chat/Chat.vue index 4f8eab5..058407d 100644 --- a/frontend/src/components/chat/Chat.vue +++ b/frontend/src/components/chat/Chat.vue @@ -1,115 +1,144 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file + diff --git a/frontend/src/components/chat/ChatBox.vue b/frontend/src/components/chat/ChatBox.vue index 4d9fc64..318e13c 100644 --- a/frontend/src/components/chat/ChatBox.vue +++ b/frontend/src/components/chat/ChatBox.vue @@ -1,173 +1,204 @@ - - - - {{ store.targetUserInfo.username }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 暂无内容 - + + + + {{ store.targetUserInfo.username }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 欢迎使用GodoOS + \ No newline at end of file + .no-message-container { + height: 100%; + margin: 120px auto; + text-align: center; + justify-content: center; + } + diff --git a/frontend/src/components/chat/ChatMenu.vue b/frontend/src/components/chat/ChatMenu.vue index aa86def..2439900 100644 --- a/frontend/src/components/chat/ChatMenu.vue +++ b/frontend/src/components/chat/ChatMenu.vue @@ -3,6 +3,10 @@ import {ref} from "vue"; import { useChatStore } from "@/stores/chat"; import {Setting as SettingIcon} from "@element-plus/icons-vue"; const store = useChatStore() +const getOnline = () => { + store.getOnlineUsers() + store.setCurrentNavId(1) +} @@ -17,7 +21,7 @@ const store = useChatStore() - + @@ -59,7 +63,7 @@ const store = useChatStore() .menu-icon-on { margin: 20px auto; font-size: 25px; - color: #07C160; + color: #0078d4; cursor: pointer; } diff --git a/frontend/src/components/chat/ChatMessage.vue b/frontend/src/components/chat/ChatMessage.vue index 12db80e..264ada4 100644 --- a/frontend/src/components/chat/ChatMessage.vue +++ b/frontend/src/components/chat/ChatMessage.vue @@ -1,13 +1,11 @@ - - - - + + @@ -30,10 +28,7 @@ const store = useChatStore() - - - - + @@ -56,19 +51,16 @@ const store = useChatStore() - - - + {{ item.userInfo.id === store.targetUserId ? "你" : item.userInfo.username }}撤回了一条消息 - - - - {{ item.label }} + + + {{ contextItem.label }} @@ -163,4 +155,4 @@ const store = useChatStore() font-family: Arial, sans-serif; line-height: 2.2; } - + \ No newline at end of file diff --git a/frontend/src/components/chat/ChatMsgList.vue b/frontend/src/components/chat/ChatMsgList.vue index 41693d7..00c6ed9 100644 --- a/frontend/src/components/chat/ChatMsgList.vue +++ b/frontend/src/components/chat/ChatMsgList.vue @@ -1,71 +1,158 @@ - - - - + + - + - - - - {{ item.name }} + + + + {{ item.nickname }} + + {{ item.previewMessage }} + + {{ + item.targetUserId === id + ? "你" + : '"' + item.nickname + '"' + }}撤回了一条消息 + + - - {{ item.previewTimeFormat }} + + + {{ item.previewTimeFormat }} + - - {{ item.previewMessage }} - {{ item.userId === id ? "你" : "\"" + item.name + - "\""}}撤回了一条消息 - + + + + + 暂无数据 + + + diff --git a/frontend/src/components/chat/ChatUserList.vue b/frontend/src/components/chat/ChatUserList.vue index 30629a8..9b4fa52 100644 --- a/frontend/src/components/chat/ChatUserList.vue +++ b/frontend/src/components/chat/ChatUserList.vue @@ -1,71 +1,221 @@ - - - - - - - - - - {{ item.name }} - - - {{ item.previewTimeFormat }} - - - - {{ item.previewMessage }} - {{ item.userId === id ? "你" : "\"" + item.name + - "\""}}撤回了一条消息 - - - - - + + + + 同事({{store.userList.length}}) + 同事 + + + + + + + + + + + + + {{ item.nickname }} + + + + + + + + + + + + {{ item.ip }} + + + + + + + + + 暂无数据 + + + + + 部门({{store.groupList.length}}) + 部门 + + + + + + + + + + + + + {{ group.name }} + + + + + {{ group.previewTimeFormat }} + + + + + + {{ group.previewMessage }} + + + {{ + group.userId === id + ? "你" + : '"' + group.name + '"' + }}撤回了一条消息 + + + + + + + + + 暂无数据 + + + + .no-data { + text-align: center; + color: #999999; + } + \ No newline at end of file diff --git a/frontend/src/components/chat/ChatUserSetting.vue b/frontend/src/components/chat/ChatUserSetting.vue index ff34fc6..24018fc 100644 --- a/frontend/src/components/chat/ChatUserSetting.vue +++ b/frontend/src/components/chat/ChatUserSetting.vue @@ -13,7 +13,6 @@ const onSubmit = () => { - diff --git a/frontend/src/components/chat/chatUserInfo.vue b/frontend/src/components/chat/chatUserInfo.vue new file mode 100644 index 0000000..c6beb3e --- /dev/null +++ b/frontend/src/components/chat/chatUserInfo.vue @@ -0,0 +1,79 @@ + + + + + + 欢迎使用GodoOS + + + + 昵称: {{ store.targetUserInfo.nickname }} + 邮箱: {{ store.targetUserInfo.email }} + 电话: {{ store.targetUserInfo.phone }} + 描述: {{ store.targetUserInfo.desc }} + 工号: {{ store.targetUserInfo.jobNumber }} + 工作地点: {{ store.targetUserInfo.workPlace }} + 入职日期: {{ store.targetUserInfo.hiredDate }} + + 发送消息 + + + + + + diff --git a/frontend/src/stores/chat.ts b/frontend/src/stores/chat.ts index 2d4a8d4..f1fe69e 100644 --- a/frontend/src/stores/chat.ts +++ b/frontend/src/stores/chat.ts @@ -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([ + { + 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; } -}) \ No newline at end of file + + 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 + }; +}); \ No newline at end of file diff --git a/frontend/src/stores/db.ts b/frontend/src/stores/db.ts index aa9dc14..57779dd 100644 --- a/frontend/src/stores/db.ts +++ b/frontend/src/stores/db.ts @@ -1,13 +1,22 @@ -import Dexie from 'dexie' +import Dexie from 'dexie'; -export type ChatTable = 'chatuser' | 'chatmsg' | 'chatmessage' | 'groupmessage' +export type ChatTable = 'chatuser' | 'chatmsg' | 'chatmessage' | 'groupmessage' | 'chatRecord' | 'workbenchusers' | 'conversationList'; -export const dbInit:any = new Dexie('GodoOSDatabase'); +export const dbInit: any = new Dexie('GodoOSDatabase'); dbInit.version(1).stores({ - chatuser:'++id,ip,hostname,userName,avatar,mobile,nickName,isOnline,updatedAt,createdAt', - chatmsg:'++id,targetId,targetIp,senderInfo,reciperInfo,content,type,status,isRead,isMe,readAt,createdAt', - chatmessage:'++id,userId,toUserId,senderInfo,isMe,isRead,content,type,readAt,createdAt', - groupmessage:'++id,userId,groupId,senderInfo,isMe,isRead,content,type,readAt,createdAt' + // 用户列表 + workbenchusers: '++id,ip,userName,avatar,mobile,nickName,isOnline,updatedAt,createdAt', + // 聊天记录 + chatRecord: '++id,userId,targetUserId,senderInfo,previewType,previewMessage,isMe,isRead,content,type,readAt,createdAt', + // 会话列表 + conversationList: '++id,userId,targetUserId,targetIp,senderInfo,previewMessage,previewType,isMe,isRead,content,type,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', + chatmessage: '++id,userId,targetUserId,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; }; }) => { + // 手动添加索引 + tx.chatRecord.addIndex('targetUserId', (obj: { targetUserId: any; }) => obj.targetUserId); }); export const db = { @@ -55,7 +64,7 @@ export const db = { .limit(size) .toArray(); }, - async filter(tableName: ChatTable, filterFunc : any) { + async filter(tableName: ChatTable, filterFunc: any) { return dbInit[tableName].filter(filterFunc).toArray() }, table(tableName: ChatTable) { @@ -64,26 +73,28 @@ export const db = { async getOne(tableName: ChatTable, Id: number) { return dbInit[tableName].get(Id) }, - async getRow(tableName: ChatTable, fieldName: string, val: any){ + async getRow(tableName: ChatTable, fieldName: string, val: any) { return dbInit[tableName].where(fieldName).equals(val).first() }, - async get(tableName: ChatTable, whereObj : any) { + async get(tableName: ChatTable, whereObj: any) { //console.log(whereObj) const data = await dbInit[tableName].where(whereObj).first() //console.log(data) - return data? data : false + return data ? data : false }, async rows(tableName: ChatTable, whereObj: any) { return dbInit[tableName].where(whereObj).toArray() }, + async field(tableName: ChatTable, whereObj: any, field: string) { const data = await this.get(tableName, whereObj) return data ? data[field] : false }, - async getValue(tableName: ChatTable, fieldName: string, val: any, fName : string) { + async getValue(tableName: ChatTable, fieldName: string, val: any, fName: string) { const row = await this.getRow(tableName, fieldName, val); return row[fName] }, + async getByField(tableName: ChatTable, fieldName: string, val: any) { return dbInit[tableName].where(fieldName).equals(val).toArray() }, @@ -105,6 +116,10 @@ export const db = { async deleteByField(tableName: ChatTable, fieldName: string, val: any) { return dbInit[tableName].where(fieldName).equals(val).delete() }, + // 获取创建时间最近的记录 + async getLatest(tableName: ChatTable, fieldName: string, val: any) { + return dbInit[tableName].where(fieldName).equals(val).reverse().first() + }, async clear(tableName: ChatTable) { return dbInit[tableName].clear() }, From 6696374dd44951f69d3f102812d3ebc3a4eb399e Mon Sep 17 00:00:00 2001 From: godo Date: Tue, 29 Oct 2024 09:28:37 +0800 Subject: [PATCH 2/4] change the filelist --- frontend/src/components/builtin/FileList.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/builtin/FileList.vue b/frontend/src/components/builtin/FileList.vue index ca6f374..8220aff 100644 --- a/frontend/src/components/builtin/FileList.vue +++ b/frontend/src/components/builtin/FileList.vue @@ -34,7 +34,7 @@ @dragleave="handleDragLeave()" @dragstart.stop="startDragApp($event, item)" @click="handleClick(index)" @mousedown.stop :ref="(ref: any) => { if (ref) { - appPositions[indeíx] = markRaw(ref as Element); + appPositions[index] = markRaw(ref as Element); } } "> From cac7c8e803ce51a37ebf4a18aa529f66b31db3f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=AD=90=E6=97=BA?= <15039612+liu-ziwang123@user.noreply.gitee.com> Date: Tue, 29 Oct 2024 14:29:51 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E5=B8=A6=E5=AF=86=E7=A0=81=E8=AF=BB?= =?UTF-8?q?=E5=86=99=E6=96=87=E4=BB=B6=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- godo/cmd/main.go | 1 + godo/files/fs.go | 106 +++++++++--------------------------------- godo/files/os.go | 29 ++++++++---- godo/files/pwdfile.go | 102 ++++++++++++++++++++++++++++++++++++++++ godo/libs/encode.go | 65 ++++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 93 deletions(-) create mode 100644 godo/files/pwdfile.go diff --git a/godo/cmd/main.go b/godo/cmd/main.go index b0ea857..c10e000 100644 --- a/godo/cmd/main.go +++ b/godo/cmd/main.go @@ -100,6 +100,7 @@ func OsStart() { fileRouter.HandleFunc("/zip", files.HandleZip).Methods(http.MethodGet) fileRouter.HandleFunc("/unzip", files.HandleUnZip).Methods(http.MethodGet) fileRouter.HandleFunc("/watch", files.WatchHandler).Methods(http.MethodGet) + fileRouter.HandleFunc("/setfilepwd", files.HandleSetFilePwd).Methods(http.MethodGet) localchatRouter := router.PathPrefix("/localchat").Subrouter() localchatRouter.HandleFunc("/message", localchat.HandleMessage).Methods(http.MethodPost) diff --git a/godo/files/fs.go b/godo/files/fs.go index 4719135..d5408a5 100644 --- a/godo/files/fs.go +++ b/godo/files/fs.go @@ -24,9 +24,6 @@ package files import ( - "crypto/md5" - "encoding/base64" - "encoding/hex" "encoding/json" "fmt" "godo/libs" @@ -164,58 +161,6 @@ func HandleExists(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(res) } -// HandleReadFile reads a file's content -func HandleReadFile(w http.ResponseWriter, r *http.Request) { - path := r.URL.Query().Get("path") - fpwd := r.Header.Get("fpwd") - haspwd := IsHavePwd(fpwd) - // 校验文件路径 - if err := validateFilePath(path); err != nil { - libs.HTTPError(w, http.StatusBadRequest, err.Error()) - 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 - } - content := string(fileContent) - // 检查文件内容是否以"link::"开头 - if !strings.HasPrefix(content, "link::") { - content = base64.StdEncoding.EncodeToString(fileContent) - } - - // 初始响应 - res := libs.APIResponse{Code: 0, Message: "success"} - switch haspwd { - case true: - // 有密码检验密码 - isreal := CheckFilePwd(fpwd) - // 密码正确返回原文,否则返回加密文本 - if isreal { - res.Data = content - } else { - data, err := libs.EncryptData(fileContent, libs.EncryptionKey) - if err != nil { - libs.HTTPError(w, http.StatusInternalServerError, err.Error()) - return - } - res.Data = base64.StdEncoding.EncodeToString(data) - } - case false: - res.Data = content - } - - json.NewEncoder(w).Encode(res) -} - // HandleUnlink removes a file func HandleUnlink(w http.ResponseWriter, r *http.Request) { path := r.URL.Query().Get("path") @@ -372,6 +317,7 @@ func HandleCopyFile(w http.ResponseWriter, r *http.Request) { // HandleWriteFile writes content to a file func HandleWriteFile(w http.ResponseWriter, r *http.Request) { + // basepath = "/Users/sujia/.godoos/os" filePath := r.URL.Query().Get("filePath") basePath, err := libs.GetOsDir() if err != nil { @@ -386,8 +332,13 @@ func HandleWriteFile(w http.ResponseWriter, r *http.Request) { return } defer fileContent.Close() - // 输出到控制台进行调试 - //fmt.Printf("Body content: %v\n", fileContent) + filedata, err := io.ReadAll(fileContent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // 创建文件 file, err := os.Create(filepath.Join(basePath, filePath)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -395,15 +346,25 @@ func HandleWriteFile(w http.ResponseWriter, r *http.Request) { } defer file.Close() - _, err = io.Copy(file, fileContent) + // 内容为空直接返回,不为空则加密 + if len(filedata) == 0 { + CheckAddDesktop(filePath) + libs.SuccessMsg(w, "", "success") + return + } + // 加密 + data, err := libs.EncryptData(filedata, libs.EncryptionKey) if err != nil { - http.Error(w, err.Error(), http.StatusConflict) + http.Error(w, err.Error(), http.StatusInternalServerError) return } - err = CheckAddDesktop(filePath) + _, err = file.Write(data) if err != nil { - log.Printf("Error adding file to desktop: %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return } + // 判断下是否添加到桌面上 + CheckAddDesktop(filePath) res := libs.APIResponse{Message: fmt.Sprintf("File '%s' successfully written.", filePath)} json.NewEncoder(w).Encode(res) } @@ -547,26 +508,3 @@ func HandleDesktop(w http.ResponseWriter, r *http.Request) { } libs.SuccessMsg(w, rootInfo, "success") } - -// 设置文件密码 -func HandleSetFilePwd(w http.ResponseWriter, r *http.Request) { - fpwd := r.Header.Get("filepwd") - // 密码最长16位 - if fpwd == "" || len(fpwd) > 16 { - libs.ErrorMsg(w, "密码长度为空或者过长,最长为16位") - return - } - // 服务端存储 - req := libs.ReqBody{ - Name: "filepwd", - Value: fpwd, - } - libs.SetConfig(req) - // 客户端加密 - mhash := md5.New() - mhash.Write([]byte(fpwd)) - v := mhash.Sum(nil) - pwdstr := hex.EncodeToString(v) - res := libs.APIResponse{Message: "success", Data: pwdstr} - json.NewEncoder(w).Encode(res) -} diff --git a/godo/files/os.go b/godo/files/os.go index 11efc8c..320a240 100644 --- a/godo/files/os.go +++ b/godo/files/os.go @@ -24,12 +24,11 @@ package files import ( - "crypto/md5" - "encoding/hex" "fmt" "godo/libs" "io" "io/fs" + "net/http" "os" "path/filepath" "strings" @@ -338,13 +337,13 @@ func CheckDeleteDesktop(filePath string) error { } // 校验文件密码 -func CheckFilePwd(fpwd string) bool { - mhash := md5.New() - mhash.Write([]byte(fpwd)) - v := mhash.Sum(nil) - pwdstr := hex.EncodeToString(v) - oldpwd, _ := libs.GetConfig("filepwd") - return oldpwd == pwdstr +func CheckFilePwd(fpwd, salt string) bool { + pwd := libs.HashPassword(fpwd, salt) + oldpwd, err := libs.GetConfig("filepwd") + if !err { + return false + } + return oldpwd == pwd } func IsHavePwd(pwd string) bool { @@ -354,3 +353,15 @@ func IsHavePwd(pwd string) bool { return false } } + +// salt值优先从server端获取,如果没有则从header获取 +func GetSalt(r *http.Request) string { + data, ishas := libs.GetConfig("salt") + salt := data.(string) + if ishas { + return salt + } else { + salt = r.Header.Get("salt") + return salt + } +} diff --git a/godo/files/pwdfile.go b/godo/files/pwdfile.go new file mode 100644 index 0000000..0d2c1b1 --- /dev/null +++ b/godo/files/pwdfile.go @@ -0,0 +1,102 @@ +package files + +import ( + "crypto/md5" + "encoding/base64" + "encoding/hex" + "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("filepwd") + salt := r.Header.Get("salt") + // 密码最长16位 + if fpwd == "" || len(fpwd) > 16 { + libs.ErrorMsg(w, "密码长度为空或者过长,最长为16位") + return + } + // md5加密 + mhash := md5.New() + mhash.Write([]byte(fpwd)) + v := mhash.Sum(nil) + pwdstr := hex.EncodeToString(v) + + // 服务端再hash加密 + hashpwd := libs.HashPassword(pwdstr, 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: "success", Data: pwdstr} + json.NewEncoder(w).Encode(res) +} diff --git a/godo/libs/encode.go b/godo/libs/encode.go index 3ff3b03..007b98a 100644 --- a/godo/libs/encode.go +++ b/godo/libs/encode.go @@ -7,6 +7,8 @@ import ( "crypto/hmac" "crypto/rand" "crypto/sha256" + "encoding/base64" + "errors" "io" ) @@ -20,6 +22,19 @@ func pkcs7Pad(data []byte, blockSize int) []byte { return append(data, padtext...) } +// pkcs7Unpad 移除 PKCS#7 填充 +func pkcs7Unpad(data []byte) []byte { + length := len(data) + if length == 0 { + return data + } + padding := int(data[length-1]) // 将 padding 转换为 int 类型 + if padding > aes.BlockSize || padding < 1 { + return data + } + return data[:length-padding] +} + func EncryptData(data []byte, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { @@ -50,3 +65,53 @@ func EncryptData(data []byte, key []byte) ([]byte, error) { return result, nil } + +// DecryptData 使用 AES 解密数据,并验证 HMAC-SHA256 签名 +func DecryptData(ciphertext []byte, key []byte) ([]byte, error) { + // 检查 HMAC-SHA256 签名 + expectedMacSize := sha256.Size + if len(ciphertext) < expectedMacSize { + return nil, errors.New("ciphertext too short") + } + + macSum := ciphertext[len(ciphertext)-expectedMacSize:] + ciphertext = ciphertext[:len(ciphertext)-expectedMacSize] + + // 验证 HMAC-SHA256 签名 + mac := hmac.New(sha256.New, key) + mac.Write(ciphertext) + calculatedMac := mac.Sum(nil) + + if !hmac.Equal(macSum, calculatedMac) { + return nil, errors.New("invalid MAC") + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // 检查 IV 的长度 + if len(ciphertext) < aes.BlockSize { + return nil, errors.New("ciphertext too short") + } + + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + + // 使用 CBC 模式解密数据 + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(ciphertext, ciphertext) + + // 移除 PKCS#7 填充 + unpaddedData := pkcs7Unpad(ciphertext) + + return unpaddedData, nil +} + +// 哈希加密 +func HashPassword(password, salt string) string { + hash := sha256.New() + hash.Write([]byte(password + salt)) + return base64.URLEncoding.EncodeToString(hash.Sum(nil)) +} From 83b8587a71ea944bebe8cbffc95dddea2fde535b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=AD=90=E6=97=BA?= <15039612+liu-ziwang123@user.noreply.gitee.com> Date: Tue, 29 Oct 2024 16:00:39 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=9B=B4=E6=94=B9=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E6=8E=A5=E5=8F=A3=E5=92=8C=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E4=BA=86=E4=B8=80=E4=BA=9B=E5=8F=98=E9=87=8F=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- godo/files/pwdfile.go | 48 +++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/godo/files/pwdfile.go b/godo/files/pwdfile.go index 0d2c1b1..a52fbda 100644 --- a/godo/files/pwdfile.go +++ b/godo/files/pwdfile.go @@ -1,9 +1,7 @@ package files import ( - "crypto/md5" "encoding/base64" - "encoding/hex" "encoding/json" "godo/libs" "net/http" @@ -14,8 +12,8 @@ import ( func HandleReadFile(w http.ResponseWriter, r *http.Request) { path := r.URL.Query().Get("path") - fpwd := r.Header.Get("fpwd") - haspwd := IsHavePwd(fpwd) + fPwd := r.Header.Get("fPwd") + hasPwd := IsHavePwd(fPwd) // 获取salt值 salt := GetSalt(r) @@ -27,8 +25,8 @@ func HandleReadFile(w http.ResponseWriter, r *http.Request) { } // 有密码校验密码 - if haspwd { - if !CheckFilePwd(fpwd, salt) { + if hasPwd { + if !CheckFilePwd(fPwd, salt) { libs.HTTPError(w, http.StatusBadRequest, "密码错误") return } @@ -68,26 +66,15 @@ func HandleReadFile(w http.ResponseWriter, r *http.Request) { // 设置文件密码 func HandleSetFilePwd(w http.ResponseWriter, r *http.Request) { - fpwd := r.Header.Get("filepwd") + fPwd := r.Header.Get("filepPwd") salt := r.Header.Get("salt") - // 密码最长16位 - if fpwd == "" || len(fpwd) > 16 { - libs.ErrorMsg(w, "密码长度为空或者过长,最长为16位") - return - } - // md5加密 - mhash := md5.New() - mhash.Write([]byte(fpwd)) - v := mhash.Sum(nil) - pwdstr := hex.EncodeToString(v) - // 服务端再hash加密 - hashpwd := libs.HashPassword(pwdstr, salt) + hashPwd := libs.HashPassword(fPwd, salt) // 服务端存储 req := libs.ReqBody{ - Name: "filepwd", - Value: hashpwd, + Name: "filePwd", + Value: hashPwd, } libs.SetConfig(req) @@ -97,6 +84,23 @@ func HandleSetFilePwd(w http.ResponseWriter, r *http.Request) { Value: salt, } libs.SetConfig(reqSalt) - res := libs.APIResponse{Message: "success", Data: pwdstr} + 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!") +}
暂无内容
欢迎使用GodoOS
暂无数据
昵称: {{ store.targetUserInfo.nickname }}
邮箱: {{ store.targetUserInfo.email }}
电话: {{ store.targetUserInfo.phone }}
描述: {{ store.targetUserInfo.desc }}
工号: {{ store.targetUserInfo.jobNumber }}
工作地点: {{ store.targetUserInfo.workPlace }}
入职日期: {{ store.targetUserInfo.hiredDate }}