add
42
app.go
|
|
@ -1,42 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"godoos/cmd"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
go cmd.OsStart()
|
||||
}
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
cmd.OsStop()
|
||||
}
|
||||
func (a *App) OpenDirDialog() string {
|
||||
path, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: "Select Folder",
|
||||
})
|
||||
if err != nil {
|
||||
runtime.LogErrorf(a.ctx, "Error: %+v\n", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
// func (a *App) Greet(name string) string {
|
||||
// return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||
// }
|
||||
78
app/app.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"godoos/cmd"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
exDir string
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) Startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
go cmd.OsStart()
|
||||
}
|
||||
func (a *App) Shutdown(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
cmd.OsStop()
|
||||
}
|
||||
func (a *App) OpenDirDialog() string {
|
||||
path, err := wruntime.OpenDirectoryDialog(a.ctx, wruntime.OpenDialogOptions{
|
||||
Title: "Select Folder",
|
||||
})
|
||||
if err != nil {
|
||||
wruntime.LogErrorf(a.ctx, "Error: %+v\n", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func (a *App) GetAbsPath(path string) (string, error) {
|
||||
var absPath string
|
||||
var err error
|
||||
if filepath.IsAbs(path) {
|
||||
absPath = filepath.Clean(path)
|
||||
} else {
|
||||
absPath, err = filepath.Abs(filepath.Join(a.exDir, path))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
absPath = strings.ReplaceAll(absPath, "/", string(os.PathSeparator))
|
||||
//println("GetAbsPath:", absPath)
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
func (a *App) RestartApp() error {
|
||||
if runtime.GOOS == "windows" {
|
||||
name, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exec.Command(name, os.Args[1:]...).Start()
|
||||
wruntime.Quit(a.ctx)
|
||||
return nil
|
||||
}
|
||||
return errors.New("unsupported OS")
|
||||
}
|
||||
|
||||
func (a *App) GetPlatform() string {
|
||||
return runtime.GOOS
|
||||
}
|
||||
134
app/download.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cavaliergopher/grab/v3"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
func (a *App) DownloadFile(path string, url string) error {
|
||||
absPath, err := a.GetAbsPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = grab.Get(absPath, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DownloadStatus struct {
|
||||
resp *grab.Response
|
||||
cancel context.CancelFunc
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Url string `json:"url"`
|
||||
Transferred int64 `json:"transferred"`
|
||||
Size int64 `json:"size"`
|
||||
Speed float64 `json:"speed"`
|
||||
Progress float64 `json:"progress"`
|
||||
Downloading bool `json:"downloading"`
|
||||
Done bool `json:"done"`
|
||||
}
|
||||
|
||||
var downloadList []*DownloadStatus
|
||||
|
||||
func existsInDownloadList(path string, url string) bool {
|
||||
for _, ds := range downloadList {
|
||||
if ds.Path == path || ds.Url == url {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) PauseDownload(url string) {
|
||||
for _, ds := range downloadList {
|
||||
if ds.Url == url {
|
||||
if ds.cancel != nil {
|
||||
ds.cancel()
|
||||
}
|
||||
ds.resp = nil
|
||||
ds.Downloading = false
|
||||
ds.Speed = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) ContinueDownload(url string) {
|
||||
for _, ds := range downloadList {
|
||||
if ds.Url == url {
|
||||
if !ds.Downloading && ds.resp == nil && !ds.Done {
|
||||
ds.Downloading = true
|
||||
|
||||
req, err := grab.NewRequest(ds.Path, ds.Url)
|
||||
if err != nil {
|
||||
ds.Downloading = false
|
||||
break
|
||||
}
|
||||
// if PauseDownload() is called before the request finished, ds.Downloading will be false
|
||||
// if the user keeps clicking pause and resume, it may result in multiple requests being successfully downloaded at the same time
|
||||
// so we have to create a context and cancel it when PauseDownload() is called
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ds.cancel = cancel
|
||||
req = req.WithContext(ctx)
|
||||
resp := grab.DefaultClient.Do(req)
|
||||
|
||||
if resp != nil && resp.HTTPResponse != nil &&
|
||||
resp.HTTPResponse.StatusCode >= 200 && resp.HTTPResponse.StatusCode < 300 {
|
||||
ds.resp = resp
|
||||
} else {
|
||||
ds.Downloading = false
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) AddToDownloadList(path string, url string) {
|
||||
absPath, err := a.GetAbsPath(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !existsInDownloadList(absPath, url) {
|
||||
downloadList = append(downloadList, &DownloadStatus{
|
||||
resp: nil,
|
||||
Name: filepath.Base(path),
|
||||
Path: absPath,
|
||||
Url: url,
|
||||
Downloading: false,
|
||||
})
|
||||
a.ContinueDownload(url)
|
||||
} else {
|
||||
a.ContinueDownload(url)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) DownloadLoop() {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
go func() {
|
||||
for {
|
||||
<-ticker.C
|
||||
for _, ds := range downloadList {
|
||||
if ds.resp != nil {
|
||||
ds.Transferred = ds.resp.BytesComplete()
|
||||
ds.Size = ds.resp.Size()
|
||||
ds.Speed = ds.resp.BytesPerSecond()
|
||||
ds.Progress = 100 * ds.resp.Progress()
|
||||
ds.Downloading = !ds.resp.IsComplete()
|
||||
ds.Done = ds.resp.Progress() == 1
|
||||
if !ds.Downloading {
|
||||
ds.resp = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
runtime.EventsEmit(a.ctx, "downloadList", downloadList)
|
||||
}
|
||||
}()
|
||||
}
|
||||
100
app/update.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/selfupdate"
|
||||
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
type ProgressReader struct {
|
||||
reader io.Reader
|
||||
total int64
|
||||
err error
|
||||
}
|
||||
|
||||
func (pr *ProgressReader) Read(p []byte) (n int, err error) {
|
||||
n, err = pr.reader.Read(p)
|
||||
pr.err = err
|
||||
pr.total += int64(n)
|
||||
return
|
||||
}
|
||||
func (a *App) UpdateApp(url string) (broken bool, err error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
pr := &ProgressReader{reader: resp.Body}
|
||||
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
// update progress
|
||||
go func() {
|
||||
for {
|
||||
<-ticker.C
|
||||
wruntime.EventsEmit(a.ctx, "updateApp", &DownloadStatus{
|
||||
Name: filepath.Base(url),
|
||||
Path: "",
|
||||
Url: url,
|
||||
Transferred: pr.total,
|
||||
Size: resp.ContentLength,
|
||||
Speed: 0,
|
||||
Progress: 100 * (float64(pr.total) / float64(resp.ContentLength)),
|
||||
Downloading: pr.err == nil && pr.total < resp.ContentLength,
|
||||
Done: pr.total == resp.ContentLength,
|
||||
})
|
||||
if pr.err != nil || pr.total == resp.ContentLength {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var updateFile io.Reader = pr
|
||||
// extract macos binary from zip
|
||||
if strings.HasSuffix(url, ".zip") && runtime.GOOS == "darwin" {
|
||||
zipBytes, err := io.ReadAll(pr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
archive, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
file, err := archive.Open("godoos.app/Contents/MacOS/godoos")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer file.Close()
|
||||
updateFile = file
|
||||
}
|
||||
|
||||
// apply update
|
||||
err = selfupdate.Apply(updateFile, selfupdate.Options{})
|
||||
if err != nil {
|
||||
if rerr := selfupdate.RollbackError(err); rerr != nil {
|
||||
return true, rerr
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
// restart app
|
||||
if runtime.GOOS == "windows" {
|
||||
name, err := os.Executable()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
exec.Command(name, os.Args[1:]...).Start()
|
||||
wruntime.Quit(a.ctx)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package cmd
|
|||
import (
|
||||
"context"
|
||||
"godoos/libs"
|
||||
"godoos/localchat"
|
||||
"godoos/progress"
|
||||
"log"
|
||||
"net/http"
|
||||
|
|
@ -11,7 +12,7 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const serverAddress = ":56710"
|
||||
const serverAddress = ":56780"
|
||||
|
||||
var srv *http.Server
|
||||
|
||||
|
|
@ -50,6 +51,9 @@ func OsStart() {
|
|||
router.HandleFunc("/file/writefile", HandleWriteFile).Methods(http.MethodPost)
|
||||
router.HandleFunc("/file/appendfile", HandleAppendFile).Methods(http.MethodPost)
|
||||
router.HandleFunc("/file/watch", WatchHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/localchat/sse", localchat.SseHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/localchat/message", localchat.HandleMessage).Methods(http.MethodPost)
|
||||
router.HandleFunc("/localchat/upload", localchat.MultiUploadHandler).Methods(http.MethodPost)
|
||||
|
||||
go progress.CheckActive(context.Background())
|
||||
log.Printf("Listening on port: %v", serverAddress)
|
||||
|
|
|
|||
15
frontend/components.d.ts
vendored
|
|
@ -14,10 +14,15 @@ declare module 'vue' {
|
|||
BatteryPop: typeof import('./src/components/taskbar/BatteryPop.vue')['default']
|
||||
Browser: typeof import('./src/components/builtin/Browser.vue')['default']
|
||||
Calendar: typeof import('./src/components/builtin/Calendar.vue')['default']
|
||||
Chat: typeof import('./src/components/localchat/Chat.vue')['default']
|
||||
ChatContent: typeof import('./src/components/localchat/ChatContent.vue')['default']
|
||||
ChatDomain: typeof import('./src/components/localchat/ChatDomain.vue')['default']
|
||||
ChatEditor: typeof import('./src/components/localchat/ChatEditor.vue')['default']
|
||||
ChatFoot: typeof import('./src/components/localchat/ChatFoot.vue')['default']
|
||||
ChatNav: typeof import('./src/components/localchat/ChatNav.vue')['default']
|
||||
CloseButton: typeof import('./src/components/ui/CloseButton.vue')['default']
|
||||
CloseDesktop: typeof import('./src/components/desktop/CloseDesktop.vue')['default']
|
||||
ColorPicker: typeof import('./src/components/setting/ColorPicker.vue')['default']
|
||||
Compony: typeof import('./src/components/install/compony.vue')['default']
|
||||
Computer: typeof import('./src/components/computer/Computer.vue')['default']
|
||||
ComputerNavBar: typeof import('./src/components/computer/ComputerNavBar.vue')['default']
|
||||
ContextMenu: typeof import('./src/components/builtin/ContextMenu.vue')['default']
|
||||
|
|
@ -33,18 +38,23 @@ declare module 'vue' {
|
|||
DialogTemp: typeof import('./src/components/window/DialogTemp.vue')['default']
|
||||
EditFileName: typeof import('./src/components/builtin/EditFileName.vue')['default']
|
||||
EditType: typeof import('./src/components/builtin/EditType.vue')['default']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||
ElText: typeof import('element-plus/es')['ElText']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
Error: typeof import('./src/components/taskbar/Error.vue')['default']
|
||||
|
|
@ -60,12 +70,10 @@ declare module 'vue' {
|
|||
Install: typeof import('./src/components/install/Install.vue')['default']
|
||||
InstallCompony: typeof import('./src/components/install/InstallCompony.vue')['default']
|
||||
InstallMember: typeof import('./src/components/install/InstallMember.vue')['default']
|
||||
INstallMember: typeof import('./src/components/install/INstallMember.vue')['default']
|
||||
InstallPerson: typeof import('./src/components/install/InstallPerson.vue')['default']
|
||||
LockDesktop: typeof import('./src/components/desktop/LockDesktop.vue')['default']
|
||||
Magnet: typeof import('./src/components/taskbar/Magnet.vue')['default']
|
||||
MarkDown: typeof import('./src/components/builtin/MarkDown.vue')['default']
|
||||
Member: typeof import('./src/components/install/member.vue')['default']
|
||||
MenuBar: typeof import('./src/components/window/MenuBar.vue')['default']
|
||||
MenuList: typeof import('./src/components/taskbar/MenuList.vue')['default']
|
||||
MessageCenterPop: typeof import('./src/components/taskbar/MessageCenterPop.vue')['default']
|
||||
|
|
@ -80,7 +88,6 @@ declare module 'vue' {
|
|||
OpenWiteDialog: typeof import('./src/components/builtin/OpenWiteDialog.vue')['default']
|
||||
OsImage: typeof import('./src/components/builtin/OsImage.vue')['default']
|
||||
PdfViewer: typeof import('./src/components/builtin/PdfViewer.vue')['default']
|
||||
Person: typeof import('./src/components/install/person.vue')['default']
|
||||
PictureStore: typeof import('./src/components/builtin/PictureStore.vue')['default']
|
||||
QuickLink: typeof import('./src/components/computer/QuickLink.vue')['default']
|
||||
RectChosen: typeof import('./src/components/builtin/RectChosen.vue')['default']
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@liripeng/vue-audio-player": "^1.6.2",
|
||||
"dexie": "^4.0.8",
|
||||
"element-plus": "^2.7.7",
|
||||
"file-saver": "^2.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
46b0e0f4287bf8042cfe0a66114c4afc
|
||||
1fa97678edcc780911c3d714233e97d2
|
||||
2
frontend/public/image/chat/emoji.svg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656439603095" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3879" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
|
||||
</style></defs><path d="M510.944 960c-247.04 0-448-200.96-448-448s200.992-448 448-448c247.008 0 448 200.96 448 448S757.984 960 510.944 960zM510.944 128c-211.744 0-384 172.256-384 384 0 211.744 172.256 384 384 384 211.744 0 384-172.256 384-384C894.944 300.256 722.688 128 510.944 128z" p-id="3880" fill="#303133"></path><path d="M512 773.344c-89.184 0-171.904-40.32-226.912-110.624-10.88-13.92-8.448-34.016 5.472-44.896 13.888-10.912 34.016-8.48 44.928 5.472 42.784 54.688 107.136 86.048 176.512 86.048 70.112 0 134.88-31.904 177.664-87.552 10.784-14.016 30.848-16.672 44.864-5.888 14.016 10.784 16.672 30.88 5.888 44.864C685.408 732.32 602.144 773.344 512 773.344z" p-id="3881" fill="#303133"></path><path d="M368 515.2c-26.528 0-48-21.472-48-48l0-64c0-26.528 21.472-48 48-48s48 21.472 48 48l0 64C416 493.696 394.496 515.2 368 515.2z" p-id="3882" fill="#303133"></path><path d="M656 515.2c-26.496 0-48-21.472-48-48l0-64c0-26.528 21.504-48 48-48s48 21.472 48 48l0 64C704 493.696 682.496 515.2 656 515.2z" p-id="3883" fill="#303133"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
frontend/public/image/chat/emoji/666.gif
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
frontend/public/image/chat/emoji/发怒.gif
Normal file
|
After Width: | Height: | Size: 701 KiB |
BIN
frontend/public/image/chat/emoji/可爱狗.gif
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
frontend/public/image/chat/emoji/吃屎.gif
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/public/image/chat/emoji/吐了.gif
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
frontend/public/image/chat/emoji/吐槽.gif
Normal file
|
After Width: | Height: | Size: 622 KiB |
BIN
frontend/public/image/chat/emoji/哭笑.gif
Normal file
|
After Width: | Height: | Size: 615 KiB |
BIN
frontend/public/image/chat/emoji/嘲笑.gif
Normal file
|
After Width: | Height: | Size: 950 KiB |
BIN
frontend/public/image/chat/emoji/大哭.gif
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
frontend/public/image/chat/emoji/大眼.gif
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
frontend/public/image/chat/emoji/天使.gif
Normal file
|
After Width: | Height: | Size: 772 KiB |
BIN
frontend/public/image/chat/emoji/好吃.gif
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
frontend/public/image/chat/emoji/委屈.gif
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
frontend/public/image/chat/emoji/委屈巴巴.gif
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
frontend/public/image/chat/emoji/小丑.gif
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
frontend/public/image/chat/emoji/崇拜.gif
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
frontend/public/image/chat/emoji/思考.gif
Normal file
|
After Width: | Height: | Size: 890 KiB |
BIN
frontend/public/image/chat/emoji/惊吓.gif
Normal file
|
After Width: | Height: | Size: 756 KiB |
BIN
frontend/public/image/chat/emoji/想吐.gif
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
frontend/public/image/chat/emoji/想哭.gif
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
frontend/public/image/chat/emoji/拍打.gif
Normal file
|
After Width: | Height: | Size: 541 KiB |
BIN
frontend/public/image/chat/emoji/斜眼笑.gif
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
frontend/public/image/chat/emoji/斜笑.gif
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
frontend/public/image/chat/emoji/比耶.gif
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
frontend/public/image/chat/emoji/比酷.gif
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
frontend/public/image/chat/emoji/流汗.gif
Normal file
|
After Width: | Height: | Size: 2 MiB |
BIN
frontend/public/image/chat/emoji/疑问.gif
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
frontend/public/image/chat/emoji/调皮.gif
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
frontend/public/image/chat/emoji/超级崇拜.gif
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
frontend/public/image/chat/emoji/酷笑.gif
Normal file
|
After Width: | Height: | Size: 440 KiB |
BIN
frontend/public/image/chat/emoji/露牙笑.gif
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
frontend/public/image/chat/notice.mp3
Normal file
|
|
@ -1,24 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { System } from "@/system/index.ts";
|
||||
import { onMounted, computed } from "vue";
|
||||
// 使用ref和computed改进状态管理
|
||||
const isInstall: any = computed(() => {
|
||||
const item = localStorage.getItem("isGodoOSInstall");
|
||||
// 显式地进行类型检查和转换
|
||||
return item === "true";
|
||||
});
|
||||
import { onMounted } from "vue";
|
||||
|
||||
onMounted(() => {
|
||||
new System();
|
||||
});
|
||||
// router.beforeEach((to, _, next) => {
|
||||
// console.log(to.name);
|
||||
// if (!isInstall.value && to.name !== "install") {
|
||||
// next("/install");
|
||||
// } else {
|
||||
// next();
|
||||
// }
|
||||
// });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
125
frontend/src/assets/emoji.json
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
[{
|
||||
"title": "[666]",
|
||||
"icon": "/image/chat/emoji/666.gif"
|
||||
},
|
||||
{
|
||||
"title": "[比酷]",
|
||||
"icon": "/image/chat/emoji/比酷.gif"
|
||||
},
|
||||
{
|
||||
"title": "[比耶]",
|
||||
"icon": "/image/chat/emoji/比耶.gif"
|
||||
},
|
||||
{
|
||||
"title": "[超级崇拜]",
|
||||
"icon": "/image/chat/emoji/超级崇拜.gif"
|
||||
},
|
||||
{
|
||||
"title": "[嘲笑]",
|
||||
"icon": "/image/chat/emoji/嘲笑.gif"
|
||||
},
|
||||
{
|
||||
"title": "[吃屎]",
|
||||
"icon": "/image/chat/emoji/吃屎.gif"
|
||||
},
|
||||
{
|
||||
"title": "[崇拜]",
|
||||
"icon": "/image/chat/emoji/崇拜.gif"
|
||||
},
|
||||
{
|
||||
"title": "[大哭]",
|
||||
"icon": "/image/chat/emoji/大哭.gif"
|
||||
},
|
||||
{
|
||||
"title": "[大眼]",
|
||||
"icon": "/image/chat/emoji/大眼.gif"
|
||||
},
|
||||
{
|
||||
"title": "[调皮]",
|
||||
"icon": "/image/chat/emoji/调皮.gif"
|
||||
},
|
||||
{
|
||||
"title": "[发怒]",
|
||||
"icon": "/image/chat/emoji/发怒.gif"
|
||||
},
|
||||
{
|
||||
"title": "[好吃]",
|
||||
"icon": "/image/chat/emoji/好吃.gif"
|
||||
},
|
||||
{
|
||||
"title": "[惊吓]",
|
||||
"icon": "/image/chat/emoji/惊吓.gif"
|
||||
},
|
||||
{
|
||||
"title": "[可爱狗]",
|
||||
"icon": "/image/chat/emoji/可爱狗.gif"
|
||||
},
|
||||
{
|
||||
"title": "[哭笑]",
|
||||
"icon": "/image/chat/emoji/哭笑.gif"
|
||||
},
|
||||
{
|
||||
"title": "[酷笑]",
|
||||
"icon": "/image/chat/emoji/酷笑.gif"
|
||||
},
|
||||
{
|
||||
"title": "[流汗]",
|
||||
"icon": "/image/chat/emoji/流汗.gif"
|
||||
},
|
||||
{
|
||||
"title": "[露牙笑]",
|
||||
"icon": "/image/chat/emoji/露牙笑.gif"
|
||||
},
|
||||
{
|
||||
"title": "[拍打]",
|
||||
"icon": "/image/chat/emoji/拍打.gif"
|
||||
},
|
||||
{
|
||||
"title": "[思考]",
|
||||
"icon": "/image/chat/emoji/思考.gif"
|
||||
},
|
||||
{
|
||||
"title": "[天使]",
|
||||
"icon": "/image/chat/emoji/天使.gif"
|
||||
},
|
||||
{
|
||||
"title": "[吐槽]",
|
||||
"icon": "/image/chat/emoji/吐槽.gif"
|
||||
},
|
||||
{
|
||||
"title": "[吐了]",
|
||||
"icon": "/image/chat/emoji/吐了.gif"
|
||||
},
|
||||
{
|
||||
"title": "[委屈]",
|
||||
"icon": "/image/chat/emoji/委屈.gif"
|
||||
},
|
||||
{
|
||||
"title": "[委屈巴巴]",
|
||||
"icon": "/image/chat/emoji/委屈巴巴.gif"
|
||||
},
|
||||
{
|
||||
"title": "[想哭]",
|
||||
"icon": "/image/chat/emoji/想哭.gif"
|
||||
},
|
||||
{
|
||||
"title": "[想吐]",
|
||||
"icon": "/image/chat/emoji/想吐.gif"
|
||||
},
|
||||
{
|
||||
"title": "[小丑]",
|
||||
"icon": "/image/chat/emoji/小丑.gif"
|
||||
},
|
||||
{
|
||||
"title": "[斜笑]",
|
||||
"icon": "/image/chat/emoji/斜笑.gif"
|
||||
},
|
||||
{
|
||||
"title": "[斜眼笑]",
|
||||
"icon": "/image/chat/emoji/斜眼笑.gif"
|
||||
},
|
||||
{
|
||||
"title": "[疑问]",
|
||||
"icon": "/image/chat/emoji/疑问.gif"
|
||||
}
|
||||
]
|
||||
75
frontend/src/components/localchat/Chat.vue
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<el-row justify="space-between">
|
||||
<el-col :span="2">
|
||||
<chat-nav />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<chat-domain />
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<chat-content />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
|
||||
import { useLocalChatStore } from "@/stores/localchat";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getSystemConfig } from "@/system/config";
|
||||
const store = useLocalChatStore();
|
||||
const config = getSystemConfig();
|
||||
|
||||
let source:any;
|
||||
onMounted(async () => {
|
||||
await store.init()
|
||||
init()
|
||||
});
|
||||
onUnmounted(() => {
|
||||
if (source) {
|
||||
source.close();
|
||||
}
|
||||
});
|
||||
function init() {
|
||||
if (typeof EventSource === "undefined") {
|
||||
ElMessage.error("您的浏览器不支持SSE");
|
||||
return;
|
||||
}
|
||||
const sseUrl = config.apiUrl + "/localchat/sse";
|
||||
source = new EventSource(sseUrl);
|
||||
// 当接收到消息时触发
|
||||
source.onmessage = async function (event:any) {
|
||||
//console.log("has message!");
|
||||
const eventData = event.data; // 先保存原始数据
|
||||
const jsonData = JSON.parse(eventData); // 解析数据
|
||||
//console.log(jsonData);
|
||||
if (jsonData.type == "user_list") {
|
||||
//store.userList = jsonData;
|
||||
store.setUserList(jsonData.content);
|
||||
//await nextTick();
|
||||
}
|
||||
if(jsonData.type == 'text'){
|
||||
store.addText(jsonData);
|
||||
}
|
||||
if(jsonData.type == 'file'){
|
||||
store.addFile(jsonData);
|
||||
}
|
||||
};
|
||||
// 当与服务器的连接打开时触发
|
||||
source.onopen = function () {
|
||||
console.log("Connection opened.");
|
||||
};
|
||||
|
||||
// 当与服务器的连接关闭时触发
|
||||
source.onerror = function () {
|
||||
console.log("Connection closed.");
|
||||
};
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
|
||||
</style>
|
||||
248
frontend/src/components/localchat/ChatContent.vue
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, inject, ref, nextTick, watch } from "vue";
|
||||
import { useLocalChatStore } from "@/stores/localchat";
|
||||
import { formatChatTime } from "@/util/common";
|
||||
import Vditor from "vditor";
|
||||
import "vditor/dist/index.css";
|
||||
import { ElScrollbar } from "element-plus";
|
||||
import { System } from "@/system";
|
||||
const sys:any = inject<System>("system");
|
||||
const store = useLocalChatStore();
|
||||
const messageContainerRef = ref<InstanceType<typeof ElScrollbar>>();
|
||||
const messageInnerRef = ref<HTMLDivElement>();
|
||||
let isScrool = false;
|
||||
onMounted(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (messageContainerRef && messageContainerRef.value) {
|
||||
// messageContainerRef.value!.setScrollTop(
|
||||
// messageInnerRef.value!.clientHeight
|
||||
// );
|
||||
messageContainerRef.value.setScrollTop(messageInnerRef.value!.clientHeight);
|
||||
}
|
||||
});
|
||||
};
|
||||
watch(
|
||||
() => store.msgList,
|
||||
(_) => {
|
||||
if(!isScrool){
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
function replaceIconTags(text:any) {
|
||||
// 定义正则表达式,匹配 {*任意内容*} 的格式
|
||||
text = Vditor.md2html(text);
|
||||
const regex = /\{\-(.*?)\-\}/g;
|
||||
|
||||
// 使用正则表达式的replace方法进行替换
|
||||
const replacedText = text.replace(regex, (_:any, p1:string) => {
|
||||
// p1 是匹配到的内容部分,这里直接构造图片标签
|
||||
return `<img src='/image/chat/emoji/${p1}.gif' style='width:30px;height:30px;' />`;
|
||||
//return ``
|
||||
});
|
||||
|
||||
return replacedText;
|
||||
}
|
||||
async function scroll({ scrollTop }: { scrollTop: number }) {
|
||||
if (store.msgList.length + 1 > store.pageSize && scrollTop < 1) {
|
||||
isScrool = true;
|
||||
await store.moreMsgList();
|
||||
isScrool = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chatContentContainer" v-if="store.chatTargetId > 0">
|
||||
<div class="message-area">
|
||||
<el-scrollbar
|
||||
max-height="100%"
|
||||
class="scrollbar-container"
|
||||
@scroll="scroll"
|
||||
ref="messageContainerRef">
|
||||
<div ref="messageInnerRef" class="message-wrap">
|
||||
<div v-for="(item, index) in store.msgList" :key="index" :class="['message-block', item.isMe ? 'mine' : 'theirs']">
|
||||
<div class="avatar-container">
|
||||
<div class="icon-container">
|
||||
<el-icon><component :is="item.isMe ? 'UserFilled' : 'Place'"/></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div
|
||||
v-if="item.type === 'text'"
|
||||
v-html="replaceIconTags(item.content)"
|
||||
class="message-content">
|
||||
</div>
|
||||
<div v-if="item.type === 'file'">
|
||||
<div class="file-bubble">
|
||||
<div class="file-content" v-for="el in item.content" @click="sys.openFile(el.path)">
|
||||
<div class="file-icon"><FileIcon :file="el" /></div>
|
||||
<div class="file-name">{{el.name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timestamp text-grey">{{ formatChatTime(item.createdAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<ChatFoot class="mt-20px"></ChatFoot>
|
||||
</div>
|
||||
<div class="no-message-container" v-else>
|
||||
<Vue3Lottie animationLink="/bot/localchat.json" :height="300" :width="300" />
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
$win10-blue: #0078d7;
|
||||
$win10-light-blue: #c7e8ff;
|
||||
$win10-grey: #afafaf;
|
||||
$win10-light-grey: #f2f2f2;
|
||||
|
||||
.chatContentContainer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.message-area {
|
||||
|
||||
height: 420px;
|
||||
}
|
||||
|
||||
.scrollbar-container {
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.message-wrap {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.message-block {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
align-items: flex-end; // 保持底部对齐
|
||||
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 1px solid $win10-grey;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
max-width: calc(100% - 150px);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
background: $win10-light-grey;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
color: $win10-grey;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 12px;
|
||||
color: $win10-grey;
|
||||
}
|
||||
|
||||
|
||||
.mine {
|
||||
// 添加这一行来确保'mine'类的消息整体靠右
|
||||
justify-content: flex-end;
|
||||
.content {
|
||||
// 由于'mine'消息块整体靠右,内容区域不需要特别处理对齐
|
||||
// justify-content: flex-end; 可以移除,因为它会与外部的justify-content冲突
|
||||
align-items: flex-end; // 保持底部对齐
|
||||
}
|
||||
|
||||
.message-content {
|
||||
background-color: $win10-blue;
|
||||
color: $win10-light-blue;
|
||||
border-radius: 12px 2px 2px 2px; // 保持原有样式
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
order: 1;
|
||||
margin-left: 10px;
|
||||
margin-top:-20px;
|
||||
}
|
||||
}
|
||||
|
||||
.theirs {
|
||||
.content {
|
||||
// 使对方的消息内容靠左对齐
|
||||
align-items: flex-end;
|
||||
}
|
||||
.message-content {
|
||||
border-radius: 2px 12px 2px 2px;
|
||||
}
|
||||
}
|
||||
.no-message-container {
|
||||
height: 100%;
|
||||
margin: 120px auto;
|
||||
}
|
||||
.file-bubble {
|
||||
background-color: #f0f0f0; /* 背景色,可以根据需要调整 */
|
||||
border-radius: 10px; /* 圆角,让框看起来更柔和 */
|
||||
padding: 10px; /* 内边距,给内容一些空间 */
|
||||
margin-bottom: 10px; /* 气泡间的外边距,使它们看起来不紧凑 */
|
||||
max-width: 100%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 添加阴影效果,增强立体感 */
|
||||
}
|
||||
|
||||
.file-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height:36px;
|
||||
line-height:36px;
|
||||
gap: 3px;
|
||||
}
|
||||
.file-content:hover {
|
||||
background-color: #e0e0e0; /* 改变背景色,悬停时更浅或更深,根据设计调整 */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* 增强阴影效果,使气泡在悬停时更加突出 */
|
||||
transition: all 0.3s ease; /* 添加过渡效果,使变化平滑 */
|
||||
}
|
||||
.file-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
padding-left: 5px;
|
||||
/* 可选:限制文件名行数,如果需要 */
|
||||
/* max-height: 2em; */
|
||||
/* display: -webkit-box; */
|
||||
/* -webkit-line-clamp: 2; */
|
||||
/* -webkit-box-orient: vertical; */
|
||||
/* overflow: hidden; */
|
||||
}
|
||||
</style>
|
||||
199
frontend/src/components/localchat/ChatDomain.vue
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
<template>
|
||||
<div class="win11-msg-container">
|
||||
<el-scrollbar>
|
||||
<div v-if="store.navId < 2" class="user-list-area">
|
||||
<el-row
|
||||
class="user-list"
|
||||
justify="space-around"
|
||||
v-for="(msg, key) in store.contentList"
|
||||
:key="key"
|
||||
v-if="store.contentList.length > 0"
|
||||
>
|
||||
<!-- <el-col :span="5" class="avatar-col">
|
||||
<el-icon :size="22" class="avatar">
|
||||
<Place />
|
||||
</el-icon>
|
||||
</el-col> -->
|
||||
<el-col :span="22" :class="msg.targetIp == store.chatTargetIp ? 'user-item active' : 'user-item'" @click="store.setChatId(msg.targetIp)">
|
||||
<span class="username">
|
||||
|
||||
<el-icon :size="12">
|
||||
<Place />
|
||||
</el-icon>
|
||||
{{ msg.reciperInfo.hostname }}
|
||||
</span>
|
||||
<span class="msg">{{ msg.content }}</span>
|
||||
|
||||
<span class="userip" v-if="msg.readNum > 0">
|
||||
<el-badge :value="msg.readNum" :offset="[-3,8]">
|
||||
{{ formatChatTime(msg.createdAt) }}</el-badge>
|
||||
</span>
|
||||
<span class="userip" v-else>
|
||||
{{ formatChatTime(msg.createdAt) }}
|
||||
</span>
|
||||
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty v-else :image-size="100" description="消息列表为空" />
|
||||
</div>
|
||||
|
||||
<div v-else class="user-list-area">
|
||||
<el-row
|
||||
justify="start">
|
||||
<el-icon :size="18" @click="store.refreshUserList">
|
||||
<RefreshRight />
|
||||
</el-icon>
|
||||
</el-row>
|
||||
<el-row
|
||||
class="user-list"
|
||||
justify="space-around"
|
||||
v-for="(user, key) in store.userList"
|
||||
:key="key"
|
||||
v-if="store.userList.length > 0"
|
||||
>
|
||||
<el-col :span="5" class="avatar-col">
|
||||
<el-icon :size="22" class="avatar">
|
||||
<Monitor />
|
||||
</el-icon>
|
||||
<el-icon v-if="user.isOnline" :size="16" class="status-icon online">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
<el-icon v-else :size="16" class="status-icon offline">
|
||||
<Warning />
|
||||
</el-icon>
|
||||
</el-col>
|
||||
<el-col :span="19" class="user-item" @click="store.setChatId(user.ip)">
|
||||
<span class="username">{{ user.hostname }}</span>
|
||||
<span class="userip">{{ user.ip }}</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty v-else :image-size="100" description="好友列表为空" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLocalChatStore } from "@/stores/localchat";
|
||||
import { formatChatTime } from "@/util/common";
|
||||
|
||||
const store = useLocalChatStore();
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.win11-msg-container {
|
||||
height: 100vh; /* 假设顶部有100px的导航栏 */
|
||||
width: 100%;
|
||||
border-right: 1px solid rgba(238, 238, 238, 0.5); /* 更柔和的边框 */
|
||||
border-top: 1px solid rgba(238, 238, 238, 0.5);
|
||||
border-radius: 0 12px 0 0; /* Win11风格的圆角 */
|
||||
background-color: #f8f8f8; /* 使用更亮的淡灰色背景 */
|
||||
overflow: hidden; /* 清除滚动条溢出 */
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
padding: 16px; /* 内容区域的内边距 */
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
&.self {
|
||||
justify-content: flex-end; /* 自身消息右对齐 */
|
||||
}
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px; /* 圆角 */
|
||||
max-width: 75%;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
&.bubble--right {
|
||||
background-color: #e8eaed; /* 更接近Win11的发送方消息背景色 */
|
||||
}
|
||||
}
|
||||
.user-list-area {
|
||||
width: 94%;
|
||||
margin: 3%;
|
||||
}
|
||||
.user-list {
|
||||
background-color: #f8f8f8; // 淡灰色背景,与容器区分
|
||||
border-radius: 8px; // 圆角边缘,更柔和的外观
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); // 温和的阴影效果,增加深度感
|
||||
.avatar-col {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center; // 确保图标垂直居中
|
||||
}
|
||||
.active{
|
||||
background-color: #e8eaed;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column; /* 改变为列方向布局 */
|
||||
gap: 4px; /* 行间间距 */
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 50%; /* 头像圆角 */
|
||||
}
|
||||
|
||||
.username {
|
||||
text-align: left; // 设置username左对齐
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
width:95%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.msg {
|
||||
text-align: left; // 设置username左对齐
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
overflow: hidden;
|
||||
width:95%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.userip {
|
||||
text-align: right; // 设置userip右对齐
|
||||
margin-left: auto; // 这将使userip在容器内右对齐
|
||||
font-size: 11px;
|
||||
padding-right: 5px;
|
||||
color: #666;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.status-icon {
|
||||
margin-left: 2px; // 与头像保持一定距离
|
||||
margin-top:12px;
|
||||
}
|
||||
|
||||
.online {
|
||||
color: green; // 在线图标颜色
|
||||
}
|
||||
|
||||
.offline {
|
||||
color: red; // 离线图标颜色
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态调整 */
|
||||
.el-empty {
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
96
frontend/src/components/localchat/ChatEditor.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<!-- 聊天编辑区域 -->
|
||||
<div
|
||||
class="edit-box"
|
||||
@dragover.prevent
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
class="message-input"
|
||||
@keydown.enter="keyDown($event)"
|
||||
v-model="store.sendInfo" />
|
||||
<el-tooltip placement="top" content="按enter键发送,按ctrl+enter键换行">
|
||||
<el-icon :size="22" class="win11-chat-send-button" @click="store.sendMsg()">
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLocalChatStore } from "@/stores/localchat"
|
||||
import { notifyError } from "@/util/msg";
|
||||
|
||||
const store = useLocalChatStore()
|
||||
// 按下回车键
|
||||
function keyDown(event: any) {
|
||||
if (event.ctrlKey && event.keyCode === 13) {
|
||||
store.sendInfo = store.sendInfo + "\n"
|
||||
|
||||
} else if (event.keyCode === 13) {
|
||||
event.preventDefault() // 阻止浏览器默认换行操作
|
||||
send()
|
||||
return false
|
||||
}
|
||||
}
|
||||
const handleDrop = (event:any) => {
|
||||
const frompathArrStr = event?.dataTransfer?.getData('frompath');
|
||||
event.preventDefault()
|
||||
const files = JSON.parse(frompathArrStr) as string[];
|
||||
if (files && files.length > 0) {
|
||||
// 处理拖放的文件,例如上传
|
||||
console.log('Files dropped:', files);
|
||||
store.uploadFile(files)
|
||||
|
||||
}
|
||||
};
|
||||
function send(){
|
||||
if(!store.hostInfo || !store.hostInfo.ip){
|
||||
notifyError("Please wait for a moment");
|
||||
return;
|
||||
}
|
||||
store.sendMsg()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.edit-box {
|
||||
position: relative; /* 为容器添加相对定位,以便子元素可以相对于它进行定位 */
|
||||
}
|
||||
|
||||
.message-input {
|
||||
resize: none; /* 禁止调整输入框大小,保持布局稳定 */
|
||||
}
|
||||
|
||||
|
||||
.win11-chat-send-button {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
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; /* 快速过渡效果 */
|
||||
}
|
||||
|
||||
.win11-chat-send-button:hover {
|
||||
background-color: #D1E4FF; /* 悬浮时颜色略深,保持浅色调 */
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 稍微增强阴影 */
|
||||
}
|
||||
|
||||
.win11-chat-send-button:active {
|
||||
background-color: #B3D4FC; /* 按下时颜色更深,但依然保持清新 */
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0.1); /* 回复初始阴影 */
|
||||
transform: translateY(1px); /* 微小下移,模拟按下 */
|
||||
}
|
||||
</style>
|
||||
165
frontend/src/components/localchat/ChatFoot.vue
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<footer class="footer-container">
|
||||
<!--工具栏-->
|
||||
<el-row type="flex" class="toolbar">
|
||||
<el-popover
|
||||
placement="top"
|
||||
popper-class="chat-icon-popover"
|
||||
trigger="click"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="emoji-button">
|
||||
<img
|
||||
width="24"
|
||||
height="24"
|
||||
class="emoji-image"
|
||||
src="/image/chat/emoji.svg"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar class="emoji-scroll">
|
||||
<ul class="emoji-list-container">
|
||||
<li
|
||||
v-for="item in store.emojiList"
|
||||
:key="item.title"
|
||||
class="p-5px list-none hover:animate-heart-beat animate-count-animated animate-duration-1s cursor-pointer"
|
||||
:title="item.title"
|
||||
>
|
||||
<img
|
||||
width="30"
|
||||
height="30"
|
||||
:src="item.icon"
|
||||
@click="selectIcon(item.icon)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
</el-popover>
|
||||
<div class="upload-button" @click="selectImg()">
|
||||
<el-icon :size="22">
|
||||
<Picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="upload-button" @click="selectFile()">
|
||||
<el-icon :size="22">
|
||||
<Link />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="upload-button" @click="store.clearMsg()">
|
||||
<el-icon :size="22">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-row>
|
||||
<ChatEditor></ChatEditor>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLocalChatStore } from "@/stores/localchat";
|
||||
import { useChooseStore } from "@/stores/choose";
|
||||
|
||||
import { toRaw, watch } from "vue";
|
||||
const store = useLocalChatStore();
|
||||
const choose = useChooseStore();
|
||||
//const editor = ref(null)
|
||||
const imgExt = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"];
|
||||
|
||||
// 选择表情
|
||||
function selectIcon(icon: string) {
|
||||
store.sendInfo +=
|
||||
"{-" + icon.replace("/image/chat/emoji/", "").replace(".gif", "") + "-}";
|
||||
}
|
||||
function selectImg() {
|
||||
choose.select("选择图片", imgExt);
|
||||
}
|
||||
function selectFile() {
|
||||
choose.select("选择文件", "*");
|
||||
}
|
||||
watch(
|
||||
() => choose.path,
|
||||
(newVal, _) => {
|
||||
//console.log("userList 变化了:", newVal);
|
||||
const paths = toRaw(newVal)
|
||||
if(paths.length > 0){
|
||||
store.uploadFile(paths).then(() => {
|
||||
choose.path = []
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
{ deep: true } // 添加deep: true以深度监听数组或对象内部的变化
|
||||
);
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.footer-container {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.emoji-button {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.emoji-image {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.emoji-scroll {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.emoji-list {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.emoji-list-item {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
animation-duration: 1s;
|
||||
&.hover:animate-heart-beat {
|
||||
animation-name: heart-beat;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.emoji-list-container {
|
||||
margin: 0; /* 对应 m0 */
|
||||
padding: 0; /* 对应 p0 */
|
||||
display: flex; /* 对应 flex */
|
||||
flex-wrap: wrap; /* 对应 flex-wrap */
|
||||
}
|
||||
.upload-button {
|
||||
margin-left: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.answer-editor {
|
||||
/* Assuming this class already exists or you define it elsewhere */
|
||||
}
|
||||
</style>
|
||||
93
frontend/src/components/localchat/ChatNav.vue
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<el-space direction="vertical" :size="20" class="win11-chat-nav">
|
||||
<div class="nav-item" v-for="item in store.navList" :key="item.index">
|
||||
<el-button
|
||||
:icon="item.icon"
|
||||
:class="store.navId === item.index ? 'active' : ''"
|
||||
dark
|
||||
circle
|
||||
@click="store.handleSelect(item.index)"
|
||||
/>
|
||||
</div>
|
||||
</el-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLocalChatStore } from "@/stores/localchat";
|
||||
const store = useLocalChatStore();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.win11-chat-nav {
|
||||
height: 100vh;
|
||||
background-color: #f8f8f8; /* 使用更亮的淡灰色,更接近Win11的背景色 */
|
||||
border-right: 1px solid rgba(230, 230, 230, 0.5); /* 更浅的边框颜色 */
|
||||
padding: 16px;
|
||||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/* 添加全局字体样式以匹配Win11 */
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.win11-chat-nav .nav-item {
|
||||
/* 假定每个按钮的基类 */
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 3px;
|
||||
border-radius: 50%; /* 圆角 */
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 渐变阴影 */
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0; /* 鼠标悬停时的轻微颜色变化 */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&.active {
|
||||
/* 加强背景色对比,使用Win11的强调色或品牌色 */
|
||||
background-color: #4579a1; /* 示例颜色,可根据设计调整 */
|
||||
color: white; /* 文字颜色反转,确保可读性 */
|
||||
|
||||
/* 增加外边框以进一步区分 */
|
||||
border: 2px solid #005a9c; /* 较深的强调色作为边框 */
|
||||
border-radius: 50%;
|
||||
/* 内发光效果,让按钮看起来更‘活跃’ */
|
||||
box-shadow: 0 5px 8px rgba(0, 120, 212, 0.5) inset;
|
||||
|
||||
/* 微动效,当状态改变时给予用户反馈 */
|
||||
transform: scale(1.02);
|
||||
transition: transform 0.2s cubic-bezier(0.2, 0.4, 0.6, 1);
|
||||
|
||||
/* 确保文字在按下时不会因按钮尺寸变化而偏移 */
|
||||
transition-property: background-color, box-shadow, transform, color;
|
||||
|
||||
/* 如果按钮包含图标,可以考虑为图标也添加强调效果,例如改变颜色 */
|
||||
.el-icon {
|
||||
color: inherit; /* 或指定特定强调色 */
|
||||
}
|
||||
|
||||
/* 为了平滑的过渡,确保所有相关属性都被包含在transition中 */
|
||||
}
|
||||
/* 考虑到按钮是圆形且使用了 `circle` 属性,确保图标和背景颜色调整得当 */
|
||||
&.active .el-button {
|
||||
background-color: transparent !important; /* 确保背景色不影响图标颜色 */
|
||||
}
|
||||
|
||||
/* 图标颜色调整,确保在active状态下足够突出 */
|
||||
&.active .el-icon {
|
||||
color: #ffffff; /* 确保图标颜色与背景对比鲜明 */
|
||||
}
|
||||
|
||||
/* 非活动状态的悬停效果,保持与.active状态的区分 */
|
||||
.nav-item:hover:not(.active) {
|
||||
/* 调整以与.active状态区分,例如使用较浅的颜色 */
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
110
frontend/src/stores/db.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import Dexie from 'dexie'
|
||||
|
||||
export type ChatTable = 'chatuser' | 'chatmsg' | 'chatfile'
|
||||
|
||||
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',
|
||||
chatfile:'++id,msgId,fileName,fileSize,fileType,fileUrl,filePath,fileExt'
|
||||
});
|
||||
export const db = {
|
||||
|
||||
async getMaxId(tableName: ChatTable) {
|
||||
const data = await dbInit[tableName].orderBy('id').reverse().first()
|
||||
if (!data) {
|
||||
return 0
|
||||
} else {
|
||||
return data.id
|
||||
}
|
||||
},
|
||||
async getInsertId(tableName: ChatTable) {
|
||||
const id: any = await this.getMaxId(tableName)
|
||||
return id + 1
|
||||
},
|
||||
async getPage(tableName: ChatTable, page?: number, size?: number) {
|
||||
page = (!page || page < 1) ? 1 : page
|
||||
size = size ? size : 10
|
||||
const offset = (page - 1) * size
|
||||
return dbInit[tableName]
|
||||
.orderBy("id")
|
||||
.reverse()
|
||||
.offset(offset)
|
||||
.limit(size)
|
||||
.toArray();
|
||||
},
|
||||
async getAll(tableName: ChatTable) {
|
||||
return dbInit[tableName].toArray()
|
||||
},
|
||||
async count(tableName: ChatTable) {
|
||||
return dbInit[tableName].count()
|
||||
},
|
||||
async countSearch(tableName: ChatTable, whereObj?: any) {
|
||||
return dbInit[tableName].where(whereObj).count()
|
||||
},
|
||||
async pageSearch(tableName: ChatTable, page?: number, size?: number, whereObj?: any) {
|
||||
page = (!page || page < 1) ? 1 : page
|
||||
size = size ? size : 10
|
||||
const offset = (page - 1) * size
|
||||
//console.log(whereObj)
|
||||
return dbInit[tableName]
|
||||
.where(whereObj)
|
||||
.reverse()
|
||||
.offset(offset)
|
||||
.limit(size)
|
||||
.toArray();
|
||||
},
|
||||
async filter(tableName: ChatTable, filterFunc : any) {
|
||||
return dbInit[tableName].filter(filterFunc).toArray()
|
||||
},
|
||||
table(tableName: ChatTable) {
|
||||
return dbInit[tableName]
|
||||
},
|
||||
async getOne(tableName: ChatTable, Id: number) {
|
||||
return dbInit[tableName].get(Id)
|
||||
},
|
||||
async getRow(tableName: ChatTable, fieldName: string, val: any){
|
||||
return dbInit[tableName].where(fieldName).equals(val).first()
|
||||
},
|
||||
async get(tableName: ChatTable, whereObj : any) {
|
||||
//console.log(whereObj)
|
||||
const data = await dbInit[tableName].where(whereObj).first()
|
||||
//console.log(data)
|
||||
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) {
|
||||
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()
|
||||
},
|
||||
async addOne(tableName: ChatTable, data: any) {
|
||||
return dbInit[tableName].add(data)
|
||||
},
|
||||
async addAll(tableName: ChatTable, data: any) {
|
||||
return dbInit[tableName].bulkAdd(data)
|
||||
},
|
||||
async update(tableName: ChatTable, Id?: number, updates?: any) {
|
||||
return dbInit[tableName].update(Id, updates)
|
||||
},
|
||||
async modify(tableName: ChatTable, fieldName: string, val: any, updates: any) {
|
||||
return dbInit[tableName].where(fieldName).equals(val).modify(updates)
|
||||
},
|
||||
async delete(tableName: ChatTable, Id?: number) {
|
||||
return dbInit[tableName].delete(Id)
|
||||
},
|
||||
async deleteByField(tableName: ChatTable, fieldName: string, val: any) {
|
||||
return dbInit[tableName].where(fieldName).equals(val).delete()
|
||||
},
|
||||
async clear(tableName: ChatTable) {
|
||||
return dbInit[tableName].clear()
|
||||
},
|
||||
}
|
||||
464
frontend/src/stores/localchat.ts
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import emojiList from "@/assets/emoji.json"
|
||||
import { ref, toRaw, inject } from "vue";
|
||||
import { db } from './db'
|
||||
import { System,dirname } from "@/system";
|
||||
import { getSystemConfig } from "@/system/config";
|
||||
import { isBase64, base64ToBuffer } from "@/util/file";
|
||||
import { notifyError, notifySuccess } from "@/util/msg";
|
||||
export const useLocalChatStore = defineStore('localChatStore', () => {
|
||||
const config = getSystemConfig();
|
||||
const sys = inject<System>("system");
|
||||
const userList:any = ref([])
|
||||
const msgList:any = ref([])
|
||||
const contentList:any = ref([])
|
||||
const hostInfo:any = ref({})
|
||||
const showChooseFile = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(50)
|
||||
const navList = ref([
|
||||
{ index: 1, lable: "消息列表", icon: "ChatDotRound", type:"success" },
|
||||
{ index: 2, lable: "用户列表", icon: "UserFilled", type: "info" },
|
||||
])
|
||||
const navId = ref(1)
|
||||
const sendInfo = ref("")
|
||||
const chatTargetId = ref(0)
|
||||
const chatTargetIp = ref("")
|
||||
const handleSelect = (key: number) => {
|
||||
navId.value = key;
|
||||
};
|
||||
const setChatId = async(ip : string) => {
|
||||
//console.log(ip)
|
||||
//chatTargetId.value = id
|
||||
chatTargetIp.value = ip
|
||||
const data = await db.get("chatuser", {ip : ip})
|
||||
if(!data)return;
|
||||
chatTargetId.value = data.id
|
||||
clearContentList(data.id)
|
||||
currentPage.value = 1
|
||||
await getMsgList()
|
||||
}
|
||||
const initContentList = async () => {
|
||||
const list:any = {}
|
||||
const msgAll = await db.getAll('chatmsg')
|
||||
msgAll.forEach((d : any) => {
|
||||
if(!d.isMe){
|
||||
if (!list[d.targetIp]){
|
||||
list[d.targetIp] = []
|
||||
}
|
||||
list[d.targetIp].push(d)
|
||||
}
|
||||
|
||||
})
|
||||
const res = []
|
||||
for(const p in list){
|
||||
const chatArr = list[p]
|
||||
let readNum = 0
|
||||
chatArr.forEach((d:any) => {
|
||||
if(!d.isRead){
|
||||
readNum++
|
||||
}
|
||||
})
|
||||
const last = chatArr.pop()
|
||||
last.readNum = readNum
|
||||
res.push(last)
|
||||
}
|
||||
contentList.value = res.sort((a, b) => b.createdAt - a.createdAt);
|
||||
}
|
||||
const clearContentList = (targetId:number) => {
|
||||
contentList.value.forEach((d : any) => {
|
||||
if(d.targetId === targetId) {
|
||||
d.readNum = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
const clearMsg = async () => {
|
||||
if (chatTargetIp.value === '') return
|
||||
await db.deleteByField('chatmsg','targetIp', chatTargetIp.value)
|
||||
msgList.value = []
|
||||
}
|
||||
|
||||
const updateContentList = async (msg:any) => {
|
||||
if(msg.isMe)return;
|
||||
const has = contentList.value.find((d:any) => d.targetIp === msg.targetIp)
|
||||
if(has){
|
||||
contentList.value.forEach((d: any, index: number) => {
|
||||
if (d.targetIp === msg.targetIp) {
|
||||
if (!msg.isRead) {
|
||||
msg.readNum = d.readNum + 1;
|
||||
} else {
|
||||
msg.readNum = 0;
|
||||
}
|
||||
// 直接替换数组中的元素以触发更新
|
||||
contentList.value.splice(index, 1, msg);
|
||||
}
|
||||
});
|
||||
//console.log(contentList.value)
|
||||
}else{
|
||||
if(msg.isRead){
|
||||
msg.readNum = 0
|
||||
}else{
|
||||
msg.readNum = 1
|
||||
}
|
||||
contentList.value.unshift(msg)
|
||||
//console.log(contentList.value)
|
||||
}
|
||||
contentList.value = contentList.value.sort((a: any, b: any) => b.createdAt - a.createdAt)
|
||||
}
|
||||
const init = async() => {
|
||||
await getUserList()
|
||||
await initUserList()
|
||||
await initContentList()
|
||||
}
|
||||
const initUserList = async() => {
|
||||
if(userList.value.length > 0) {
|
||||
const updates : any = []
|
||||
userList.value.forEach((d: any) => {
|
||||
if (d.isOnline) {
|
||||
updates.push({
|
||||
key: d.id,
|
||||
changes: {
|
||||
isOnline: false
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
await db.table('chatuser').bulkUpdate(updates)
|
||||
}
|
||||
}
|
||||
const refreshUserList = async () => {
|
||||
await db.clear('chatuser')
|
||||
userList.value = []
|
||||
}
|
||||
const getMsgList = async () => {
|
||||
if (chatTargetId.value < 1)return;
|
||||
//msgList.value = await db.getByField('chatmsg', 'targetId', chatTargetId.value)
|
||||
//msgList.value = await db.pageSearch('chatmsg', currentPage.value, pageSize.value, { targetId: chatTargetId.value })
|
||||
const offset = (currentPage.value - 1) * pageSize.value
|
||||
const list = await db.table('chatmsg')
|
||||
.where({ targetIp: chatTargetIp.value })
|
||||
.desc()
|
||||
.offset(offset)
|
||||
.limit(pageSize.value)
|
||||
.toArray();
|
||||
list.sort((a:any,b:any) => a.id > b.id)
|
||||
msgList.value = list;
|
||||
}
|
||||
const moreMsgList = async() => {
|
||||
if (chatTargetId.value < 1) return;
|
||||
//const list = await db.pageSearch('chatmsg', currentPage.value + 1, pageSize.value, { targetId: chatTargetId.value })
|
||||
const offset = currentPage.value * pageSize.value
|
||||
const list = await db.table('chatmsg')
|
||||
.where({ targetIp: chatTargetIp.value })
|
||||
.desc()
|
||||
.offset(offset)
|
||||
.limit(pageSize.value)
|
||||
.toArray();
|
||||
if(list && list.length > 0) {
|
||||
list.sort((a: any, b: any) => a.id > b.id)
|
||||
currentPage.value = currentPage.value + 1
|
||||
msgList.value = [...list, ...msgList.value];
|
||||
}
|
||||
}
|
||||
const getUserList = async () => {
|
||||
const list = await db.getAll('chatuser')
|
||||
list.sort((a: any, b: any) => a.updatedAt > b.updatedAt)
|
||||
userList.value = list
|
||||
}
|
||||
|
||||
const setUserList = async (data:any) => {
|
||||
//console.log(data)
|
||||
if(data.length < 1){
|
||||
return
|
||||
}
|
||||
hostInfo.value = data[0]
|
||||
data.shift()
|
||||
const ips:any = []
|
||||
userList.value.forEach((d : any) => {
|
||||
ips.push(d.ip)
|
||||
});
|
||||
const has:any = []
|
||||
const nothas:any = []
|
||||
data.forEach((d:any) => {
|
||||
if(ips.includes(d.ip)){
|
||||
has.push(d.ip)
|
||||
}else{
|
||||
nothas.push(d)
|
||||
}
|
||||
})
|
||||
if(has.length > 0) {
|
||||
const updates:any = []
|
||||
userList.value.forEach((d: any) => {
|
||||
if(has.includes(d.ip)){
|
||||
updates.push({
|
||||
key : d.id,
|
||||
changes : {
|
||||
isOnline : true,
|
||||
updatedAt:Date.now()
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
await db.table('chatuser').bulkUpdate(updates)
|
||||
}
|
||||
if(nothas.length > 0) {
|
||||
nothas.forEach((d:any) => {
|
||||
d.isOnline = true
|
||||
d.username = data.hostname
|
||||
d.createdAt = Date.now()
|
||||
d.updatedAt = Date.now()
|
||||
})
|
||||
await db.table('chatuser').bulkAdd(nothas)
|
||||
}
|
||||
await getUserList()
|
||||
}
|
||||
const addFile = async (data:any) => {
|
||||
const targetUser:any = await getTargetUser(data)
|
||||
const files:any = []
|
||||
data.fileList.forEach((d:any) => {
|
||||
d.save_path = d.save_path.replace(/\\/g, "/");
|
||||
files.push({
|
||||
name : d.name,
|
||||
path : d.save_path,
|
||||
ext : d.save_path.split('.').pop(),
|
||||
content: d.content
|
||||
})
|
||||
})
|
||||
const saveMsg: any = {
|
||||
type: 'file',
|
||||
targetId: targetUser.id,
|
||||
targetIp: targetUser.ip,
|
||||
content: files,
|
||||
reciperInfo: data.senderInfo,
|
||||
createdAt: Date.now(),
|
||||
isMe: false,
|
||||
isRead: false,
|
||||
status: 'reciped'
|
||||
}
|
||||
if (targetUser.id === chatTargetId.value) {
|
||||
saveMsg.readAt = Date.now()
|
||||
saveMsg.isRead = true
|
||||
msgList.value.push(saveMsg)
|
||||
}
|
||||
console.log(saveMsg)
|
||||
await db.addOne('chatmsg', saveMsg)
|
||||
//await getMsgList()
|
||||
|
||||
await updateContentList(saveMsg)
|
||||
if (config.storeType === 'browser') {
|
||||
await storeFile(files)
|
||||
}
|
||||
handleSelect(1)
|
||||
}
|
||||
const storeFile = async(fileList : any) => {
|
||||
if (fileList.length < 1) return;
|
||||
console.log(fileList)
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
let content = fileList[i].content
|
||||
if(typeof content === 'string') {
|
||||
if(isBase64(content)){
|
||||
content = base64ToBuffer(content);
|
||||
}
|
||||
const path = dirname(fileList[i].path)
|
||||
//console.log(path)
|
||||
await sys?.fs.mkdir(path);
|
||||
await sys?.fs.writeFile(fileList[i].path, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
const getTargetUser = async (data:any) => {
|
||||
let targetUser:any = userList.value.find((d: any) => d.ip === data.senderInfo.ip)
|
||||
if (!targetUser){
|
||||
targetUser = {
|
||||
isOnline : true,
|
||||
ip:data.senderInfo.ip,
|
||||
hostname: data.senderInfo.hostname,
|
||||
username : data.senderInfo.hostname,
|
||||
createdAt : Date.now(),
|
||||
updatedAt : Date.now()
|
||||
}
|
||||
targetUser.id = await db.addOne("chatuser",targetUser)
|
||||
userList.value.unshift(targetUser)
|
||||
}
|
||||
return targetUser
|
||||
}
|
||||
const addText = async (data:any) => {
|
||||
const targetUser:any = await getTargetUser(data)
|
||||
|
||||
const saveMsg:any = {
|
||||
type: 'text',
|
||||
targetId: targetUser.id,
|
||||
targetIp: targetUser.ip,
|
||||
content: data.content,
|
||||
reciperInfo: data.senderInfo,
|
||||
createdAt: Date.now(),
|
||||
isMe: false,
|
||||
isRead: false,
|
||||
status: 'reciped'
|
||||
}
|
||||
if (targetUser.id === chatTargetId.value){
|
||||
saveMsg.readAt = Date.now()
|
||||
saveMsg.isRead = true
|
||||
msgList.value.push(saveMsg)
|
||||
}
|
||||
//console.log(saveMsg)
|
||||
await db.addOne('chatmsg', saveMsg)
|
||||
//await getMsgList()
|
||||
|
||||
await updateContentList(saveMsg)
|
||||
handleSelect(1)
|
||||
}
|
||||
const sendMsg = async () => {
|
||||
if(chatTargetId.value < 1) {
|
||||
return
|
||||
}
|
||||
const saveMsg:any = {
|
||||
type : 'text',
|
||||
targetId : chatTargetId.value,
|
||||
targetIp: chatTargetIp.value,
|
||||
content: sendInfo.value.trim(),
|
||||
senderInfo: toRaw(hostInfo.value),
|
||||
createdAt:Date.now(),
|
||||
isMe:true,
|
||||
isRead:false,
|
||||
status:'sending'
|
||||
}
|
||||
//console.log(saveMsg)
|
||||
const msgId = await db.addOne('chatmsg', saveMsg)
|
||||
//await getMsgList()
|
||||
msgList.value.push(saveMsg)
|
||||
const targetUser = userList.value.find((d: any) => d.id === chatTargetId.value)
|
||||
//console.log(targetUser)
|
||||
if(targetUser.isOnline) {
|
||||
const postUrl = `http://${targetUser.ip}:56780/message`
|
||||
const completion = await fetch(postUrl, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(saveMsg),
|
||||
})
|
||||
if (!completion.ok) {
|
||||
console.log(completion)
|
||||
}else{
|
||||
saveMsg.isRead = true
|
||||
saveMsg.status = 'sended'
|
||||
saveMsg.readAt = Date.now()
|
||||
await db.update('chatmsg', msgId, saveMsg)
|
||||
|
||||
}
|
||||
}
|
||||
sendInfo.value = ""
|
||||
await updateContentList(saveMsg)
|
||||
|
||||
}
|
||||
//上传文件资源
|
||||
async function uploadFile(paths:any) {
|
||||
if (chatTargetId.value < 1) {
|
||||
return
|
||||
}
|
||||
const targetUser = userList.value.find((d: any) => d.id === chatTargetId.value)
|
||||
if (!targetUser.isOnline) {
|
||||
notifyError("The user is not online!");
|
||||
return;
|
||||
}
|
||||
if (!hostInfo.value || !hostInfo.value.ip) {
|
||||
notifyError("Please wait for a moment");
|
||||
return;
|
||||
}
|
||||
//console.log(paths)
|
||||
const formData = new FormData();
|
||||
const errstr:any = []
|
||||
const files:any = []
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const content = await sys?.fs.readFile(paths[i]);
|
||||
let blobContent;
|
||||
if(!content || content == ''){
|
||||
errstr.push(paths[i] + " is empty")
|
||||
continue
|
||||
}
|
||||
if (content instanceof ArrayBuffer) {
|
||||
blobContent = new Blob([content]);
|
||||
}
|
||||
else if (typeof content === 'string') {
|
||||
if (isBase64(content)) {
|
||||
const base64 = base64ToBuffer(content);
|
||||
blobContent = new Blob([base64]);
|
||||
} else {
|
||||
blobContent = new Blob([content], { type: "text/plain;charset=utf-8" });
|
||||
}
|
||||
}
|
||||
else {
|
||||
errstr.push(paths[i] + " type is error")
|
||||
continue
|
||||
}
|
||||
const fileName = paths[i].split("/").pop()
|
||||
files.push({
|
||||
name: fileName,
|
||||
path: paths[i],
|
||||
ext : fileName.split(".").pop(),
|
||||
})
|
||||
//files.push(blobContent);
|
||||
formData.append(`files`, blobContent, fileName);
|
||||
}
|
||||
if(errstr.length > 0) {
|
||||
errstr.forEach((d:any) => {
|
||||
notifyError(d);
|
||||
})
|
||||
return
|
||||
}
|
||||
//formData.append("files", files);
|
||||
formData.append("ip", hostInfo.value.ip);
|
||||
formData.append("hostname", hostInfo.value.hostname);
|
||||
//console.log(formData)
|
||||
const postUrl = `http://${targetUser.ip}:56780/upload`
|
||||
const res = await fetch(postUrl, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.log(res);
|
||||
notifyError("Upload error!");
|
||||
return;
|
||||
}
|
||||
const saveMsg: any = {
|
||||
type: 'file',
|
||||
targetId: targetUser.id,
|
||||
targetIp: targetUser.ip,
|
||||
content: files,
|
||||
reciperInfo: toRaw(targetUser),
|
||||
createdAt: Date.now(),
|
||||
isMe: false,
|
||||
isRead: true,
|
||||
status: 'reciped'
|
||||
}
|
||||
console.log(saveMsg)
|
||||
await db.addOne('chatmsg', saveMsg)
|
||||
msgList.value.push(saveMsg)
|
||||
|
||||
notifySuccess("upload success!");
|
||||
}
|
||||
return {
|
||||
userList,
|
||||
navList,
|
||||
sendInfo,
|
||||
navId,
|
||||
chatTargetId,
|
||||
chatTargetIp,
|
||||
msgList,
|
||||
hostInfo,
|
||||
contentList,
|
||||
emojiList,
|
||||
showChooseFile,
|
||||
pageSize,
|
||||
init,
|
||||
setUserList,
|
||||
getUserList,
|
||||
handleSelect,
|
||||
setChatId,
|
||||
sendMsg,
|
||||
addText,
|
||||
addFile,
|
||||
uploadFile,
|
||||
moreMsgList,
|
||||
refreshUserList,
|
||||
clearMsg
|
||||
}
|
||||
})
|
||||
|
|
@ -181,20 +181,18 @@ export const appList = [
|
|||
ext: ['bb'],
|
||||
eventType: "exportBaiban"
|
||||
},
|
||||
// {
|
||||
// name: 'localchat',
|
||||
// appIcon: "chat",
|
||||
// content: Chat,
|
||||
// view: Chat,
|
||||
// frame: true,
|
||||
// width: 800,
|
||||
// height: 600,
|
||||
// center: true,
|
||||
// resizable: true,
|
||||
// isDeskTop: true,
|
||||
// isMagnet: false,
|
||||
// ext: []
|
||||
// },
|
||||
{
|
||||
name: 'localchat',
|
||||
appIcon: "chat",
|
||||
content: "Chat",
|
||||
frame: true,
|
||||
width: 800,
|
||||
height: 600,
|
||||
center: true,
|
||||
resizable: true,
|
||||
isDeskTop: true,
|
||||
isMagnet: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'piceditor',
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const getSystemConfig = (ifset = false) => {
|
|||
}
|
||||
// 初始化API相关URL,若本地存储中已存在则不进行覆盖
|
||||
if (!config.apiUrl) {
|
||||
config.apiUrl = 'http://localhost:56710';
|
||||
config.apiUrl = 'http://localhost:56780';
|
||||
}
|
||||
|
||||
// 初始化用户信息,若本地存储中已存在则不进行覆盖
|
||||
|
|
|
|||
111
frontend/src/util/update.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//import { EventsOff, EventsOn } from '~/runtime';
|
||||
import manifest from '../../package.json';
|
||||
export async function checkUpdate() {
|
||||
if(!(window as any).go) return;
|
||||
const updateGiteeUrl = `https://gitee.com/api/v5/repos/ruitao_admin/godoos/releases/`
|
||||
const releaseRes = await fetch(updateGiteeUrl)
|
||||
if(!releaseRes.ok) return;
|
||||
const releaseData = await releaseRes.json()
|
||||
const versionTag = releaseData.tag_name;
|
||||
if(!versionTag) return;
|
||||
if (versionTag.replace('v', '') <= manifest.version) return;
|
||||
const verifyUrl = `${updateGiteeUrl}tags/${versionTag}`;
|
||||
const verRes = await fetch(verifyUrl);
|
||||
if(!verRes.ok) return;
|
||||
const verData = await verRes.json()
|
||||
if(!verData.assets || verData.assets.length <= 0) return;
|
||||
const updateUrl = `${updateGiteeUrl}download/${versionTag}/${asset.name}`;
|
||||
|
||||
fetch(`${updateGiteeUrl}latest`).then((r) => {
|
||||
if (r.ok) {
|
||||
r.json().then((data) => {
|
||||
if (data.tag_name) {
|
||||
// const versionTag = data.tag_name;
|
||||
// console.log(versionTag)
|
||||
// if (versionTag.replace('v', '') > manifest.version) {
|
||||
// const verifyUrl = `${updateGiteeUrl}tags/${versionTag}`;
|
||||
// }
|
||||
/*
|
||||
if (versionTag.replace('v', '') > manifest.version) {
|
||||
const verifyUrl = `${updateGiteeUrl}tags/${versionTag}`;
|
||||
|
||||
fetch(verifyUrl).then((r) => {
|
||||
if (r.ok) {
|
||||
r.json().then((data) => {
|
||||
if (data.assets && data.assets.length > 0) {
|
||||
const asset = data.assets.find((a: any) => a.name.toLowerCase().includes(commonStore.platform.toLowerCase().replace('darwin', 'macos')));
|
||||
if (asset) {
|
||||
const updateUrl = `${updateGiteeUrl}download/${versionTag}/${asset.name}`;
|
||||
toastWithButton(t('New Version Available') + ': ' + versionTag, t('Update'), () => {
|
||||
DeleteFile('cache.json');
|
||||
const progressId = 'update_app';
|
||||
const progressEvent = 'updateApp';
|
||||
const updateProgress = (ds: DownloadStatus | null) => {
|
||||
const content =
|
||||
t('Downloading update, please wait. If it is not completed, please manually download the program from GitHub and replace the original program.')
|
||||
+ (ds ? ` (${ds.progress.toFixed(2)}% ${bytesToReadable(ds.transferred)}/${bytesToReadable(ds.size)})` : '');
|
||||
const options: ToastOptions = {
|
||||
type: 'info',
|
||||
position: 'bottom-left',
|
||||
autoClose: false,
|
||||
toastId: progressId,
|
||||
hideProgressBar: false,
|
||||
progress: ds ? ds.progress / 100 : 0
|
||||
};
|
||||
if (toast.isActive(progressId))
|
||||
toast.update(progressId, {
|
||||
render: content,
|
||||
...options
|
||||
});
|
||||
else
|
||||
toast(content, options);
|
||||
};
|
||||
updateProgress(null);
|
||||
EventsOn(progressEvent, updateProgress);
|
||||
UpdateApp(updateUrl).then(() => {
|
||||
toast(t('Update completed, please restart the program.'), {
|
||||
type: 'success',
|
||||
position: 'bottom-left',
|
||||
autoClose: false
|
||||
}
|
||||
);
|
||||
}).catch((e) => {
|
||||
toast(t('Update Error') + ' - ' + (e.message || e), {
|
||||
type: 'error',
|
||||
position: 'bottom-left',
|
||||
autoClose: false
|
||||
});
|
||||
}).finally(() => {
|
||||
toast.dismiss(progressId);
|
||||
EventsOff(progressEvent);
|
||||
});
|
||||
}, {
|
||||
autoClose: false,
|
||||
position: 'bottom-left'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error('Verify response was not ok.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (notifyEvenLatest) {
|
||||
toast(t('This is the latest version'), { type: 'success', position: 'bottom-left', autoClose: 2000 });
|
||||
}
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
throw new Error('Invalid response.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error('Network response was not ok.');
|
||||
}
|
||||
}
|
||||
).catch((e) => {
|
||||
//toast(t('Updates Check Error') + ' - ' + (e.message || e), { type: 'error', position: 'bottom-left' });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -34,10 +34,11 @@
|
|||
"baseUrl": "./",
|
||||
"outDir": "./",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"~/*": ["./wailsjs/*"],
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts","src/**/**/*.ts", "src/**/*", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"include": ["src/**/*.ts","wailsjs/**/*.ts","wailsjs/**/*.js","src/**/**/*.ts", "src/**/*", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ export default defineConfig(async () => ({
|
|||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, 'src')
|
||||
"@": path.resolve(__dirname, 'src'),
|
||||
"~": path.resolve(__dirname, 'wailsjs')
|
||||
},
|
||||
},
|
||||
build:{
|
||||
|
|
|
|||
3
go.mod
|
|
@ -5,12 +5,15 @@ go 1.21
|
|||
toolchain go1.22.5
|
||||
|
||||
require (
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/minio/selfupdate v0.6.0
|
||||
github.com/shirou/gopsutil v2.21.11+incompatible
|
||||
github.com/wailsapp/wails/v2 v2.9.1
|
||||
)
|
||||
|
||||
require (
|
||||
aead.dev/minisign v0.2.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
|
|
|
|||
17
go.sum
|
|
@ -1,5 +1,9 @@
|
|||
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
|
||||
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -39,6 +43,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
|
|||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
|
||||
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
|
|
@ -75,18 +81,27 @@ github.com/wailsapp/wails/v2 v2.9.1 h1:irsXnoQrCpeKzKTYZ2SUVlRRyeMR6I0vCO9Q1cvlE
|
|||
github.com/wailsapp/wails/v2 v2.9.1/go.mod h1:7maJV2h+Egl11Ak8QZN/jlGLj2wg05bsQS+ywJPT0gI=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
@ -95,7 +110,9 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
|
|
|
|||
142
localchat/client.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
package localchat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// func StartServiceDiscovery() {
|
||||
// conn, err := net.ListenPacket("udp", broadcastAddr)
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return
|
||||
// }
|
||||
// defer conn.Close()
|
||||
|
||||
// buffer := make([]byte, 1024)
|
||||
// for {
|
||||
// n, addr, err := conn.ReadFrom(buffer)
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// continue
|
||||
// }
|
||||
// fmt.Printf("Received message: %s from %s\n", buffer[:n], addr)
|
||||
// var udpMsg UdpMessage
|
||||
// err = json.Unmarshal(buffer[:n], &udpMsg)
|
||||
// if err != nil {
|
||||
// fmt.Printf("Error unmarshalling JSON: %v\n", err)
|
||||
// continue
|
||||
// }
|
||||
// log.Printf("Get message: %+v", udpMsg)
|
||||
// OnlineUsers[udpMsg.IP] = udpMsg
|
||||
// }
|
||||
// }
|
||||
func DiscoverServers() {
|
||||
broadcastTicker := time.NewTicker(broadcartTime)
|
||||
done := make(chan struct{}) // New channel to signal when to stop
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-broadcastTicker.C:
|
||||
conn, err := net.Dial("udp", broadcastAddr)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
myIP, myHostname, err := getMyIPAndHostname()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get my IP and hostname: %v", err)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
msg := UdpMessage{
|
||||
Type: "online",
|
||||
Hostname: myHostname,
|
||||
IP: myIP,
|
||||
Message: time.Now().Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
jsonData, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal message to JSON: %v", err)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
log.Printf("Sending message: %+v", msg)
|
||||
_, err = conn.Write(jsonData)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
conn.Close() // Close the connection after use
|
||||
case <-done: // Signal to stop
|
||||
broadcastTicker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// You might have some condition here to decide when to stop the ticker
|
||||
// For example, a signal handling mechanism or a specific duration.
|
||||
// After that condition is met, you would call:
|
||||
// close(done)
|
||||
}
|
||||
|
||||
// 获取自己的IP地址和主机名
|
||||
func getMyIPAndHostname() (string, string, error) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get hostname: %w", err)
|
||||
}
|
||||
|
||||
addrs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get network interfaces: %w", err)
|
||||
}
|
||||
|
||||
var preferredIP net.IP
|
||||
for _, iface := range addrs {
|
||||
if iface.Flags&net.FlagUp == 0 {
|
||||
// Skip interfaces that are not up
|
||||
continue
|
||||
}
|
||||
ifAddrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue // Ignore this interface if we can't get its addresses
|
||||
}
|
||||
for _, addr := range ifAddrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if ip.IsLoopback() {
|
||||
continue // Skip loopback addresses
|
||||
}
|
||||
if ip.To4() != nil && (ip.IsPrivate() || ip.IsGlobalUnicast()) {
|
||||
// Prefer global unicast or private addresses over link-local
|
||||
preferredIP = ip
|
||||
break
|
||||
}
|
||||
}
|
||||
if preferredIP != nil {
|
||||
// Found a preferred IP, break out of the loop
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if preferredIP == nil {
|
||||
return "", "", fmt.Errorf("no preferred non-loopback IPv4 address found")
|
||||
}
|
||||
|
||||
return preferredIP.String(), hostname, nil
|
||||
}
|
||||
44
localchat/file.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package localchat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetChatPath() (string, error) {
|
||||
baseDir, err := getAppDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
modelDir := filepath.Join(baseDir, "C", "Users", "Reciv", time.Now().Format("2006-01-02"))
|
||||
if !pathExists(modelDir) {
|
||||
os.MkdirAll(modelDir, 0755)
|
||||
}
|
||||
return modelDir, nil
|
||||
}
|
||||
func getAppDir() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
return filepath.Join(homeDir, ".godoos", "os"), nil
|
||||
}
|
||||
func pathExists(dir string) bool {
|
||||
_, err := os.Stat(dir)
|
||||
if err == nil {
|
||||
//log.Println("文件夹存在")
|
||||
return true
|
||||
} else if os.IsNotExist(err) {
|
||||
//log.Println("文件夹不存在")
|
||||
return false
|
||||
} else if os.IsExist(err) {
|
||||
//log.Println("文件夹存在")
|
||||
return true
|
||||
} else {
|
||||
log.Println("发生错误:", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
84
localchat/server.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package localchat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
|
||||
func StartServiceDiscovery() {
|
||||
conn, err := net.ListenPacket("udp", broadcastAddr)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
n, addr, err := conn.ReadFrom(buffer)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Received message: %s from %s\n", buffer[:n], addr)
|
||||
var udpMsg UdpMessage
|
||||
err = json.Unmarshal(buffer[:n], &udpMsg)
|
||||
if err != nil {
|
||||
fmt.Printf("Error unmarshalling JSON: %v\n", err)
|
||||
continue
|
||||
}
|
||||
log.Printf("Get message: %+v", udpMsg)
|
||||
OnlineUsers[udpMsg.IP] = udpMsg
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func StartServiceDiscovery(port int, clientPort int) {
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
serviceAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to resolve UDP address: %v", err)
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP("udp", serviceAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to listen on UDP: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get hostname: %v", err)
|
||||
}
|
||||
|
||||
for {
|
||||
// 监听UDP广播请求
|
||||
_, _, err := conn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
log.Printf("Error reading from UDP: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 构建广播消息,包含IP和主机名
|
||||
ip, _, _ := net.SplitHostPort(serviceAddr.String())
|
||||
response := fmt.Sprintf("Chat Server is running at IP: %s, Hostname: %s", ip, hostname)
|
||||
broadcastMessage(conn, response, clientPort)
|
||||
}
|
||||
}
|
||||
|
||||
func broadcastMessage(conn *net.UDPConn, message string, clientPort int) {
|
||||
// 广播给所有客户端
|
||||
broadcastAddr := &net.UDPAddr{IP: net.ParseIP("255.255.255.255"), Port: clientPort}
|
||||
_, err := conn.WriteToUDP([]byte(message), broadcastAddr)
|
||||
if err != nil {
|
||||
log.Printf("Failed to broadcast message: %v", err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
112
localchat/sse.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package localchat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
go StartServiceDiscovery()
|
||||
go DiscoverServers()
|
||||
}
|
||||
|
||||
func SseHandler(w http.ResponseWriter, r *http.Request) {
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
ticker := time.NewTicker(broadcartTime) // 每3秒检查一次在线用户
|
||||
defer ticker.Stop()
|
||||
// 处理新消息
|
||||
ctx := r.Context()
|
||||
// 使用Context来监听请求的取消
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Recovered in SSE goroutine: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done(): // 当请求被取消时,退出循环
|
||||
return
|
||||
case msg := <-messageChan:
|
||||
// 构造JSON数据
|
||||
jsonData, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal message to JSON: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 通过SSE发送JSON数据
|
||||
fmt.Fprintf(w, "data: %s\n\n", string(jsonData))
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}()
|
||||
myIP, myHostname, err := getMyIPAndHostname()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get my IP and hostname: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C: // 每隔一段时间检查并广播在线用户
|
||||
var userList []UdpMessage
|
||||
// 首先将自己的IP和主机名放入列表
|
||||
myMsg := UdpMessage{
|
||||
IP: myIP,
|
||||
Hostname: myHostname,
|
||||
Type: "online",
|
||||
Message: time.Now().Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
userList = append(userList, myMsg)
|
||||
log.Printf("Online users: %v", OnlineUsers)
|
||||
for ip, info := range OnlineUsers {
|
||||
if ip != myIP { // 确保不重复添加自己
|
||||
userList = append(userList, info)
|
||||
}
|
||||
}
|
||||
res := UserList{Type: "user_list", Content: userList}
|
||||
// 将用户列表转换为JSON字符串
|
||||
jsonData, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal online users to JSON: %v", err)
|
||||
continue
|
||||
}
|
||||
// 通过SSE发送JSON数据
|
||||
fmt.Fprintf(w, "data: %s\n\n", string(jsonData))
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
func HandleMessage(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var msg Message
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(&msg); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
// 将消息放入messageChan
|
||||
messageChan <- msg
|
||||
log.Printf("Received text message from %s: %s", msg.SenderInfo.IP, msg.Content)
|
||||
// 这里可以添加存储文本消息到数据库或其他处理逻辑
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, "Text message send successfully")
|
||||
}
|
||||
61
localchat/type.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package localchat
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// type SenderInfo struct {
|
||||
// SenderIP string `json:"sender_ip"`
|
||||
// Username string `json:"username"`
|
||||
// }
|
||||
type Message struct {
|
||||
Type string `json:"type"` // 消息类型,如"text"、"image"等
|
||||
Content string `json:"content"` // 消息内容
|
||||
SenderInfo UserInfo `json:"senderInfo"` // 发送者的IP地址
|
||||
FileInfo FilePartInfo `json:"fileInfo"`
|
||||
FileList []UploadInfo `json:"fileList"`
|
||||
}
|
||||
type UserInfo struct {
|
||||
IP string `json:"ip"`
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
type UdpMessage struct {
|
||||
Type string `json:"type"`
|
||||
IP string `json:"ip"`
|
||||
Hostname string `json:"hostname"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
type UserList struct {
|
||||
Type string `json:"type"`
|
||||
Content []UdpMessage `json:"content"`
|
||||
}
|
||||
type UploadInfo struct {
|
||||
Name string `json:"name"`
|
||||
SavePath string `json:"save_path"`
|
||||
Content string `json:"content"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// 文件分片信息
|
||||
type FilePartInfo struct {
|
||||
FileName string `json:"filename"`
|
||||
PartNumber int `json:"part_number"`
|
||||
TotalParts int `json:"total_parts"`
|
||||
}
|
||||
|
||||
// 分片上传状态跟踪
|
||||
type UploadStatus struct {
|
||||
sync.Mutex
|
||||
Status map[string]int // key: fileName, value: 已上传分片数
|
||||
}
|
||||
|
||||
var (
|
||||
messageChan = make(chan Message, 100) // 缓存大小根据实际情况设定
|
||||
)
|
||||
var uploadStatus = UploadStatus{Status: make(map[string]int)}
|
||||
var broadcartTime = 3 * time.Second
|
||||
|
||||
var broadcastAddr = "224.0.0.1:1679" // 多播地址
|
||||
// var broadcastAddr = "255.255.255.255:1769" // 广播地址
|
||||
var OnlineUsers = make(map[string]UdpMessage) // 全局map,key为IP,value为主机名
|
||||
204
localchat/upload.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package localchat
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 合并文件分片
|
||||
func mergeFiles(fileName string, totalParts int, chatDir string) error {
|
||||
var parts []io.Reader
|
||||
for i := 1; i <= totalParts; i++ {
|
||||
filePath := fmt.Sprintf("%v%v_%v.part", chatDir, fileName, i)
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open part %d: %w", i, err)
|
||||
}
|
||||
defer file.Close()
|
||||
parts = append(parts, file)
|
||||
}
|
||||
|
||||
mergedFilePath := fmt.Sprintf("%vmerged_%v", chatDir, fileName)
|
||||
mergedFile, err := os.Create(mergedFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create merged file: %w", err)
|
||||
}
|
||||
defer mergedFile.Close()
|
||||
|
||||
_, err = io.Copy(mergedFile, io.MultiReader(parts...))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to merge files: %w", err)
|
||||
}
|
||||
|
||||
// 合并后清理分片文件
|
||||
for i := 1; i <= totalParts; i++ {
|
||||
os.Remove(fmt.Sprintf("%v%v_%v.part", chatDir, fileName, i))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UploadBigFileHandler(w http.ResponseWriter, r *http.Request, msg Message) {
|
||||
|
||||
chatDir, err := GetChatPath()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get chat path", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建或打开临时文件以写入分片
|
||||
tempFilePath := fmt.Sprintf("%v%v_%v.part", chatDir, msg.FileInfo.FileName, msg.FileInfo.PartNumber)
|
||||
out, err := os.Create(tempFilePath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create temp file: %v", err)
|
||||
http.Error(w, "Failed to create temp file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// 将请求体的内容写入临时文件
|
||||
_, err = io.Copy(out, r.Body)
|
||||
if err != nil {
|
||||
log.Printf("Failed to write file part: %v", err)
|
||||
http.Error(w, "Failed to write file part", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新上传状态
|
||||
uploadStatus.Lock()
|
||||
uploadStatus.Status[msg.FileInfo.FileName]++
|
||||
if uploadStatus.Status[msg.FileInfo.FileName] == msg.FileInfo.TotalParts {
|
||||
// 所有分片上传完成,触发合并
|
||||
go func() {
|
||||
err := mergeFiles(msg.FileInfo.FileName, msg.FileInfo.TotalParts, chatDir)
|
||||
if err != nil {
|
||||
log.Printf("Failed to merge files for %v: %v", msg.FileInfo.FileName, err)
|
||||
} else {
|
||||
log.Printf("Merged file %v successfully", msg.FileInfo.FileName)
|
||||
}
|
||||
// 清理状态记录
|
||||
delete(uploadStatus.Status, msg.FileInfo.FileName)
|
||||
msg.Content = "uploaded"
|
||||
messageChan <- msg
|
||||
}()
|
||||
}
|
||||
uploadStatus.Unlock()
|
||||
|
||||
// 返回成功响应
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
fmt.Fprintln(w, "File part uploaded successfully")
|
||||
}
|
||||
|
||||
// SaveContentToFile 保存内容到文件并返回UploadInfo结构体
|
||||
func SaveContentToFile(content, fileName string) (UploadInfo, error) {
|
||||
uploadBaseDir, err := GetChatPath()
|
||||
if err != nil {
|
||||
return UploadInfo{}, err
|
||||
}
|
||||
appDir, err := getAppDir()
|
||||
if err != nil {
|
||||
return UploadInfo{}, err
|
||||
}
|
||||
|
||||
// 去除文件名中的空格
|
||||
fileNameWithoutSpaces := strings.ReplaceAll(fileName, " ", "_")
|
||||
fileNameWithoutSpaces = strings.ReplaceAll(fileNameWithoutSpaces, "/", "")
|
||||
fileNameWithoutSpaces = strings.ReplaceAll(fileNameWithoutSpaces, `\`, "")
|
||||
// 提取文件名和扩展名
|
||||
// 查找最后一个点的位置
|
||||
lastDotIndex := strings.LastIndexByte(fileNameWithoutSpaces, '.')
|
||||
|
||||
// 如果找到点,则提取扩展名,否则视为没有扩展名
|
||||
ext := ""
|
||||
if lastDotIndex != -1 {
|
||||
ext = fileNameWithoutSpaces[lastDotIndex:]
|
||||
fileNameWithoutSpaces = fileNameWithoutSpaces[:lastDotIndex]
|
||||
} else {
|
||||
ext = ""
|
||||
}
|
||||
randFileName := fmt.Sprintf("%s_%s%s", fileNameWithoutSpaces, strconv.FormatInt(time.Now().UnixNano(), 10), ext)
|
||||
savePath := filepath.Join(uploadBaseDir, randFileName)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(savePath), 0755); err != nil {
|
||||
return UploadInfo{}, err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(savePath, []byte(content), 0644); err != nil {
|
||||
return UploadInfo{}, err
|
||||
}
|
||||
content = string(content)
|
||||
// 检查文件内容是否以"link::"开头
|
||||
if !strings.HasPrefix(content, "link::") {
|
||||
content = base64.StdEncoding.EncodeToString([]byte(content))
|
||||
}
|
||||
return UploadInfo{
|
||||
Name: fileNameWithoutSpaces,
|
||||
SavePath: strings.TrimPrefix(savePath, appDir),
|
||||
Content: content,
|
||||
CreatedAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MultiUploadHandler 处理多文件上传请求
|
||||
func MultiUploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(10000 << 20); err != nil {
|
||||
http.Error(w, "Failed to parse multipart form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
files := r.MultipartForm.File["files"]
|
||||
if len(files) == 0 {
|
||||
http.Error(w, "No file parts in the request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fileInfoList := make([]UploadInfo, 0, len(files))
|
||||
|
||||
for _, fileHeader := range files {
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to open uploaded file", http.StatusBadRequest)
|
||||
continue
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read uploaded file", http.StatusBadRequest)
|
||||
continue
|
||||
}
|
||||
//log.Printf(string(content))
|
||||
// 保存上传的文件内容
|
||||
info, err := SaveContentToFile(string(content), fileHeader.Filename)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to save uploaded file", http.StatusBadRequest)
|
||||
continue
|
||||
}
|
||||
log.Println(info.SavePath)
|
||||
|
||||
//info.SavePath = savePath
|
||||
fileInfoList = append(fileInfoList, info)
|
||||
}
|
||||
user := UserInfo{
|
||||
IP: r.FormValue("ip"),
|
||||
Hostname: r.FormValue("hostname"),
|
||||
}
|
||||
msg := Message{
|
||||
Type: "file",
|
||||
Content: "file recieved",
|
||||
SenderInfo: user,
|
||||
FileList: fileInfoList,
|
||||
}
|
||||
messageChan <- msg
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, "File send successfully")
|
||||
//serv.Res(serv.Response{Code: 0, Data: fileInfoList}, w)
|
||||
}
|
||||
117
localchat/windiscovery.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package localchat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
|
||||
func StartServiceDiscovery() {
|
||||
// 解析多播地址
|
||||
addr, err := net.ResolveUDPAddr("udp4", broadcastAddr)
|
||||
if err != nil {
|
||||
fmt.Println("Error resolving multicast address:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 监听本地网络接口上的多播地址
|
||||
conn, err := net.ListenMulticastUDP("udp4", nil, addr)
|
||||
if err != nil {
|
||||
fmt.Println("Error listening on multicast address:", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
// 设置组播通信的网路接口
|
||||
// intf,err := net.InterfaceByName("eth0")
|
||||
// if err != nil {
|
||||
// fmt.Println("Error getting network interface:", err)
|
||||
// return
|
||||
// }
|
||||
// conn.setMulticastInterface(intf)
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
n, addr, err := conn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
log.Printf("Error reading from UDP: %v", err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Received message: %s from %s\n", buffer[:n], addr)
|
||||
|
||||
var udpMsg UdpMessage
|
||||
err = json.Unmarshal(buffer[:n], &udpMsg)
|
||||
if err != nil {
|
||||
fmt.Printf("Error unmarshalling JSON: %v\n", err)
|
||||
continue
|
||||
}
|
||||
log.Printf("Get message: %+v", udpMsg)
|
||||
OnlineUsers[udpMsg.IP] = udpMsg
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// findMulticastInterface 查找适合多播的网络接口
|
||||
func findMulticastInterface() (*net.Interface, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
if strings.HasPrefix(iface.Name, "eth") || strings.HasPrefix(iface.Name, "en") || strings.HasPrefix(iface.Name, "wlan") {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
}
|
||||
if ip != nil && !ip.IsLoopback() {
|
||||
return &iface, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no suitable multicast interface found")
|
||||
}
|
||||
|
||||
// setupWindowsMulticast 为Windows平台设置多播
|
||||
func setupWindowsMulticast(conn net.PacketConn, iface *net.Interface, multicastIP net.IP) error {
|
||||
// 获取文件描述符
|
||||
if sock, err := conn.(*net.UDPConn).File(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer sock.Close()
|
||||
fd := sock.Fd()
|
||||
|
||||
// 加入多播组
|
||||
var mreq windows.RawSockaddrInet4
|
||||
mreq.Family = windows.AF_INET
|
||||
mreq.Port = 0
|
||||
|
||||
// 确保 multicastIP 是 IPv4 并转换为 [4]byte
|
||||
ipv4 := multicastIP.To4()
|
||||
if ipv4 == nil {
|
||||
return errors.New("multicast IP is not an IPv4 address")
|
||||
}
|
||||
copy(mreq.Addr[:], ipv4)
|
||||
|
||||
if err := windows.SetsockoptIPv4MulticastInterface(fd, windows.IPMULTICAST_IF, &mreq); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 绑定到特定接口(可选,根据需求调整)
|
||||
ifaceIndex := uint32(iface.Index)
|
||||
if err := windows.BindToDevice(fd, windows.StringToUTF16Ptr(fmt.Sprintf("%d", ifaceIndex))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}*/
|
||||
8
main.go
|
|
@ -3,6 +3,8 @@ package main
|
|||
import (
|
||||
"embed"
|
||||
|
||||
App "godoos/app"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
|
|
@ -13,7 +15,7 @@ var assets embed.FS
|
|||
|
||||
func main() {
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
app := App.NewApp()
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
|
|
@ -24,8 +26,8 @@ func main() {
|
|||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
//OnShutdown: app.shutdown,
|
||||
OnStartup: app.Startup,
|
||||
OnShutdown: app.Shutdown,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
|
|
|
|||