prr 7 months ago
parent
commit
a93d3b4462
  1. 3
      frontend/auto-imports.d.ts
  2. 1
      frontend/components.d.ts
  3. 231
      frontend/src/components/chat/Chat.vue
  4. 341
      frontend/src/components/chat/ChatBox.vue
  5. 8
      frontend/src/components/chat/ChatMenu.vue
  6. 26
      frontend/src/components/chat/ChatMessage.vue
  7. 199
      frontend/src/components/chat/ChatMsgList.vue
  8. 268
      frontend/src/components/chat/ChatUserList.vue
  9. 1
      frontend/src/components/chat/ChatUserSetting.vue
  10. 79
      frontend/src/components/chat/chatUserInfo.vue
  11. 435
      frontend/src/stores/chat.ts
  12. 39
      frontend/src/stores/db.ts
  13. 1
      godo/cmd/main.go
  14. 106
      godo/files/fs.go
  15. 29
      godo/files/os.go
  16. 106
      godo/files/pwdfile.go
  17. 65
      godo/libs/encode.go

3
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')
}

1
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']

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

@ -1,115 +1,144 @@
<script setup lang="ts">
import { useChatStore } from '@/stores/chat';
import { Search } from '@element-plus/icons-vue';
import {getWorkflowUrl} from '@/system/config'
const store = useChatStore()
const workUrl = getWorkflowUrl()
onMounted(() => {
store.initChat()
})
import { useChatStore } from "@/stores/chat";
import { getWorkflowUrl } from "@/system/config";
import { Search } from "@element-plus/icons-vue";
const store = useChatStore();
const workUrl = getWorkflowUrl();
onMounted(() => {
store.initChat();
// store.initSSE();
});
</script>
<template>
<el-container class="container">
<!--菜单-->
<el-aside class="menu">
<chat-menu />
</el-aside>
<el-container class="side" v-if="store.currentNavId < 3">
<!--搜索栏-->
<el-header class="search" v-if="store.currentNavId < 2">
<el-input placeholder="搜索" :prefix-icon="Search" class="search-input" v-model="store.search" />
</el-header>
<!--好友列表-->
<el-main class="list">
<el-scrollbar>
<chat-msg-list v-if="store.currentNavId == 0" />
<chat-user-list v-if="store.currentNavId == 1" />
<!-- <chat-work-list v-if="store.currentNavId == 2" /> -->
</el-scrollbar>
</el-main>
</el-container>
<el-container class="chat-box">
<chat-box v-if="store.currentNavId < 2" />
</el-container>
<el-container class="chat-setting" v-if="store.currentNavId == 2">
<iframe class="workflow" :src="workUrl"></iframe>
</el-container>
<el-container class="chat-setting" v-if="store.currentNavId == 5">
<ChatUserSetting />
</el-container>
</el-container>
<el-container class="container">
<!--菜单-->
<el-aside class="menu">
<chat-menu />
</el-aside>
<el-container
class="side"
v-if="store.currentNavId < 3"
>
<!--搜索栏-->
<el-header
class="search"
v-if="store.currentNavId < 2"
>
<el-input
placeholder="搜索"
:prefix-icon="Search"
class="search-input"
v-model="store.search"
/>
</el-header>
<!--好友列表-->
<el-main class="list">
<el-scrollbar>
<chat-msg-list v-if="store.currentNavId == 0" />
<chat-user-list v-if="store.currentNavId == 1" />
<!-- <chat-work-list v-if="store.currentNavId == 2" /> -->
</el-scrollbar>
</el-main>
</el-container>
<el-container class="chat-box">
<chat-box v-if="store.currentNavId < 1" />
<chat-user-info v-if="store.currentNavId ==1"></chat-user-info>
</el-container>
<el-container
class="chat-setting"
v-if="store.currentNavId == 2"
>
<iframe
class="workflow"
:src="workUrl"
></iframe>
</el-container>
<el-container
class="chat-setting"
v-if="store.currentNavId == 5"
>
<ChatUserSetting />
</el-container>
</el-container>
</template>
<style scoped>
.container {
display: flex;
height: 100%;
width: 100%;
overflow-y: hidden;
overflow-x: hidden;
}
.container {
display: flex;
height: 100%;
width: 100%;
overflow-y: hidden;
overflow-x: hidden;
}
.menu {
width: 55px;
background-color: #2E2E2E;
overflow-y: hidden;
overflow-x: hidden;
-webkit-app-region: drag;
}
.menu {
width: 55px;
.side {
flex: 1;
/* 占据剩余宽度 */
max-height: max-content;
border-right: 1px solid #edebeb;
overflow-y: hidden;
overflow-x: hidden;
background-color: #F7F7F7;
}
background-color: white;
overflow-y: hidden;
overflow-x: hidden;
-webkit-app-region: drag;
}
.search {
width: 100%;
/* 占据整个宽度 */
.side {
flex: 1;
/* 占据剩余宽度 */
max-height: max-content;
border-right: 1px solid #edebeb;
overflow-y: hidden;
overflow-x: hidden;
background-color: #f7f7f7;
}
height: 50px;
padding: 0;
-webkit-app-region: drag;
}
.search {
width: 100%;
/* 占据整个宽度 */
height: 50px;
padding: 0;
-webkit-app-region: drag;
}
.search-input {
width: calc(100% - 20px);
/* 减去左右边距 */
margin: 10px;
-webkit-app-region: no-drag;
--el-input-placeholder-color: #818181 !important;
--el-input-icon-color: #5D5D5D !important;
}
.search-input {
width: calc(100% - 20px);
/* 减去左右边距 */
margin: 10px;
-webkit-app-region: no-drag;
--el-input-placeholder-color: #818181 !important;
--el-input-icon-color: #5d5d5d !important;
}
.list {
width: 100%;
/* 占据整个宽度 */
padding: 0;
overflow-y: hidden;
overflow-x: hidden;
}
.list {
width: 100%;
/* 占据整个宽度 */
padding: 0;
overflow-y: hidden;
overflow-x: hidden;
}
.chat-box {
flex: 3;
/* 占据剩余宽度的三倍 */
max-height: max-content;
background-color: #F5F5F5;
}
.chat-box {
flex: 3;
/* 占据剩余宽度的三倍 */
max-height: max-content;
background-color: #f5f5f5;
}
.chat-setting {
width: calc(100% - 65px);
/* 占据整个宽度 */
height: 100%;
overflow: hidden;
}
.workflow {
width: 100%;
height: 100%;
object-fit: contain;
border: none;
.chat-setting {
width: calc(100% - 65px);
/* 占据整个宽度 */
height: 100%;
overflow: hidden;
}
.workflow {
width: 100%;
height: 100%;
object-fit: contain;
border: none;
}
.no-message-container {
height: 100%;
margin: 120px auto;
text-align: center;
font-size:14px;
justify-content: center;
}
</style>
</style>

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

@ -1,173 +1,204 @@
<script setup lang="ts">
import { useChatStore } from '@/stores/chat';
const store = useChatStore()
import { useChatStore } from "@/stores/chat";
const store = useChatStore();
</script>
<template>
<div class="chatbox-main" v-if="store.targetUserId > 0">
<!--聊天顶部区-->
<el-header class="chat-header">
<div class="header-title">{{ store.targetUserInfo.username }}</div>
</el-header>
<!--聊天主体区-->
<el-main class="msg-main">
<el-scrollbar ref="store.scrollbarRef">
<div ref="store.innerRef">
<ChatMessage />
</div>
</el-scrollbar>
</el-main>
<el-footer class="msg-footer">
<!--聊天输入选项-->
<div class="input-option">
<el-icon :size="20" class="input-option-icon">
<Picture />
</el-icon>
<el-icon :size="20" class="input-option-icon">
<Link />
</el-icon>
<el-icon :size="20" class="input-option-icon">
<Delete />
</el-icon>
</div>
<!--聊天输入区-->
<div class="input-textarea">
<el-input type="textarea" maxlength="1000" resize="none" class="textarea"
@keyup.enter.exact="store.sendMessage" v-model="store.message" />
</div>
<!--聊天发送按钮-->
<el-tooltip placement="top" content="按enter键发送,按ctrl+enter键换行">
<el-icon :size="22" class="input-button">
<Promotion />
</el-icon>
</el-tooltip>
</el-footer>
</div>
<div class="no-message-container" v-else>
<el-icon :size="180" color="#0078d7">
<ScaleToOriginal />
</el-icon>
<p>暂无内容</p>
</div>
<div
class="chatbox-main"
v-if="store.targetUserId > 0"
>
<!--聊天顶部区-->
<el-header class="chat-header">
<div class="header-title">{{ store.targetUserInfo.username }}</div>
</el-header>
<!--聊天主体区-->
<el-main class="msg-main">
<el-scrollbar ref="store.scrollbarRef">
<div ref="store.innerRef">
<ChatMessage />
</div>
</el-scrollbar>
</el-main>
<el-footer class="msg-footer">
<!--聊天输入选项-->
<div class="input-option">
<el-icon
:size="20"
class="input-option-icon"
>
<Picture />
</el-icon>
<el-icon
:size="20"
class="input-option-icon"
>
<Link />
</el-icon>
<el-icon
:size="20"
class="input-option-icon"
>
<Delete />
</el-icon>
</div>
<!--聊天输入区-->
<div class="input-textarea">
<el-input
type="textarea"
maxlength="1000"
resize="none"
class="textarea"
@keyup.enter.exact="store.sendMessage"
v-model="store.message"
/>
</div>
<!--聊天发送按钮-->
<el-tooltip
placement="top"
content="按enter键发送,按ctrl+enter键换行"
>
<el-icon
@click="store.sendMessage"
:size="22"
class="input-button"
>
<Promotion />
</el-icon>
</el-tooltip>
</el-footer>
</div>
<div
class="no-message-container"
v-else
>
<el-icon
:size="180"
color="#0078d7"
>
<ChatDotSquare />
</el-icon>
<p>欢迎使用GodoOS</p>
</div>
</template>
<style scoped>
.chatbox-main {
width: 100%;
height: 100%;
}
.chatbox-main {
width: 100%;
height: 100%;
}
.chat-header {
width: 100%;
/* 占据整个宽度 */
height: 50px;
line-height: 50px;
padding: 0;
-webkit-app-region: drag;
}
.chat-header {
width: 100%;
/* 占据整个宽度 */
height: 50px;
line-height: 50px;
padding: 0;
-webkit-app-region: drag;
}
header-title {
font-size: 20px;
text-align: left;
margin-left: 15px;
}
header-title {
font-size: 20px;
text-align: left;
margin-left: 15px;
}
.msg-main {
width: 100%;
/* 占据整个宽度 */
height: calc(70% - 50px);
padding: 0;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-width: 0;
border-right-width: 0;
border-color: #D6D6D6;
border-style: solid;
}
.msg-main {
width: 100%;
/* 占据整个宽度 */
height: calc(70% - 50px);
padding: 0;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-width: 0;
border-right-width: 0;
border-color: #d6d6d6;
border-style: solid;
}
.msg-footer {
width: 100%;
/* 占据整个宽度 */
height: 30%;
padding: 0;
}
.msg-footer {
width: 100%;
/* 占据整个宽度 */
height: 30%;
padding: 0;
}
.input-option {
height: 20px;
padding: 5px;
}
.input-option {
height: 20px;
padding: 5px;
}
.input-option-icon {
color: #494949;
margin-left: 20px;
margin-top: 5px;
cursor: pointer;
}
.input-option-icon {
color: #494949;
margin-left: 20px;
margin-top: 5px;
cursor: pointer;
}
.input-textarea {
height: calc(100% - 50px);
width: calc(100% - 20px);
/* 减去左右边距 */
margin: 10px;
}
.input-textarea {
height: calc(100% - 50px);
width: calc(100% - 20px);
/* 减去左右边距 */
margin: 10px;
}
.textarea {
font-size: 16px;
font-family: Arial, sans-serif;
line-height: 1.5;
width: 100%;
height: 100%;
overflow-y: hidden;
overflow-x: hidden;
--el-input-border-radius: 0;
--el-input-border-color: transparent;
--el-input-hover-border-color: transparent;
--el-input-clear-hover-color: transparent;
--el-input-focus-border-color: transparent;
}
.textarea {
font-size: 16px;
font-family: Arial, sans-serif;
line-height: 1.5;
width: 100%;
height: 100%;
overflow-y: hidden;
overflow-x: hidden;
--el-input-border-radius: 0;
--el-input-border-color: transparent;
--el-input-hover-border-color: transparent;
--el-input-clear-hover-color: transparent;
--el-input-focus-border-color: transparent;
}
.input-button {
position: absolute;
bottom: 12px;
right: 12px;
width: 30px;
/* 缩小宽度 */
height: 30px;
/* 减小高度 */
border-radius: 50%;
/* 较小的圆角 */
background-color: #E8F0FE;
/* 浅蓝色,符合Win11的轻量风格 */
color: #0078D4;
/* 使用Win11的强调色作为文字颜色 */
font-weight: bold;
border: 1px solid #B3D4FC;
/* 添加边框,保持简洁风格 */
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
/* 轻微阴影 */
transition: all 0.2s ease;
/* 快速过渡效果 */
}
.input-button {
position: absolute;
bottom: 12px;
right: 12px;
width: 30px;
/* 缩小宽度 */
height: 30px;
/* 减小高度 */
border-radius: 50%;
/* 较小的圆角 */
background-color: #e8f0fe;
/* 浅蓝色,符合Win11的轻量风格 */
color: #0078d4;
/* 使用Win11的强调色作为文字颜色 */
font-weight: bold;
border: 1px solid #b3d4fc;
/* 添加边框,保持简洁风格 */
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
/* 轻微阴影 */
transition: all 0.2s ease;
/* 快速过渡效果 */
}
.input-button:hover {
background-color: #D1E4FF;
/* 悬浮时颜色略深,保持浅色调 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
/* 稍微增强阴影 */
}
.input-button:hover {
background-color: #d1e4ff;
/* 悬浮时颜色略深,保持浅色调 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
/* 稍微增强阴影 */
}
.input-button:active {
background-color: #B3D4FC;
/* 按下时颜色更深,但依然保持清新 */
box-shadow: 0 1px 2px rgba(0, 0, 0.1);
/* 回复初始阴影 */
transform: translateY(1px);
/* 微小下移,模拟按下 */
}
.input-button:active {
background-color: #b3d4fc;
/* 按下时颜色更深,但依然保持清新 */
box-shadow: 0 1px 2px rgba(0, 0, 0.1);
/* 回复初始阴影 */
transform: translateY(1px);
/* 微小下移,模拟按下 */
}
.no-message-container {
height: 100%;
margin: 120px auto;
text-align: center;
justify-content: center;
}
</style>
.no-message-container {
height: 100%;
margin: 120px auto;
text-align: center;
justify-content: center;
}
</style>

8
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)
}
</script>
<template>
@ -17,7 +21,7 @@ const store = useChatStore()
<ChatRound />
</el-icon>
</el-row>
<el-row @click="store.setCurrentNavId(1)">
<el-row @click="getOnline">
<el-icon v-if="store.currentNavId === 1" class="menu-icon-on">
<UserFilled />
</el-icon>
@ -59,7 +63,7 @@ const store = useChatStore()
.menu-icon-on {
margin: 20px auto;
font-size: 25px;
color: #07C160;
color: #0078d4;
cursor: pointer;
}

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

@ -1,13 +1,11 @@
<script setup lang="ts">
import { useChatStore } from '@/stores/chat';
const store = useChatStore()
const store = useChatStore();
</script>
<template>
<div v-for="item in store.msgList">
<!--聊天记录-用户-->
<div v-if="item.userInfo.id === store.targetUserId">
<div class="chat-item">
<div v-for="item in store.chatHistory" :key="item.id">
<div v-if="!item.isme" class="chat-item">
<el-row>
<el-col :span="8" />
<el-col :span="14">
@ -30,10 +28,7 @@ const store = useChatStore()
</el-col>
</el-row>
</div>
</div>
<!--聊天记录-好友-->
<div v-else>
<div class="chat-item">
<div v-else class="chat-item">
<el-row>
<el-col :span="2">
<div class="chat-avatar">
@ -56,19 +51,16 @@ const store = useChatStore()
<el-col :span="8" />
</el-row>
</div>
</div>
<div v-if="item.type === 1">
<div class="withdraw">
<div v-if="item.type === 1" class="withdraw">
{{ item.userInfo.id === store.targetUserId ? "你" : item.userInfo.username }}撤回了一条消息
</div>
</div>
</div>
<!--悬浮菜单-->
<div class="context-menu" v-if="store.contextMenu.visible"
:style="{ top: `${store.contextMenu.y}px`, left: `${store.contextMenu.x}px` }">
<div v-for="item in store.contextMenu.list" class="context-menu-item">
<div class="context-menu-item-font" @click="store.handleContextMenu()">
{{ item.label }}
<div v-for="contextItem in store.contextMenu.list" :key="contextItem.id" class="context-menu-item">
<div class="context-menu-item-font" @click="store.handleContextMenu(contextItem)">
{{ contextItem.label }}
</div>
</div>
</div>
@ -163,4 +155,4 @@ const store = useChatStore()
font-family: Arial, sans-serif;
line-height: 2.2;
}
</style>
</style>

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

@ -1,71 +1,158 @@
<script setup>
import { ref } from "vue";
import { useChatStore } from "@/stores/chat";
const store = useChatStore()
</script>
<template>
<div v-for="item in store.chatList">
<div class="list-item" @click="store.changeChatList(item)"
:style="{ backgroundColor: item.id === store.targetUserId ? '#C4C4C4' : '' }">
<div v-if="store.chatList.length > 0" v-for="item in store.chatList" :key="item.id">
<div
class="list-item"
@click="store.changeChatList(item.id)"
:style="{
backgroundColor: item.id === store.targetUserId ? '#C4C4C4' : '',
}"
>
<el-row>
<el-col :span="6">
<el-avatar shape="square" :size="40" class="avatar" :src="item.avatar" />
<el-avatar
shape="square"
:size="40"
class="avatar"
:src="item.avatar"
/>
</el-col>
<el-col :span="18">
<el-row>
<el-col :span="18">
<div class="previewName">{{ item.name }}</div>
<el-col :span="18" class="preview">
<el-row class="preview-content">
<el-col :span="18" class="preview-left">
<div class="previewName">{{ item.nickname }}</div>
<div class="previewChat">
<span v-if="item.previewType === 0">{{ item.previewMessage }}</span>
<span v-if="item.previewType === 1">
{{
item.targetUserId === id
? "你"
: '"' + item.nickname + '"'
}}撤回了一条消息
</span>
</div>
</el-col>
<el-col :span="6">
<div class="previewTime">{{ item.previewTimeFormat }}</div>
<el-col :span="6" class="preview-right">
<div class="previewTime">
{{ item.previewTimeFormat }}
</div>
</el-col>
</el-row>
<el-row>
<div v-if="item.previewType === 0" class="previewChat">{{ item.previewMessage }}</div>
<div v-if="item.previewType === 1" class="previewChat">{{ item.userId === id ? "" : "\"" + item.name +
"\""}}撤回了一条消息</div>
</el-row>
</el-col>
</el-row>
</div>
</div>
<div v-else class="emptyChat">
<el-icon :size="60" class="chat-icon">
<ChatSquare />
</el-icon>
<p class="empty-message">暂无数据</p>
</div>
</template>
<script setup>
import { useChatStore } from "@/stores/chat";
import { ref } from "vue";
const store = useChatStore();
const id = ref("1");
</script>
<style scoped>
.list-item {
width: 100%;
height: 60px;
}
.list-item:hover {
background-color: #D0D0D0;
}
.avatar {
margin: 10px
}
.previewName {
margin-top: 10px;
font-size: 15px;
font-family: Arial, sans-serif;
line-height: 1.5;
}
.previewChat {
font-size: 14px;
font-family: Arial, sans-serif;
color: #999999;
}
.previewTime {
float: right;
margin-top: 10px;
margin-right: 10px;
font-size: 12px;
font-family: Arial, sans-serif;
color: #999999;
}
.list-item {
width: 100%;
height: 60px;
display: flex;
}
.list-item:hover {
background-color: #d0d0d0;
}
.avatar {
margin: 10px;
}
.preview {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
}
.preview-content {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.preview-left {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
.previewName {
margin-left: 10px;
font-size: 12px;
font-family: Arial, sans-serif;
line-height: 1.5;
overflow: hidden; /* 隐藏超出部分 */
text-overflow: ellipsis; /* 显示为省略号 */
white-space: nowrap; /* 不换行 */
min-width: 100px; /* 最小宽度 */
max-width: 100%; /* 最大宽度 */
}
.previewChat {
height: 20px;
margin-left: 10px;
font-size: 10px;
font-family: Arial, sans-serif;
color: #999999;
overflow: hidden; /* 隐藏超出部分 */
text-overflow: ellipsis; /* 显示为省略号 */
white-space: nowrap; /* 不换行 */
min-width: 90px; /* 最小宽度 */
max-width: 100%; /* 最大宽度 */
}
.preview-right {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.previewTime {
font-size: 12px;
font-family: Arial, sans-serif;
color: #999999;
min-width: 30px; /* 最小宽度 */
max-width: 100%; /* 最大宽度 */
}
.emptyChat {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
margin: 120px auto;
text-align: center;
font-size: 14px;
}
.chat-icon {
color: #0078d4;
margin-bottom: 20px;
}
.empty-message {
font-size: 16px;
color: #666666;
}
</style>

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

@ -1,71 +1,221 @@
<script setup>
import { ref } from "vue";
import { useChatStore } from "@/stores/chat";
const store = useChatStore()
import { useChatStore } from "@/stores/chat";
import {
ElAvatar,
ElCol,
ElCollapse,
ElCollapseItem,
ElRow,
} from "element-plus";
const store = useChatStore();
</script>
<template>
<div v-for="item in store.chatList">
<div class="list-item" @click="store.changeChatList(item)"
:style="{ backgroundColor: item.id === store.targetUserId ? '#C4C4C4' : '' }">
<el-row>
<el-col :span="6">
<el-avatar shape="square" :size="40" class="avatar" :src="item.avatar" />
</el-col>
<el-col :span="18">
<el-row>
<el-col :span="18">
<div class="previewName">{{ item.name }}</div>
</el-col>
<el-col :span="6">
<div class="previewTime">{{ item.previewTimeFormat }}</div>
</el-col>
</el-row>
<el-row>
<div v-if="item.previewType === 0" class="previewChat">{{ item.previewMessage }}</div>
<div v-if="item.previewType === 1" class="previewChat">{{ item.userId === id ? "" : "\"" + item.name +
"\""}}撤回了一条消息</div>
</el-row>
</el-col>
</el-row>
</div>
</div>
<el-collapse
v-model="store.activeNames"
@change="handleChange"
>
<el-collapse-item name="1">
<template #title>
<span v-if="store.userList.length > 0" class="title">同事{{store.userList.length}}</span>
<span v-else class="title">同事</span>
</template>
<div v-if="store.userList.length > 0">
<div
v-for="item in store.userList"
:key="item.id"
>
<div
class="list-item"
@click="store.changeChatList(item.id)"
:style="{
backgroundColor:
item.id === store.targetUserId ? '#C4C4C4' : '',
}"
>
<el-row>
<el-col :span="6">
<el-avatar
shape="square"
:size="40"
class="avatar"
:src="item.avatar"
/>
</el-col>
<el-col :span="18">
<el-row>
<el-col :span="18">
<div class="previewName">
{{ item.nickname }}
<el-icon class="online-icon" v-if="item.isOnline">
<CircleCheckFilled />
</el-icon>
</div>
</el-col>
<el-col :span="6">
<!-- 空白占位 -->
</el-col>
</el-row>
<el-row>
<div class="previewIP">
{{ item.ip }}
</div>
</el-row>
</el-col>
</el-row>
</div>
</div>
</div>
<div v-else>
<p class="no-data">暂无数据</p>
</div>
</el-collapse-item>
<el-collapse-item name="2">
<template #title>
<span v-if="store.groupList.length > 0" class="title">部门{{store.groupList.length}}</span>
<span v-else class="title">部门</span>
</template>
<div v-if="store.groupList.length > 0">
<div
v-for="group in store.groupList"
:key="group.id"
>
<div
class="list-item"
@click="store.changeGroupList(group)"
:style="{
backgroundColor:
group.id === store.targetGroupId
? '#C4C4C4'
: '',
}"
>
<el-row>
<el-col :span="6">
<el-avatar
shape="square"
:size="40"
class="avatar"
:src="group.avatar"
/>
</el-col>
<el-col :span="18">
<el-row>
<el-col :span="18">
<div class="previewName">
{{ group.name }}
</div>
</el-col>
<el-col :span="6">
<div class="previewTime">
{{ group.previewTimeFormat }}
</div>
</el-col>
</el-row>
<el-row>
<div
v-if="group.previewType === 0"
class="previewChat"
>
{{ group.previewMessage }}
</div>
<div
v-if="group.previewType === 1"
class="previewChat"
>
{{
group.userId === id
? "你"
: '"' + group.name + '"'
}}撤回了一条消息
</div>
</el-row>
</el-col>
</el-row>
</div>
</div>
</div>
<div v-else>
<p class="no-data">暂无数据</p>
</div>
</el-collapse-item>
</el-collapse>
</template>
<style scoped>
.list-item {
width: 240px;
height: 60px;
}
.title {
padding-left: 10px;
}
.list-item {
width: 100%;
height: 60px;
border: 0.5px solid #d0d0d0;
}
.list-item:hover {
background-color: #d0d0d0;
}
.avatar {
margin: 10px;
}
.previewName {
margin-left: 10px;
font-size: 14px;
font-family: Arial, sans-serif;
line-height: 1.5;
overflow: hidden; /* 隐藏超出部分 */
text-overflow: ellipsis; /* 显示为省略号 */
white-space: nowrap; /* 不换行 */
max-width: 100%; /* 最大宽度 */
display: flex; /* 使用 Flexbox */
align-items: center; /* 垂直居中 */
}
/* 为了使内部的 el-row 垂直居中,我们也需要设置其父级元素 */
.el-row {
display: flex;
align-items: center;
}
.list-item:hover {
background-color: #D0D0D0;
}
.previewIP {
margin-left: 10px;
font-size: 12px;
font-family: Arial, sans-serif;
color: #999999;
display: flex; /* 使用 Flexbox */
align-items: center; /* 垂直居中 */
}
.avatar {
margin: 10px
}
.online-icon {
font-size: 16px; /* 调整图标大小 */
color: green; /* 在线状态颜色 */
margin-left: 5px; /* 与用户名之间的间距 */
}
.previewName {
margin-top: 10px;
font-size: 15px;
font-family: Arial, sans-serif;
line-height: 1.5;
}
.previewChat {
margin-left: 10px;
font-size: 12px; /* 调整字体大小 */
font-family: Arial, sans-serif;
color: #999999;
overflow: hidden; /* 隐藏超出部分 */
text-overflow: ellipsis; /* 显示为省略号 */
white-space: nowrap; /* 不换行 */
max-width: 100%; /* 最大宽度 */
}
.previewChat {
font-size: 14px;
font-family: Arial, sans-serif;
color: #999999;
}
.previewTime {
float: right;
margin-top: 10px;
margin-right: 10px;
font-size: 12px;
font-family: Arial, sans-serif;
color: #999999;
}
.previewTime {
float: right;
margin-top: 10px;
margin-right: 10px;
font-size: 12px;
font-family: Arial, sans-serif;
color: #999999;
}
</style>
.no-data {
text-align: center;
color: #999999;
}
</style>

1
frontend/src/components/chat/ChatUserSetting.vue

@ -13,7 +13,6 @@ const onSubmit = () => {
</script>
<template>
<el-form :model="form" label-width="160px" style="padding: 30px;">
<el-form-item label="头像">
<el-avatar shape="square" :size="80" :src="store.userInfo.avatar"/>
</el-form-item>

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

@ -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>

435
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<ChatListItem[]>([
{
id: 2,
nickname: '朋友2',
avatar: '/logo.png',
previewTimeFormat: "昨天",
previewType: 1,
previewMessage: "测试消息",
},
]);
// 模拟数据 - 聊天消息列表
const chatHistory = ref([]);
// 群组数据
const groupList = ref([
{
id: 1,
name: '群组1',
avatar: '/logo.png',
previewTimeFormat: "今天",
previewType: 0,
previewMessage: "这是一个示例消息。",
},
{
id: 2,
name: '群组2',
avatar: '/logo.png',
previewTimeFormat: "今天",
previewType: 0,
previewMessage: "这是一个示例消息。",
},
{
id: 3,
name: '群组3',
avatar: '/logo.png',
previewTimeFormat: "今天",
previewType: 0,
previewMessage: "这是一个示例消息。",
}
const setCurrentNavId = (id: number) => {
currentNavId.value = id
]);
const activeNames = ref([]);
const userInfo: any = ref({});
const showChooseFile = ref(false);
const currentPage = ref(1);
const pageSize = ref(50);
const innerRef = ref(null);
const scrollbarRef = ref(null);
const config = getSystemConfig();
const currentNavId = ref(0);
const message: any = ref('');
const targetUserInfo: any = ref({});
const targetUserId = ref();
const search = ref('');
const messageStore = useMessageStore();
const apiUrl = "http://192.168.1.10:8816";
const contextMenu = ref({
visible: false,
chatMessageId: 0,
list: [
{
id: 2,
label: '撤回',
}
],
x: 0,
y: 0
});
const initChat = () => {
if (config.userInfo.avatar == '') {
config.userInfo.avatar = '/logo.png';
}
const sendMessage = async () => {
await setScrollToBottom()
userInfo.value = config.userInfo;
};
const setCurrentNavId = (id: number) => {
currentNavId.value = id;
};
const sendMessage = async () => {
const chatSendUrl = apiUrl + '/chat/send';
const messageHistory = {
type: 'text',
createdAt: Date.now(),
content: message.value,
targetUserId: targetUserId.value,
previewType: 0, // 消息类型,0表示正常消息,1表示撤回消息
previewMessage: message.value,
isMe: true,
isRead: false,
userInfo: {
id: config.userInfo.id,
username: config.userInfo.username,
avatar: config.userInfo.avatar,
},
};
const res = await fetchPost(chatSendUrl, messageHistory);
if (res.ok) {
// 本地存储一份聊天记录
await db.addOne('chatRecord', messageHistory);
// 更新聊天历史
chatHistory.value.push(messageHistory);
// 更新 chatList 和 conversationList
await updateConversationList(targetUserId.value);
// 清空输入框
clearMessage();
// 聊天框滚动到底部
await setScrollToBottom();
return;
}
const setScrollToBottom = async () => {
await nextTick()
const max = innerRef.value.clientHeight
scrollbarRef.value.setScrollTop(max)
notifyError("消息发送失败");
};
const updateConversationList = async (id: number) => {
// 先判断是否已经存在该会话
const res = await db.getRow('conversationList', 'id', id);
if (res) {
// 更新现有会话
const updatedConversation = {
...res,
previewMessage: message.value,
previewTimeFormat: formatTime(Date.now()),
previewType: 0,
};
await db.update('conversationList', id, updatedConversation);
// 更新 chatList
const existingConversationIndex = chatList.value.findIndex(conversation => conversation.id === id);
if (existingConversationIndex !== -1) {
chatList.value[existingConversationIndex] = updatedConversation;
} else {
chatList.value.push(updatedConversation);
}
} else {
const targetUser = await db.getOne('workbenchusers', id);
const lastMessage = messageHistory;
const targetUserInfo = {
id: targetUser.id,
nickname: targetUser.nickname,
avatar: targetUser.avatar,
};
// 计算时间差
const now = new Date();
const createdAt = new Date(lastMessage.createdAt);
const diffTime = Math.abs(now.getTime() - createdAt.getTime());
// 根据时间差格式化时间
const previewTimeFormat = formatTime(Date.now());
const newConversation = {
...targetUserInfo,
previewTimeFormat,
previewMessage: lastMessage.content,
previewType: lastMessage.type,
};
// 添加到 conversationList
await db.addOne('conversationList', newConversation);
// 添加到 chatList
chatList.value.push(newConversation);
}
const changeChatList = async () => {
// const res = await getChatList(id)
// chatList.value = res.data
};
const formatTime = (timestamp: number): string => {
const now = new Date();
const createdAt = new Date(timestamp);
const diffTime = Math.abs(now.getTime() - createdAt.getTime());
const minutes = Math.floor(diffTime / (1000 * 60));
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (minutes < 1) {
return '刚刚';
} else if (minutes < 60) {
return `${minutes}分钟前`;
} else if (hours < 24) {
return `${hours}小时前`;
} else {
return `${days}天前`;
}
const handleContextMenu = async () => {
contextMenu.value.visible = false;
};
const clearMessage = () => {
message.value = '';
};
const initSSE = async () => {
console.log('initSSE');
const source = new EventSource(`${apiUrl}/chat/message`);
console.log(source);
source.onmessage = function (event) {
const data = JSON.parse(event.data);
console.log(data);
messageStore.handleMessage(data);
};
source.onerror = function (event) {
console.error('EventSource error:', event);
};
};
const setScrollToBottom = async () => {
await nextTick(); // 确保 DOM 已经更新完毕
// 检查 innerRef 是否存在
if (!innerRef.value) {
console.warn('innerRef is not defined.');
return;
}
const showContextMenu = (event: any, id: number) => {
contextMenu.value.visible = true;
contextMenu.value.chatMessageId = id;
contextMenu.value.x = event.x;
contextMenu.value.y = event.y;
// 设置滚动条到底部
const max = innerRef.value.clientHeight;
if (scrollbarRef.value) {
scrollbarRef.value.setScrollTop(max);
} else {
console.warn('scrollbarRef is not defined.');
}
return {
emojiList,
userList,
chatList,
msgList,
userInfo,
search,
showChooseFile,
currentPage,
pageSize,
scrollbarRef,
innerRef,
currentNavId,
targetUserInfo,
targetUserId,
message,
contextMenu,
initChat,
setCurrentNavId,
sendMessage,
changeChatList,
handleContextMenu,
showContextMenu
};
const changeChatList = async (id: number) => {
// 设置 targetUserId
targetUserId.value = id;
// 获取当前用户和目标用户的聊天记录
const messagesList = await db.getByField('chatRecord', 'targetUserId', id);
chatHistory.value = messagesList;
// 设置目标用户的信息
setTargetUserInfo(id);
};
const setTargetUserInfo = async (id: number) => {
targetUserInfo.value = await db.getOne('workbenchusers', id);
};
const handleContextMenu = async () => {
contextMenu.value.visible = false;
};
const getOnlineUsers = async () => {
const res = await fetchGet(apiUrl + '/chat/online?page=1');
if (!res.ok) {
notifyError("获取在线用户失败");
return;
}
})
const data = await res.json();
console.log(data);
const onlineUsers = data.data.list.map((item: any) => ({
id: item.id,
username: item.username,
nickname: item.nickname,
email: item.email,
phone: item.phone,
desc: item.desc,
jobNumber: item.job_number,
workPlace: item.work_place,
hiredDate: item.hired_date,
avatar: item.avatar || '/logo.png',
isOnline: true,
ip: item.login_ip,
updatedAt: item.updated_at,
createdAt: item.add_time,
}));
// 更新或添加用户
const existingUserIds = await db.getAll('workbenchusers').then(users => users.map(user => user.id));
const newUserIds = onlineUsers.filter(user => !existingUserIds.includes(user.id)).map(user => user.id);
// 更新现有用户的状态
const updatePromises = onlineUsers.filter(user => existingUserIds.includes(user.id)).map(user => {
return db.update('workbenchusers', user.id, {
isOnline: true,
updatedAt: user.updatedAt,
ip: user.ip,
});
});
// 添加新用户
const addPromises = onlineUsers.filter((user: { id: any }) => newUserIds.includes(user.id)).map((user: any) => {
return db.addOne('workbenchusers', user);
});
await Promise.all([...updatePromises, ...addPromises]);
// 更新 userList
userList.value = await db.getAll('workbenchusers');
console.log(userList.value);
};
const showContextMenu = (event: any, id: number) => {
contextMenu.value.visible = true;
contextMenu.value.chatMessageId = id;
contextMenu.value.x = event.x;
contextMenu.value.y = event.y;
};
return {
emojiList,
userList,
chatList,
groupList,
chatHistory,
userInfo,
search,
showChooseFile,
currentPage,
pageSize,
scrollbarRef,
innerRef,
currentNavId,
targetUserInfo,
targetUserId,
message,
contextMenu,
activeNames,
initChat,
initSSE,
setCurrentNavId,
sendMessage,
changeChatList,
handleContextMenu,
showContextMenu,
getOnlineUsers,
updateConversationList
};
});

39
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()
},

1
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)

106
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)
}

29
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
}
}

106
godo/files/pwdfile.go

@ -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!")
}

65
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))
}

Loading…
Cancel
Save