From 4c1d67b51313882030894d8078b75bcdf1262324 Mon Sep 17 00:00:00 2001 From: Sun <95302870@qq.com> Date: Tue, 9 Jan 2024 14:03:03 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=201.3.0-beta24-01-09=20S?= =?UTF-8?q?quashed=20commit=20of=20the=20following:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 53d1f382c5142f6a388b2d3fa13caef04a48db91 Author: Sun <95302870@qq.com> Date: Tue Jan 9 11:54:33 2024 +0800 系统状态标题加上阴影,beta版本最终优化 commit fc56328765b8a4d902b59f2393f43e10f3c3dac5 Author: Sun <95302870@qq.com> Date: Mon Jan 8 22:37:24 2024 +0800 磁盘监控增加表单验证 commit 3905717d420d8339e0d04443a79e69229cd10a1a Author: Sun <95302870@qq.com> Date: Mon Jan 8 22:36:10 2024 +0800 删除无用文件 commit 89b6b633107832465973656abfdb6e25be156747 Author: Sun <95302870@qq.com> Date: Mon Jan 8 21:10:10 2024 +0800 修改翻译文件为json格式(为了方便引用插件)优化过期登录弹窗多个的问题 commit 2efb31571ebe5997113c75e69f973dd436cef985 Author: Sun <95302870@qq.com> Date: Mon Jan 8 13:56:57 2024 +0800 增加vscode工作区文件 commit bc79b661db2defa36bdfadb13b1038e906e36d7e Author: Sun <95302870@qq.com> Date: Mon Jan 8 13:56:45 2024 +0800 暂时解决依赖循环的问题 commit a24520f8087a45abe08ee7ea864169888754a5fc Author: Sun <95302870@qq.com> Date: Sun Jan 7 20:07:52 2024 +0800 修改设置里面壁纸提示词错误的问题修改首页默认标题 commit 394c6ce20ce33cd3edf43b33ac7c1f5b23cfe89a Author: Sun <95302870@qq.com> Date: Sun Jan 7 14:53:53 2024 +0800 适配多语言 Squashed commit of the following: commit 632f86c0228c68391c01865c7576f3aa0408c102 Author: Sun <95302870@qq.com> Date: Sun Jan 7 14:47:55 2024 +0800 退出的时候清除appstore commit b9d805e49a3c6b2ad38bc8d527cb12cc8709012e Author: Sun <95302870@qq.com> Date: Sun Jan 7 13:55:20 2024 +0800 系统状态监控适配国际化 commit daece99723ec96d210241d2ca4e5a85dc5ae69bd Author: Sun <95302870@qq.com> Date: Sun Jan 7 13:09:46 2024 +0800 适配添加项目页面的国际化配置还有时钟的星期* commit 8ea2b2fe951f6266415c96a197cb8d00faef4058 Author: Sun <95302870@qq.com> Date: Sun Jan 7 12:01:55 2024 +0800 完成适配所有apps国际化 commit 21ef54e0d4afb10f560c8cb7aff666374afe0f87 Author: Sun <95302870@qq.com> Date: Sat Jan 6 21:36:07 2024 +0800 增加读取默认浏览器语言 commit 6f710bbebe63ab2800193f27c71e5c0034f11978 Author: Sun <95302870@qq.com> Date: Sat Jan 6 21:09:58 2024 +0800 登录页面增加语言选择选项 commit cb7c4a89a160ed3ef91ad566ec98e75325e7601f Author: Sun <95302870@qq.com> Date: Sat Jan 6 20:37:16 2024 +0800 首次尝试增加英文语言,并在我的信息设置 commit fb996e17cd11611d30c0e12feee00ddf7b225e32 Author: Sun <95302870@qq.com> Date: Sat Jan 6 18:22:40 2024 +0800 完成基础设置页面的语言国际化适配 commit ffc378a38fa4221a9240b067660614ab43009325 Author: Sun <95302870@qq.com> Date: Sat Jan 6 17:35:13 2024 +0800 增加完善基本配置中的系统状态开关 commit c91eaf3e941dfa91b7feca925109ec7121874fda Merge: 7ebe358 a60f72c Author: Sun <95302870@qq.com> Date: Sat Jan 6 12:57:05 2024 +0800 Merge branch 'feature/monitor2' into dev commit 7ebe35856e423bb10d8078636b0c80e472203a68 Merge: d3e3cf5 779712a Author: Sun <95302870@qq.com> Date: Sat Jan 6 12:56:15 2024 +0800 Merge branch 'feature/footer' into dev commit a60f72c2779a4adee77f4ab161fb4fad21ff0611 Author: Sun <95302870@qq.com> Date: Sat Jan 6 12:55:04 2024 +0800 优化了首页 commit 899c945fff12290f3c81348a3c262400b1a0ce15 Author: Sun <95302870@qq.com> Date: Sat Jan 6 12:54:19 2024 +0800 完成系统监控 commit cdf16277ff85cee5029de3b7ea78b14bc0274623 Author: Sun <95302870@qq.com> Date: Sat Jan 6 12:41:38 2024 +0800 增加删除功能 commit 128af005ebc95b73ecef8873301a61556984fbea Author: Sun <95302870@qq.com> Date: Sat Jan 6 11:56:32 2024 +0800 完成排序保存功能 commit 3f4b3c67f261f21121c8e9f7c14d926f152a5836 Author: Sun <95302870@qq.com> Date: Sat Jan 6 11:46:59 2024 +0800 初步完成了增改查包括磁盘状态 commit a85d90985df45af75f8fbc165021b160d5e6500d Author: Sun <95302870@qq.com> Date: Fri Jan 5 22:19:47 2024 +0800 增加磁盘信息卡片的适配 commit c955afd86134b38620a884e9e6540eda398392e4 Author: Sun <95302870@qq.com> Date: Fri Jan 5 21:36:38 2024 +0800 增加获取磁盘挂载点接口 commit 21e8e8f1b872a7e7989c45b29061f52127dffce5 Author: Sun <95302870@qq.com> Date: Fri Jan 5 21:14:57 2024 +0800 基础完成了增改查cpu和内存状态 commit bdbcd50aa1b1b6958043e26be5b705430312c64c Author: Sun <95302870@qq.com> Date: Fri Jan 5 14:25:09 2024 +0800 优化公共入口组件 commit 9735e67a7d6334e39d4f58b053c32b3be25e7fa8 Author: Sun <95302870@qq.com> Date: Fri Jan 5 14:01:46 2024 +0800 适配三个组件 commit 3a82949afc64147209be046f9298d55096abd0c3 Author: Sun <95302870@qq.com> Date: Fri Jan 5 13:58:58 2024 +0800 优化组件 commit 0d0421c8ebc749889422c416970fd5760f2a5f6a Author: Sun <95302870@qq.com> Date: Fri Jan 5 13:27:29 2024 +0800 初步完成了编辑器 commit 1474f796fc29051b7e0813839dbb6bdc22293990 Author: Sun <95302870@qq.com> Date: Thu Jan 4 16:22:33 2024 +0800 完成大图标小图标切换 commit baf64a927280fdb04579d5afeff1b5d50e56556a Author: Sun <95302870@qq.com> Date: Thu Jan 4 12:08:39 2024 +0800 增加获取各项监控的单独api commit d3e3cf5d58168cad1e2fa3c96e2381c21aabcab4 Author: Sun <95302870@qq.com> Date: Wed Jan 3 20:46:58 2024 +0800 尝试将所有监控放在顶部 commit 8dfec7e4b78cc393fad96df9d452c5f2dd4933bb Author: Sun <95302870@qq.com> Date: Wed Jan 3 20:02:03 2024 +0800 完整横条显示并对容量尺寸单位优化自动识别 commit fe967a93141472970616a5eac416c59510810b64 Author: Sun <95302870@qq.com> Date: Wed Jan 3 18:55:39 2024 +0800 适配显示了cpu、硬盘、内存信息 commit 11ea134be3ce8aca9c1bf6af4610e3bb4e09eca7 Author: Sun <95302870@qq.com> Date: Tue Jan 2 23:14:04 2024 +0800 完成系统监控的基础api接口 commit c447884d77349553864e0e045b83b3a4b67345da Author: Sun <95302870@qq.com> Date: Tue Jan 2 22:11:34 2024 +0800 完成基本的系统监控类库 commit 779712a5da617fa090056a5d549145f687d8db54 Author: Sun <95302870@qq.com> Date: Tue Jan 2 17:14:16 2024 +0800 增加自定义footer --- .vscode/settings.json | 8 +- index.html | 2 +- .../apiData/systemApiStructs/monitor.go | 5 + service/api/api_v1/system/A_ENTER.go | 1 + service/api/api_v1/system/monitor.go | 96 ++++++ service/assets/version | 2 +- service/global/global.go | 1 + service/global/monitor.go | 18 + service/initialize/A_ENTER.go | 2 + .../initialize/systemMonitor/systemMonitor.go | 53 +++ service/lib/cache/base.go | 5 + service/lib/monitor/monitor.go | 149 +++++++++ service/router/system/A_ENTER.go | 1 + service/router/system/monitor.go | 23 ++ src/api/system/systemMonitor.ts | 32 ++ .../svg-icons/clarity-hard-disk-solid.svg | 1 + src/assets/svg-icons/ion-language.svg | 1 + .../material-symbols-memory-alt-rounded.svg | 1 + src/assets/svg-icons/solar-cpu-bold.svg | 1 + src/components/apps/About/index.vue | 14 +- src/components/apps/ImportExport/index.vue | 56 ++-- src/components/apps/ItemGroupManage/index.vue | 29 +- src/components/apps/Style/index.vue | 94 ++++-- src/components/apps/UserInfo/index.vue | 49 ++- src/components/common/ItemCard/index.vue | 53 +++ src/components/common/index.ts | 2 + src/components/deskModule/Clock/index.vue | 15 +- .../AppIconSystemMonitor/CPU.vue | 59 ++++ .../AppIconSystemMonitor/Disk.vue | 69 ++++ .../AppIconSystemMonitor/Memory.vue | 64 ++++ .../AppIconSystemMonitor/index.vue | 118 +++++++ .../SystemMonitor/Edit/DiskEditor/index.vue | 163 +++++++++ .../Edit/GenericProgressStyleEditor/index.vue | 101 ++++++ .../deskModule/SystemMonitor/Edit/index.vue | 148 +++++++++ .../deskModule/SystemMonitor/common.ts | 88 +++++ .../components/GenericMonitorCard/index.vue | 61 ++++ .../components/GenericProgress/index.vue | 57 ++++ .../deskModule/SystemMonitor/index.vue | 308 ++++++++++++++++++ .../deskModule/SystemMonitor/typings.ts | 33 ++ src/components/deskModule/index.ts | 3 +- src/enums/panel/index.ts | 1 + src/locales/en-US.json | 246 ++++++++++++++ src/locales/en-US.ts | 94 ------ src/locales/index.ts | 17 +- src/locales/zh-CN.json | 245 ++++++++++++++ src/locales/zh-CN.ts | 121 ------- src/store/modules/app/helper.ts | 11 +- src/store/modules/app/index.ts | 7 +- src/store/modules/moduleConfig/index.ts | 4 +- src/store/modules/panel/helper.ts | 7 + src/typings/panel.d.ts | 4 + src/typings/systemMonitor.d.ts | 43 +++ src/utils/cmn/index.ts | 8 + src/utils/defaultData/index.ts | 17 + src/utils/request/index.ts | 15 +- .../index.vue => exception/test/zujian.vue} | 0 .../home/components/AppStarter/index.vue | 18 +- .../home/components/EditItem/IconEditor.vue | 23 +- src/views/home/components/EditItem/index.vue | 52 +-- src/views/home/index.vue | 67 ++-- src/views/login/index.vue | 23 +- sun-panel.code-workspace | 84 +++++ 62 files changed, 2696 insertions(+), 397 deletions(-) create mode 100644 service/api/api_v1/common/apiData/systemApiStructs/monitor.go create mode 100644 service/api/api_v1/system/monitor.go create mode 100644 service/global/monitor.go create mode 100644 service/initialize/systemMonitor/systemMonitor.go create mode 100644 service/lib/monitor/monitor.go create mode 100644 service/router/system/monitor.go create mode 100644 src/api/system/systemMonitor.ts create mode 100644 src/assets/svg-icons/clarity-hard-disk-solid.svg create mode 100644 src/assets/svg-icons/ion-language.svg create mode 100644 src/assets/svg-icons/material-symbols-memory-alt-rounded.svg create mode 100644 src/assets/svg-icons/solar-cpu-bold.svg create mode 100644 src/components/common/ItemCard/index.vue create mode 100644 src/components/deskModule/SystemMonitor/AppIconSystemMonitor/CPU.vue create mode 100644 src/components/deskModule/SystemMonitor/AppIconSystemMonitor/Disk.vue create mode 100644 src/components/deskModule/SystemMonitor/AppIconSystemMonitor/Memory.vue create mode 100644 src/components/deskModule/SystemMonitor/AppIconSystemMonitor/index.vue create mode 100644 src/components/deskModule/SystemMonitor/Edit/DiskEditor/index.vue create mode 100644 src/components/deskModule/SystemMonitor/Edit/GenericProgressStyleEditor/index.vue create mode 100644 src/components/deskModule/SystemMonitor/Edit/index.vue create mode 100644 src/components/deskModule/SystemMonitor/common.ts create mode 100644 src/components/deskModule/SystemMonitor/components/GenericMonitorCard/index.vue create mode 100644 src/components/deskModule/SystemMonitor/components/GenericProgress/index.vue create mode 100644 src/components/deskModule/SystemMonitor/index.vue create mode 100644 src/components/deskModule/SystemMonitor/typings.ts create mode 100644 src/locales/en-US.json delete mode 100644 src/locales/en-US.ts create mode 100644 src/locales/zh-CN.json delete mode 100644 src/locales/zh-CN.ts create mode 100644 src/typings/systemMonitor.d.ts create mode 100644 src/utils/defaultData/index.ts rename src/views/{home/applist/index.vue => exception/test/zujian.vue} (100%) create mode 100644 sun-panel.code-workspace diff --git a/.vscode/settings.json b/.vscode/settings.json index a161def..4da5073 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,16 +19,12 @@ "markdown" ], "cSpell.words": [ - "antfu", "axios", "bumpp", - "chatgpt", - "chenzhaoyu", "commitlint", "davinci", "dockerhub", "esno", - "GPTAPI", "highlightjs", "hljs", "iconify", @@ -39,7 +35,6 @@ "mdhljs", "mila", "nodata", - "OPENAI", "pinia", "Popconfirm", "rushstack", @@ -50,8 +45,7 @@ "Typecheck", "unplugin", "VITE", - "vueuse", - "Zhao" + "vueuse" ], "i18n-ally.enabledParsers": [ "ts" diff --git a/index.html b/index.html index b8f7493..24e2ca0 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ <link rel="apple-touch-icon" href="/favicon.ico"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" /> - <title>Sun Panel</title> + <title>Sun-Panel</title> </head> <body class="dark:bg-black"> diff --git a/service/api/api_v1/common/apiData/systemApiStructs/monitor.go b/service/api/api_v1/common/apiData/systemApiStructs/monitor.go new file mode 100644 index 0000000..f39f325 --- /dev/null +++ b/service/api/api_v1/common/apiData/systemApiStructs/monitor.go @@ -0,0 +1,5 @@ +package systemApiStructs + +type MonitorGetDiskStateByPathReq struct { + Path string `json:"path"` +} diff --git a/service/api/api_v1/system/A_ENTER.go b/service/api/api_v1/system/A_ENTER.go index f1b8636..490e69e 100644 --- a/service/api/api_v1/system/A_ENTER.go +++ b/service/api/api_v1/system/A_ENTER.go @@ -9,4 +9,5 @@ type ApiSystem struct { RegisterApi RegisterApi NoticeApi NoticeApi ModuleConfigApi ModuleConfigApi + MonitorApi MonitorApi } diff --git a/service/api/api_v1/system/monitor.go b/service/api/api_v1/system/monitor.go new file mode 100644 index 0000000..7a14d2c --- /dev/null +++ b/service/api/api_v1/system/monitor.go @@ -0,0 +1,96 @@ +package system + +import ( + "sun-panel/api/api_v1/common/apiData/systemApiStructs" + "sun-panel/api/api_v1/common/apiReturn" + "sun-panel/global" + "sun-panel/lib/monitor" + "time" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" +) + +type MonitorApi struct{} + +const cacheSecond = 3 + +// 弃用 +func (a *MonitorApi) GetAll(c *gin.Context) { + if value, ok := global.SystemMonitor.Get("value"); ok { + apiReturn.SuccessData(c, value) + return + } + apiReturn.Error(c, "failed") +} + +func (a *MonitorApi) GetCpuState(c *gin.Context) { + if v, ok := global.SystemMonitor.Get(global.SystemMonitor_CPU_INFO); ok { + global.Logger.Debugln("读取缓存的的CPU信息") + apiReturn.SuccessData(c, v) + return + } + cpuInfo, err := monitor.GetCPUInfo() + + if err != nil { + apiReturn.Error(c, "failed") + return + } + // 缓存 + global.SystemMonitor.Set(global.SystemMonitor_CPU_INFO, cpuInfo, cacheSecond*time.Second) + apiReturn.SuccessData(c, cpuInfo) +} + +func (a *MonitorApi) GetMemonyState(c *gin.Context) { + if v, ok := global.SystemMonitor.Get(global.SystemMonitor_MEMORY_INFO); ok { + global.Logger.Debugln("读取缓存的的RAM信息") + apiReturn.SuccessData(c, v) + return + } + memoryInfo, err := monitor.GetMemoryInfo() + + if err != nil { + apiReturn.Error(c, "failed") + return + } + + // 缓存 + global.SystemMonitor.Set(global.SystemMonitor_MEMORY_INFO, memoryInfo, cacheSecond*time.Second) + apiReturn.SuccessData(c, memoryInfo) +} + +func (a *MonitorApi) GetDiskStateByPath(c *gin.Context) { + + req := systemApiStructs.MonitorGetDiskStateByPathReq{} + if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil { + apiReturn.ErrorParamFomat(c, err.Error()) + return + } + + cacheDiskName := global.SystemMonitor_DISK_INFO + req.Path + + if v, ok := global.SystemMonitor.Get(cacheDiskName); ok { + global.Logger.Debugln("读取缓存的的DISK信息") + apiReturn.SuccessData(c, v) + return + } + + diskState, err := monitor.GetDiskInfoByPath(req.Path) + if err != nil { + apiReturn.Error(c, "failed") + return + } + + // 缓存 + global.SystemMonitor.Set(cacheDiskName, diskState, cacheSecond*time.Second) + apiReturn.SuccessData(c, diskState) +} + +func (a *MonitorApi) GetDiskMountpoints(c *gin.Context) { + if list, err := monitor.GetDiskMountpoints(); err != nil { + apiReturn.Error(c, err.Error()) + return + } else { + apiReturn.SuccessData(c, list) + } +} diff --git a/service/assets/version b/service/assets/version index 58ba41a..0a8912e 100644 --- a/service/assets/version +++ b/service/assets/version @@ -1 +1 @@ -8|1.2.1 \ No newline at end of file +9|1.3.0-beta24-01-09 \ No newline at end of file diff --git a/service/global/global.go b/service/global/global.go index 0e133c6..b24a5c3 100644 --- a/service/global/global.go +++ b/service/global/global.go @@ -35,5 +35,6 @@ var ( Db *gorm.DB RedisDb *redis.Client SystemSetting *systemSetting.SystemSettingCache + SystemMonitor cache.Cacher[interface{}] RateLimit *RateLimiter ) diff --git a/service/global/monitor.go b/service/global/monitor.go new file mode 100644 index 0000000..e3e3db7 --- /dev/null +++ b/service/global/monitor.go @@ -0,0 +1,18 @@ +package global + +import ( + "sun-panel/lib/monitor" +) + +const ( + SystemMonitor_CPU_INFO = "CPU_INFO" + SystemMonitor_MEMORY_INFO = "MEMORY_INFO" + SystemMonitor_DISK_INFO = "DISK_INFO" +) + +type ModelSystemMonitor struct { + CPUInfo monitor.CPUInfo `json:"cpuInfo"` + DiskInfo []monitor.DiskInfo `json:"diskInfo"` + NetIOCountersInfo []monitor.NetIOCountersInfo `json:"netIOCountersInfo"` + MemoryInfo monitor.MemoryInfo `json:"memoryInfo"` +} diff --git a/service/initialize/A_ENTER.go b/service/initialize/A_ENTER.go index 449561f..78abfd7 100644 --- a/service/initialize/A_ENTER.go +++ b/service/initialize/A_ENTER.go @@ -17,6 +17,7 @@ import ( "sun-panel/lib/cmn" "sun-panel/models" "sun-panel/structs" + "time" "log" @@ -88,6 +89,7 @@ func InitApp() error { // 其他的初始化 global.VerifyCodeCachePool = other.InitVerifyCodeCachePool() global.SystemSetting = systemSettingCache.InItSystemSettingCache() + global.SystemMonitor = global.NewCache[interface{}](5*time.Hour, -1, "systemMonitorCache") return nil } diff --git a/service/initialize/systemMonitor/systemMonitor.go b/service/initialize/systemMonitor/systemMonitor.go new file mode 100644 index 0000000..1b155f1 --- /dev/null +++ b/service/initialize/systemMonitor/systemMonitor.go @@ -0,0 +1,53 @@ +package systemMonitor + +import ( + "sun-panel/global" + "sun-panel/lib/cache" + "sun-panel/lib/monitor" + "time" +) + +func Start(cacher cache.Cacher[global.ModelSystemMonitor], interval time.Duration) { + go func() { + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + go func() { + monitorInfo := GetInfo() + // jsonByte, _ := json.Marshal(monitorInfo) + // fmt.Println("系统监控:", string(jsonByte)) + cacher.SetDefault("value", monitorInfo) + }() + } + } + + }() + +} + +func GetInfo() global.ModelSystemMonitor { + + var modelSystemMonitor global.ModelSystemMonitor + + if cpuInfo, err := monitor.GetCPUInfo(); err == nil { + modelSystemMonitor.CPUInfo = cpuInfo + } + + if v, err := monitor.GetDiskInfo(); err == nil { + modelSystemMonitor.DiskInfo = v + } + + if v, err := monitor.GetNetIOCountersInfo(); err == nil { + modelSystemMonitor.NetIOCountersInfo = v + } + + if v, err := monitor.GetMemoryInfo(); err == nil { + modelSystemMonitor.MemoryInfo = v + } + + return modelSystemMonitor +} diff --git a/service/lib/cache/base.go b/service/lib/cache/base.go index b522d70..99dbf1e 100644 --- a/service/lib/cache/base.go +++ b/service/lib/cache/base.go @@ -4,6 +4,11 @@ import ( "time" ) +const ( + CACHE_DRIVE_REDIS = "redis" + CACHE_DRIVE_MEMORY = "memory" +) + // 缓存接口-支持Redis和内存使用 type Cacher[T any] interface { // 设置 diff --git a/service/lib/monitor/monitor.go b/service/lib/monitor/monitor.go new file mode 100644 index 0000000..72c2904 --- /dev/null +++ b/service/lib/monitor/monitor.go @@ -0,0 +1,149 @@ +package monitor + +import ( + "time" + + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/mem" + "github.com/shirou/gopsutil/v3/net" +) + +type CPUInfo struct { + CoreCount int32 `json:"coreCount"` + CPUNum int `json:"cpuNum"` + Model string `json:"model"` + Usages []float64 `json:"usages"` +} + +type DiskInfo struct { + Mountpoint string `json:"mountpoint"` + Total uint64 `json:"total"` + Used uint64 `json:"used"` + Free uint64 `json:"free"` + UsedPercent float64 `json:"usedPercent"` +} + +type NetIOCountersInfo struct { + BytesSent uint64 `json:"bytesSent"` + BytesRecv uint64 `json:"bytesRecv"` + Name string `json:"name"` +} + +type MemoryInfo struct { + Total uint64 `json:"total"` + Free uint64 `json:"free"` + Used uint64 `json:"used"` + UsedPercent float64 `json:"usedPercent"` +} + +// 获取CPU信息 +func GetCPUInfo() (CPUInfo, error) { + cpuInfoRes := CPUInfo{} + cpuInfo, err := cpu.Info() + + if err == nil && len(cpuInfo) > 0 { + cpuInfoRes.CoreCount = cpuInfo[0].Cores + cpuInfoRes.Model = cpuInfo[0].ModelName + } + numCPU, _ := cpu.Counts(true) + cpuInfoRes.CPUNum = numCPU + cpuPercentages, err := cpu.Percent(time.Second, true) + cpuInfoRes.Usages = cpuPercentages + + return cpuInfoRes, err +} + +// 获取内存信息 单位:MB +func GetMemoryInfo() (MemoryInfo, error) { + memoryInfo := MemoryInfo{} + // 获取内存信息 + memInfo, err := mem.VirtualMemory() + if err == nil { + memoryInfo.Free = memInfo.Free + memoryInfo.Total = memInfo.Total + memoryInfo.Used = memInfo.Used + memoryInfo.UsedPercent = memInfo.UsedPercent + } + + return memoryInfo, err +} + +// 获取每个磁盘分区使用情况 +func GetDiskInfo() ([]DiskInfo, error) { + disks := []DiskInfo{} + // 获取所有磁盘分区的信息 + partitions, err := disk.Partitions(true) + if err != nil { + return disks, err + } + + for _, partition := range partitions { + usage, err := disk.Usage(partition.Mountpoint) + if err != nil { + // fmt.Printf("Error getting disk usage for %s: %v\n", partition.Mountpoint, err) + continue + } + + disks = append(disks, DiskInfo{ + Mountpoint: partition.Mountpoint, + Total: usage.Total / 1024 / 1024, + Used: usage.Used / 1024 / 1024, + Free: usage.Free / 1024 / 1024, + UsedPercent: usage.UsedPercent, + }) + } + + return disks, nil +} + +func GetDiskMountpoints() ([]disk.PartitionStat, error) { + return disk.Partitions(true) +} + +func GetDiskInfoByPath(path string) (*DiskInfo, error) { + diskInfo := DiskInfo{} + usage, err := disk.Usage(path) + if err != nil { + return nil, err + } + diskInfo.Free = usage.Free + diskInfo.Mountpoint = usage.Path + diskInfo.Total = usage.Total + diskInfo.Used = usage.Used + diskInfo.UsedPercent = usage.UsedPercent + return &diskInfo, nil +} + +// 获取网络统计信息 +func GetNetIOCountersInfo() ([]NetIOCountersInfo, error) { + netInfo := []NetIOCountersInfo{} + netStats, err := net.IOCounters(true) + if err == nil { + for _, netStat := range netStats { + netInfo = append(netInfo, NetIOCountersInfo{ + BytesRecv: netStat.BytesRecv, + BytesSent: netStat.BytesSent, + Name: netStat.Name, + }) + + } + } + return netInfo, err +} + +// func GetCountDiskInfo() { +// // 获取所有磁盘的总使用情况 +// allUsage, err := disk.Usage("/") +// if err != nil { +// fmt.Printf("Error getting total disk usage: %v\n", err) +// return +// } + +// // 打印所有磁盘的总使用情况 +// fmt.Println("Total Disk Usage:") +// fmt.Printf("Total: %d MB\n", allUsage.Total/1024/1024) +// fmt.Printf("Used: %d MB\n", allUsage.Used/1024/1024) +// fmt.Printf("Free: %d MB\n", allUsage.Free/1024/1024) +// fmt.Printf("Usage: %.2f%%\n", allUsage.UsedPercent) +// } diff --git a/service/router/system/A_ENTER.go b/service/router/system/A_ENTER.go index 62e8b7c..c65dedf 100644 --- a/service/router/system/A_ENTER.go +++ b/service/router/system/A_ENTER.go @@ -11,4 +11,5 @@ func Init(routerGroup *gin.RouterGroup) { InitRegister(routerGroup) InitNoticeRouter(routerGroup) InitModuleConfigRouter(routerGroup) + InitMonitorRouter(routerGroup) } diff --git a/service/router/system/monitor.go b/service/router/system/monitor.go new file mode 100644 index 0000000..d4f3ff0 --- /dev/null +++ b/service/router/system/monitor.go @@ -0,0 +1,23 @@ +package system + +import ( + "sun-panel/api/api_v1" + "sun-panel/api/api_v1/middleware" + + "github.com/gin-gonic/gin" +) + +func InitMonitorRouter(router *gin.RouterGroup) { + api := api_v1.ApiGroupApp.ApiSystem.MonitorApi + r := router.Group("", middleware.LoginInterceptor) + r.POST("/system/monitor/getDiskMountpoints", api.GetDiskMountpoints) + + // 公开模式 + rPublic := router.Group("", middleware.PublicModeInterceptor) + { + rPublic.POST("/system/monitor/getAll", api.GetAll) + rPublic.POST("/system/monitor/getCpuState", api.GetCpuState) + rPublic.POST("/system/monitor/getDiskStateByPath", api.GetDiskStateByPath) + rPublic.POST("/system/monitor/getMemonyState", api.GetMemonyState) + } +} diff --git a/src/api/system/systemMonitor.ts b/src/api/system/systemMonitor.ts new file mode 100644 index 0000000..0e17ff2 --- /dev/null +++ b/src/api/system/systemMonitor.ts @@ -0,0 +1,32 @@ +import { post } from '@/utils/request' + +export function getAll<T>() { + return post<T>({ + url: '/system/monitor/getAll', + }) +} + +export function getCpuState<T>() { + return post<T>({ + url: '/system/monitor/getCpuState', + }) +} + +export function getDiskStateByPath<T>(path: string) { + return post<T>({ + url: '/system/monitor/getDiskStateByPath', + data: { path }, + }) +} + +export function getMemonyState<T>() { + return post<T>({ + url: '/system/monitor/getMemonyState', + }) +} + +export function getDiskMountpoints<T>() { + return post<T>({ + url: '/system/monitor/getDiskMountpoints', + }) +} diff --git a/src/assets/svg-icons/clarity-hard-disk-solid.svg b/src/assets/svg-icons/clarity-hard-disk-solid.svg new file mode 100644 index 0000000..f97333e --- /dev/null +++ b/src/assets/svg-icons/clarity-hard-disk-solid.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 36 36"><path fill="currentColor" d="M30.86 8.43A2 2 0 0 0 28.94 7H7.06a2 2 0 0 0-1.93 1.47L2.29 20h31.42Z" class="clr-i-solid clr-i-solid-path-1"/><path fill="currentColor" d="M2 22v7a2 2 0 0 0 2 2h28a2 2 0 0 0 2-2v-7Zm28 5h-4v-2h4Z" class="clr-i-solid clr-i-solid-path-2"/><path fill="none" d="M0 0h36v36H0z"/></svg> \ No newline at end of file diff --git a/src/assets/svg-icons/ion-language.svg b/src/assets/svg-icons/ion-language.svg new file mode 100644 index 0000000..0a25377 --- /dev/null +++ b/src/assets/svg-icons/ion-language.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><path fill="#888888" d="m478.33 433.6l-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4ZM334.83 362L368 281.65L401.17 362Zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73c39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36c-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93c.92 1.19 1.83 2.35 2.74 3.51c-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59c22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9Z"/></svg> \ No newline at end of file diff --git a/src/assets/svg-icons/material-symbols-memory-alt-rounded.svg b/src/assets/svg-icons/material-symbols-memory-alt-rounded.svg new file mode 100644 index 0000000..0f5dcfa --- /dev/null +++ b/src/assets/svg-icons/material-symbols-memory-alt-rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M7 15q.425 0 .713-.288T8 14v-4q0-.425-.288-.712T7 9q-.425 0-.712.288T6 10v4q0 .425.288.713T7 15m5 0q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9q-.425 0-.712.288T11 10v4q0 .425.288.713T12 15m5 0q.425 0 .713-.288T18 14v-4q0-.425-.288-.712T17 9q-.425 0-.712.288T16 10v4q0 .425.288.713T17 15M4 19q-.825 0-1.412-.587T2 17V7q0-.825.588-1.412T4 5h1V4q0-.425.288-.712T6 3q.425 0 .713.288T7 4v1h4V4q0-.425.288-.712T12 3q.425 0 .713.288T13 4v1h4V4q0-.425.288-.712T18 3q.425 0 .713.288T19 4v1h1q.825 0 1.413.588T22 7v10q0 .825-.587 1.413T20 19h-1v1q0 .425-.288.713T18 21q-.425 0-.712-.288T17 20v-1h-4v1q0 .425-.288.713T12 21q-.425 0-.712-.288T11 20v-1H7v1q0 .425-.288.713T6 21q-.425 0-.712-.288T5 20v-1z"/></svg> \ No newline at end of file diff --git a/src/assets/svg-icons/solar-cpu-bold.svg b/src/assets/svg-icons/solar-cpu-bold.svg new file mode 100644 index 0000000..590f39d --- /dev/null +++ b/src/assets/svg-icons/solar-cpu-bold.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M9.181 10.181c.053-.053.148-.119.45-.16c.323-.043.761-.044 1.439-.044h1.86c.678 0 1.116.001 1.438.045c.303.04.398.106.45.16c.054.052.12.147.16.45c.044.322.045.76.045 1.438v1.86c0 .678-.001 1.116-.045 1.438c-.04.303-.106.398-.16.45c-.052.054-.147.12-.45.16c-.322.044-.76.045-1.438.045h-1.86c-.678 0-1.116-.001-1.438-.045c-.303-.04-.398-.106-.45-.16c-.054-.052-.12-.147-.16-.45c-.044-.322-.045-.76-.045-1.438v-1.86c0-.678.001-1.116.045-1.438c.04-.303.106-.398.16-.45"/><path fill="currentColor" fill-rule="evenodd" d="M12 3c.385 0 .698.312.698.698v2.79c.51.002.974.005 1.395.017V3.698a.698.698 0 0 1 1.395 0v2.79a.703.703 0 0 1-.008.108c.936.115 1.585.353 2.078.846c.493.493.731 1.142.846 2.078a.702.702 0 0 1 .108-.008h2.79a.698.698 0 0 1 0 1.395h-2.807c.012.421.016.885.017 1.395h2.79a.698.698 0 0 1 0 1.396h-2.79c-.002.51-.005.974-.017 1.395h2.807a.698.698 0 0 1 0 1.395h-2.79a.703.703 0 0 1-.108-.008c-.115.936-.353 1.585-.846 2.078c-.493.493-1.142.731-2.078.846a.639.639 0 0 1 .008.108v2.79a.698.698 0 0 1-1.395 0v-2.807a53.57 53.57 0 0 1-1.395.017v2.79a.698.698 0 0 1-1.396 0v-2.79a56.16 56.16 0 0 1-1.395-.017v2.807a.698.698 0 0 1-1.395 0v-2.79c0-.037.002-.073.008-.108c-.936-.115-1.585-.353-2.078-.846c-.493-.493-.731-1.142-.846-2.078a.703.703 0 0 1-.108.008h-2.79a.698.698 0 0 1 0-1.395h2.807a56.235 56.235 0 0 1-.017-1.395h-2.79a.698.698 0 0 1 0-1.396h2.79c.002-.51.005-.974.017-1.395H2.698a.698.698 0 0 1 0-1.395h2.79c.037 0 .073.002.108.008c.115-.936.353-1.585.846-2.078c.493-.493 1.142-.731 2.078-.846a.702.702 0 0 1-.008-.108v-2.79a.698.698 0 0 1 1.395 0v2.807a53.57 53.57 0 0 1 1.395-.017v-2.79c0-.386.313-.698.698-.698m-.976 5.581c-.619 0-1.152 0-1.578.058c-.458.061-.896.2-1.252.555c-.355.356-.494.794-.555 1.252c-.058.427-.058.96-.058 1.578v1.952c0 .619 0 1.151.058 1.578c.061.458.2.896.555 1.252c.356.355.794.494 1.252.555c.426.058.96.058 1.578.058h1.952c.619 0 1.151 0 1.578-.058c.458-.061.896-.2 1.252-.555c.355-.356.494-.794.555-1.252c.058-.427.058-.96.058-1.578v-1.952c0-.619 0-1.151-.058-1.578c-.061-.458-.2-.896-.555-1.252c-.356-.355-.794-.494-1.252-.555c-.427-.058-.96-.058-1.578-.058z" clip-rule="evenodd"/></svg> \ No newline at end of file diff --git a/src/components/apps/About/index.vue b/src/components/apps/About/index.vue index 4de4cab..381dce9 100644 --- a/src/components/apps/About/index.vue +++ b/src/components/apps/About/index.vue @@ -35,7 +35,7 @@ onMounted(() => { </div> <div class="text-xl"> <NGradientText type="info"> - <a href="https://github.com/hslr-s/sun-panel/releases" class="font-semibold" title="点此查看更新说明" target="_blank">v{{ versionName }}</a> + <a href="https://github.com/hslr-s/sun-panel/releases" class="font-semibold" :title="$t('apps.about.viewUpdateLog')" target="_blank">v{{ versionName }}</a> </NGradientText> </div> </div> @@ -43,19 +43,23 @@ onMounted(() => { <NDivider> • </NDivider> <div class="flex flex-col items-center justify-center text-base"> <div> - 建议反馈:<a href="https://github.com/hslr-s/sun-panel/issues" target="_blank" class="link">Github Issues</a> + <a href="https://github.com/hslr-s/sun-panel/releases" target="_blank" class="link">{{ $t('apps.about.checkUpdate') }}</a> </div> <div> - QQ交流群:<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=_I9WIoJn1roIdoaAqelSj9qClLKlXIa1&authKey=GfsQP2GagHnus0jMc7U8Sm6VhWjtsipXUzCHbFwQsGyHMgmYWx6ZbAP%2Bhut%2B4D6N&noverify=0&group_code=276594668" target="_blank" class="link">276594668</a> + {{ $t('apps.about.issue') }}<a href="https://github.com/hslr-s/sun-panel/issues" target="_blank" class="link">Github Issues</a> + </div> + + <div> + {{ $t('apps.about.QQGroup') }}<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=_I9WIoJn1roIdoaAqelSj9qClLKlXIa1&authKey=GfsQP2GagHnus0jMc7U8Sm6VhWjtsipXUzCHbFwQsGyHMgmYWx6ZbAP%2Bhut%2B4D6N&noverify=0&group_code=276594668" target="_blank" class="link">{{ $t("apps.about.addQQGroupUrl") }}</a> | <span class="link cursor-pointer" @click="qqGroupQRShow = !qqGroupQRShow"> - 二维码(推荐) + {{ $t('apps.about.QR') }} </span> </div> <div> - 开发者:<a href="https://blog.enianteam.com/u/sun/content/11" target="_blank" class="link">红烧猎人</a> | <a href="https://github.com/hslr-s/sun-panel/blob/master/doc/donate.md" target="_blank" class="text-red-600 hover:text-red-900">🧧打赏</a> + {{ $t('apps.about.author') }}<a href="https://github.com/hslr-s" target="_blank" class="link">红烧猎人</a> | <a href="https://github.com/hslr-s/sun-panel/blob/master/doc/donate.md" target="_blank" class="text-red-600 hover:text-red-900">{{ $t('apps.about.donate') }}</a> </div> <div class="flex mt-[10px]"> diff --git a/src/components/apps/ImportExport/index.vue b/src/components/apps/ImportExport/index.vue index 47a17d5..59d9caf 100644 --- a/src/components/apps/ImportExport/index.vue +++ b/src/components/apps/ImportExport/index.vue @@ -91,9 +91,9 @@ async function importIcons(): Promise<string | null> { } catch (error) { if (error instanceof Error) - return `发生错误: ${error.message}` + return `${t('common.failed')}: ${error.message}` else - return '发生未知错误' + return t('common.unknownError') } } @@ -162,7 +162,7 @@ function handleFileChange(options: { file: UploadFileInfo; fileList: Array<Uploa importCheck() } else { - ms.error('异常请重新上传') + ms.error(`${t('common.failed')}: ${t('common.repeatLater')}`) } uploadLoading.value = false } @@ -178,13 +178,13 @@ function importCheck() { importObj.value = importJsonString(jsonData.value) if (importObj.value) { if (!importObj.value.isPassCheckMd5()) - importWarning.value.push('文件被修改过,谨慎导入') + importWarning.value.push(t('apps.exportImport.fileModified')) if (!importObj.value.isPassCheckConfigVersionOld()) - importWarning.value.push('配置文件版本过低,但是兼容') + importWarning.value.push(t('apps.exportImport.warnConfigFileLow')) if (!importObj.value.isPassCheckConfigVersionNew()) - importWarning.value.push('当前软件版本可能过旧,很有可能无法兼容该配置文件,请谨慎导入。推荐将软件更新到新版后再次导入') + importWarning.value.push(t('apps.exportImport.softwareVersionLow')) // (暂时不做)此处可以判断,当前的配置文件是否存在的导入项目(不存在隐藏importItems里面的值)操作变量:importItems @@ -196,24 +196,24 @@ function importCheck() { } catch (error) { if (error instanceof ConfigVersionLowError) { - ms.error('配置文件版本过低,无法兼容') - console.log('配置文件版本过低') + ms.error(t('apps.exportImport.errorConfigFileLow')) + console.error('The configuration file version is too low to be compatible') } else if (error instanceof FormatError) { - ms.error('格式不正确,无法导入') - console.log('格式不正确') + ms.error(t('apps.exportImport.errorConfigFileFormat')) + console.error('The format is incorrect and cannot be imported') } } } else { - ms.error('数据不正确') + ms.error(t('apps.exportImport.errorConfigFileFormat')) } } // 开始导出 async function handleStartExport() { loading.value = true - console.log('要导出的项目', checkedItems.value) + // console.log('要导出的项目', checkedItems.value) // 获取软件版本号 const exportResult = exportJson(version.value) if (checkedItems.value.includes('icons')) { @@ -223,7 +223,7 @@ async function handleStartExport() { console.log('export icons finish', iconGroups) } - console.log('导出结果') + // console.log('导出结果') jsonData.value = exportResult.string() exportResult.exportFile() @@ -244,14 +244,14 @@ async function handleStartImport() { loading.value = false importRoundModalShow.value = false - ms.success(`${t('common.success')},请手动刷新页面`) + ms.success(`${t('common.success')}, ${t('common.refreshPage')}`) } </script> <template> <div class="pt-2"> <NAlert type="info" :bordered="false"> - <p>导入图标配置数据不会清空现有图标数据</p> + <p>{{ $t('apps.exportImport.tip') }}</p> </NAlert> <div class="flex justify-center m-[50px]"> <div class="m-[10px]"> @@ -266,7 +266,7 @@ async function handleStartImport() { <template #icon> <SvgIcon icon="fa6:solid-file-import" /> </template> - 导入配置 + {{ $t('apps.exportImport.import') }} </NButton> </NUpload> </div> @@ -275,13 +275,13 @@ async function handleStartImport() { <template #icon> <SvgIcon icon="fa6:solid-file-export" /> </template> - 导出配置 + {{ $t('apps.exportImport.export') }} </NButton> </div> </div> <div class="flex justify-center"> - <a href="https://hslr-s.github.io/sun-panel-tool-page/#/" target="_blank">浏览器书签转换工具</a> + <a href="https://hslr-s.github.io/sun-panel-tool-page/#/" target="_blank">{{ $t('apps.exportImport.transmuteStandard') }}</a> </div> <!-- 调试模式 --> @@ -310,7 +310,7 @@ async function handleStartImport() { </div> </div> - <RoundCardModal v-model:show="importRoundModalShow" style="max-width: 400px;" title="导入"> + <RoundCardModal v-model:show="importRoundModalShow" style="max-width: 400px;" :title=" $t('apps.exportImport.import')"> <div v-if="importWarning.length > 0"> <NAlert :title="$t('common.warning')" type="warning"> <div v-for="(text, index) in importWarning " :key="index"> @@ -319,39 +319,39 @@ async function handleStartImport() { </NAlert> </div> <NDivider title-placement="left"> - 请选择要导入的配置数据 + {{ $t('apps.exportImport.selectImportData') }} </NDivider> <NSpace justify="center" style="margin-top: 20px;"> <NCheckboxGroup v-model:value="checkedItems"> - <NCheckbox v-if="importItems.includes('icons')" value="icons" label="图标" /> - <NCheckbox v-if="importItems.includes('style')" value="style" label="样式配置" /> + <NCheckbox v-if="importItems.includes('icons')" value="icons" :label="$t('apps.exportImport.moduleIcon')" /> + <NCheckbox v-if="importItems.includes('style')" value="style" :label="$t('apps.exportImport.moduleStyle')" /> </NCheckboxGroup> </NSpace> <NSpace justify="center"> <div class="mt-[50px]"> <NButton type="success" :disabled="checkedItems.length === 0" :loading="loading" @click="handleStartImport"> - 继续导入 + {{ $t('common.continue') }} </NButton> </div> </NSpace> </RoundCardModal> - <RoundCardModal v-model:show="exportRoundModalShow" style="max-width: 400px;" title="导出"> + <RoundCardModal v-model:show="exportRoundModalShow" style="max-width: 400px;" :title=" $t('apps.exportImport.export')"> <NDivider title-placement="left"> - 请选择要导出的配置数据 + {{ $t('apps.exportImport.selectExportData') }} </NDivider> <NSpace justify="center" style="margin-top: 20px;"> <NCheckboxGroup v-model:value="checkedItems"> - <NCheckbox v-if="importItems.includes('icons')" value="icons" label="图标" /> - <NCheckbox v-if="importItems.includes('style')" value="style" label="样式配置" /> + <NCheckbox v-if="importItems.includes('icons')" value="icons" :label="$t('apps.exportImport.moduleIcon')" /> + <NCheckbox v-if="importItems.includes('style')" value="style" :label="$t('apps.exportImport.moduleStyle')" /> </NCheckboxGroup> </NSpace> <NSpace justify="center"> <div class="mt-[50px]"> <NButton type="success" :disabled="checkedItems.length === 0" :loading="loading" @click="handleStartExport"> - 继续导出 + {{ $t('common.continue') }} </NButton> </div> </NSpace> diff --git a/src/components/apps/ItemGroupManage/index.vue b/src/components/apps/ItemGroupManage/index.vue index 092988a..f50d1f2 100644 --- a/src/components/apps/ItemGroupManage/index.vue +++ b/src/components/apps/ItemGroupManage/index.vue @@ -5,6 +5,7 @@ import { NButton, NCard, NForm, NFormItem, NInput, useDialog, useMessage } from import { VueDraggable } from 'vue-draggable-plus' import { deletes, edit, getList, saveSort } from '@/api/panel/itemIconGroup' import { RoundCardModal, SvgIcon } from '@/components/common' +import { t } from '@/locales' interface EditModalArg { show: boolean @@ -33,7 +34,7 @@ const editModalArg = ref<EditModalArg>({ { required: true, trigger: 'blur', - message: '必填项', + message: t('form.required'), }, ], }, @@ -66,26 +67,26 @@ function handleSaveSort() { } saveSort(saveItems).then(({ code, msg }) => { if (code === 0) { - ms.success('保存成功') + ms.success(t('common.saveSuccess')) sortStatus.value = false } else { - ms.error(`保存失败:${msg}`) + ms.error(`${t('common.saveFail')}:${msg}`) } }) } function handleDelete(groupInfo: Panel.ItemIconGroup) { dialog.warning({ - title: '警告', - content: `你确定删除此分组[ ${groupInfo.title} ],删除后此分组应用图标将丢失?`, - positiveText: '确定', - negativeText: '取消', + title: t('common.warning'), + content: t('apps.itemGroupManage.deleteWarnText', { name: groupInfo.title }), + positiveText: t('common.confirm'), + negativeText: t('common.cancel'), onPositiveClick: () => { if (groupInfo.id) { deletes([groupInfo.id]).then(({ code, msg }) => { if (code !== 0) - ms.error(`删除失败:${msg}`) + ms.error(t('common.deleteFail')) else refreshList() }) @@ -126,15 +127,15 @@ onMounted(() => { <div class="h-full"> <div class="p-2"> <NButton type="success" size="small" style="margin-right: 10px;" @click="handleAddGroup"> - 新增分组 + {{ $t('common.add') }} </NButton> <NButton v-if="!sortStatus" size="small" @click="handleDragSort"> - 排序 + {{ $t('common.sort') }} </NButton> <NButton v-else type="warning" size="small" @click="handleSaveSort"> - 保存排序 + {{ $t('common.saveSort') }} </NButton> </div> @@ -181,8 +182,8 @@ onMounted(() => { <RoundCardModal v-model:show="editModalArg.show" size="small" type="small" :title="editModalArg.editStatus === 1 ? '添加' : '编辑'" style="width: 400px;"> <NForm ref="formRef" :model="editModalArg.model" :rules="editModalArg.rules"> - <NFormItem path="title" label="分组名称"> - <NInput v-model:value="editModalArg.model.title" type="text" :maxlength="20" show-count placeholder="请输入" /> + <NFormItem path="title" :label="$t('apps.itemGroupManage.groupName')"> + <NInput v-model:value="editModalArg.model.title" type="text" :maxlength="20" show-count /> </NFormItem> <!-- <NFormItem path="name" label="昵称"> @@ -191,7 +192,7 @@ onMounted(() => { </NForm> <template #footer> <NButton type="success" size="small" class="float-right" @click="handleSaveGroup"> - 确定 + {{ $t('common.confirm') }} </NButton> </template> </RoundCardModal> diff --git a/src/components/apps/Style/index.vue b/src/components/apps/Style/index.vue index 42b77e2..bffbe06 100644 --- a/src/components/apps/Style/index.vue +++ b/src/components/apps/Style/index.vue @@ -5,20 +5,22 @@ import { NButton, NCard, NColorPicker, NGrid, NGridItem, NInput, NInputGroup, NP import { useAuthStore, usePanelState } from '@/store' import { set as setUserConfig } from '@/api/panel/userConfig' import { PanelPanelConfigStyleEnum } from '@/enums/panel' +import { t } from '@/locales' const authStore = useAuthStore() const panelState = usePanelState() const ms = useMessage() +const showWallpaperInput = ref(false) const isSaveing = ref(false) const iconTypeOptions = [ { - label: '详情图标', + label: t('apps.baseSettings.detailIcon'), value: PanelPanelConfigStyleEnum.info, }, { - label: '小图标', + label: t('apps.baseSettings.smallIcon'), value: PanelPanelConfigStyleEnum.icon, }, ] @@ -61,9 +63,9 @@ function handleUploadBackgroundFinish({ function uploadCloud() { setUserConfig({ panel: panelState.panelConfig }).then((res) => { if (res.code === 0) - ms.success('配置已保存') + ms.success(t('apps.baseSettings.configSaved')) else - ms.error(`配置已保存${res.msg}`) + ms.error(t('apps.baseSettings.configFailed', { message: res.msg })) }) } @@ -82,7 +84,7 @@ function resetPanelConfig() { <div> <div> - 文本内容 + {{ $t('apps.baseSettings.textContent') }} </div> <div class="flex items-center mt-[5px]"> <NInput v-model:value="panelState.panelConfig.logoText" type="text" show-count :maxlength="20" placeholder="请输入文字" /> @@ -92,35 +94,53 @@ function resetPanelConfig() { <NCard style="border-radius:10px" class="mt-[10px]" size="small"> <div class="text-slate-500 mb-[5px] font-bold"> - 时钟 + {{ $t('apps.baseSettings.clock') }} </div> <div class="flex items-center mt-[5px]"> - <span class="mr-[10px]">显示秒</span> + <span class="mr-[10px]">{{ $t('apps.baseSettings.clockSecondShow') }}</span> <NSwitch v-model:value="panelState.panelConfig.clockShowSecond" /> </div> </NCard> <NCard style="border-radius:10px" class="mt-[10px]" size="small"> <div class="text-slate-500 mb-[5px] font-bold"> - 搜索框 + {{ $t('apps.baseSettings.searchBar') }} </div> <div class="flex items-center mt-[5px]"> - <span class="mr-[10px]">显示</span> + <span class="mr-[10px]">{{ $t('common.show') }}</span> <NSwitch v-model:value="panelState.panelConfig.searchBoxShow" /> </div> <div v-if="panelState.panelConfig.searchBoxShow" class="flex items-center mt-[5px]"> - <span class="mr-[10px]">允许搜索快捷图标</span> + <span class="mr-[10px]">{{ $t('apps.baseSettings.searchBarSearchItem') }}</span> <NSwitch v-model:value="panelState.panelConfig.searchBoxSearchIcon" /> </div> </NCard> <NCard style="border-radius:10px" class="mt-[10px]" size="small"> <div class="text-slate-500 mb-[5px] font-bold"> - 图标 + {{ $t('apps.baseSettings.systemMonitorStatus') }} + </div> + <div class="flex items-center mt-[5px]"> + <span class="mr-[10px]">{{ $t('common.show') }}</span> + <NSwitch v-model:value="panelState.panelConfig.systemMonitorShow" /> + </div> + <div v-if="panelState.panelConfig.systemMonitorShow" class="flex items-center mt-[5px]"> + <span class="mr-[10px]">{{ $t('apps.baseSettings.showTitle') }}</span> + <NSwitch v-model:value="panelState.panelConfig.systemMonitorShowTitle" /> + </div> + <div v-if="panelState.panelConfig.systemMonitorShow" class="flex items-center mt-[5px]"> + <span class="mr-[10px]">{{ $t('apps.baseSettings.publicVisitModeShow') }}</span> + <NSwitch v-model:value="panelState.panelConfig.systemMonitorPublicVisitModeShow" /> + </div> + </NCard> + + <NCard style="border-radius:10px" class="mt-[10px]" size="small"> + <div class="text-slate-500 mb-[5px] font-bold"> + {{ $t('common.icon') }} </div> <div class="mt-[5px]"> <div> - 样式 + {{ $t('common.style') }} </div> <div class="flex items-center mt-[5px]"> <NSelect v-model:value="panelState.panelConfig.iconStyle" :options="iconTypeOptions" /> @@ -129,7 +149,7 @@ function resetPanelConfig() { <div v-if="panelState.panelConfig.iconStyle === PanelPanelConfigStyleEnum.info" class="mt-[5px]"> <div> - 隐藏描述信息 + {{ $t('apps.baseSettings.hideDescription') }} </div> <div class="flex items-center mt-[5px]"> <NSwitch v-model:value="panelState.panelConfig.iconTextInfoHideDescription" /> @@ -138,7 +158,7 @@ function resetPanelConfig() { <div v-if="panelState.panelConfig.iconStyle === PanelPanelConfigStyleEnum.icon" class="mt-[5px]"> <div> - 隐藏标题 + {{ $t('apps.baseSettings.hideTitle') }} </div> <div class="flex items-center mt-[5px]"> <NSwitch v-model:value="panelState.panelConfig.iconTextIconHideTitle" /> @@ -147,7 +167,7 @@ function resetPanelConfig() { <div class="mt-[5px]"> <div> - 文字颜色 + {{ $t('common.textColor') }} </div> <div class="flex items-center mt-[5px]"> <NColorPicker @@ -168,7 +188,7 @@ function resetPanelConfig() { </NCard> <NCard style="border-radius:10px" class="mt-[10px]" size="small"> <div class="text-slate-500 mb-[5px] font-bold"> - 壁纸 + {{ $t('apps.baseSettings.wallpaper') }} </div> <NUpload action="/api/file/uploadImg" @@ -186,32 +206,40 @@ function resetPanelConfig() { :style="{ background: `url(${panelState.panelConfig.backgroundImageSrc}) no-repeat`, backgroundSize: 'cover' }" > <div class="text-shadow text-white"> - 点击上传替换图片或拖拽到框内 + {{ $t('apps.baseSettings.uploadOrDragText') }} </div> </div> </NUploadDragger> </NUpload> + <div class="flex items-center mt-[5px]"> + <span class="mr-[10px]">{{ $t('apps.baseSettings.customImageAddress') }}</span> + <NSwitch v-model:value="showWallpaperInput" /> + </div> + <div v-if="showWallpaperInput" class="mt-1"> + <NInput v-model:value="panelState.panelConfig.backgroundImageSrc" type="text" size="small" clearable /> + </div> + <div class="flex items-center mt-[10px]"> - <span class="mr-[10px]">模糊</span> + <span class="mr-[10px]">{{ $t('apps.baseSettings.vague') }}</span> <NSlider v-model:value="panelState.panelConfig.backgroundBlur" class="max-w-[200px]" :step="2" :max="20" /> </div> <div class="flex items-center mt-[10px]"> - <span class="mr-[10px]">遮罩</span> + <span class="mr-[10px]">{{ $t('apps.baseSettings.mask') }}</span> <NSlider v-model:value="panelState.panelConfig.backgroundMaskNumber" class="max-w-[200px]" :step="0.1" :max="1" /> </div> </NCard> <NCard style="border-radius:10px" class="mt-[10px]" size="small"> <div class="text-slate-500 mb-[5px] font-bold"> - 内容区域 + {{ $t('apps.baseSettings.contentArea') }} </div> <NGrid cols="2"> <NGridItem span="12 400:12"> <div class="flex items-center mt-[10px]"> - <span class="mr-[10px]">最大宽度</span> + <span class="mr-[10px]">{{ $t('apps.baseSettings.maxWidth') }}</span> <div class="flex"> <NInputGroup> <NInput v-model:value="panelState.panelConfig.maxWidth" size="small" type="number" :maxlength="10" :style="{ width: '100px' }" placeholder="1200" /> @@ -222,39 +250,51 @@ function resetPanelConfig() { </NGridItem> <NGridItem span="12 400:12"> <div class="flex items-center mt-[10px]"> - <span class="mr-[10px]">左右边距</span> + <span class="mr-[10px]">{{ $t('apps.baseSettings.leftRightMargin') }}</span> <NSlider v-model:value="panelState.panelConfig.marginX" class="max-w-[200px]" :step="1" :max="100" /> </div> </NGridItem> <NGridItem span="12 400:12"> <div class="flex items-center mt-[10px]"> - <span class="mr-[10px]">上边距 (%)</span> + <span class="mr-[10px]">{{ $t('apps.baseSettings.topMargin') }} (%)</span> <NSlider v-model:value="panelState.panelConfig.marginTop" class="max-w-[200px]" :step="1" :max="50" /> </div> </NGridItem> <NGridItem span="12 400:6"> <div class="flex items-center mt-[10px]"> - <span class="mr-[10px]">下边距 (%)</span> + <span class="mr-[10px]">{{ $t('apps.baseSettings.bottomMargin') }} (%)</span> <NSlider v-model:value="panelState.panelConfig.marginBottom" class="max-w-[200px]" :step="1" :max="50" /> </div> </NGridItem> </NGrid> </NCard> + <NCard style="border-radius:10px" class="mt-[10px]" size="small"> + <div class="text-slate-500 mb-[5px] font-bold"> + {{ $t('apps.baseSettings.customFooter') }} + </div> + + <NInput + v-model:value="panelState.panelConfig.footerHtml" + type="textarea" + clearable + /> + </NCard> + <NCard style="border-radius:10px" class="mt-[10px]" size="small"> <NPopconfirm @positive-click="resetPanelConfig" > <template #trigger> <NButton size="small" quaternary type="error"> - 重置 + {{ $t('common.reset') }} </NButton> </template> - 确定要重置这些样式吗? + {{ $t('apps.baseSettings.resetWarnText') }} </NPopconfirm> <NButton size="small" quaternary type="success" class="ml-[10px]" @click="uploadCloud"> - 立即保存 + {{ $t('common.save') }} </NButton> </NCard> </div> diff --git a/src/components/apps/UserInfo/index.vue b/src/components/apps/UserInfo/index.vue index 62b7440..e20d78f 100644 --- a/src/components/apps/UserInfo/index.vue +++ b/src/components/apps/UserInfo/index.vue @@ -1,10 +1,11 @@ <script setup lang="ts"> import type { FormInst, FormRules } from 'naive-ui' -import { NButton, NCard, NDivider, NForm, NFormItem, NInput, useDialog, useMessage } from 'naive-ui' +import { NButton, NCard, NDivider, NForm, NFormItem, NInput, NSelect, useDialog, useMessage } from 'naive-ui' import { ref } from 'vue' -import { useAuthStore, usePanelState, useUserStore } from '@/store' +import { useAppStore, useAuthStore, usePanelState, useUserStore } from '@/store' +import { languageOptions } from '@/utils/defaultData' +import type { Language } from '@/store/modules/app/helper' import { logout } from '@/api' -import { router } from '@/router' import { RoundCardModal, SvgIcon } from '@/components/common/' import { updateInfo, updatePassword } from '@/api/system/user' import { updateLocalUserInfo } from '@/utils/cmn' @@ -12,14 +13,21 @@ import { t } from '@/locales' const userStore = useUserStore() const authStore = useAuthStore() +const appStore = useAppStore() const panelState = usePanelState() const ms = useMessage() const dialog = useDialog() +const languageValue = ref(appStore.language) +// const themeValue = ref(appStore.theme) const nickName = ref(authStore.userInfo?.name || '') const isEditNickNameStatus = ref(false) const formRef = ref<FormInst | null>(null) - +// const themeOptions: { label: string; key: string; value: Theme }[] = [ +// { label: t('apps.userInfo.themeStyle.dark'), key: 'dark', value: 'dark' }, +// { label: t('apps.userInfo.themeStyle.light'), key: 'light', value: 'light' }, +// { label: t('apps.userInfo.themeStyle.auto'), key: 'Auto', value: 'auto' }, +// ] const updatePasswordModalState = ref({ show: false, loading: false, @@ -59,8 +67,9 @@ async function logoutApi() { userStore.resetUserInfo() authStore.removeToken() panelState.removeState() + appStore.removeToken() ms.success(t('settingUserInfo.logoutSuccess')) - router.push({ path: '/login' }) + // router.push({ path: '/login' }) location.reload()// 强制刷新一下页面 } @@ -122,6 +131,18 @@ function handleLogout() { }, }) } + +function handleChangeLanuage(value: Language) { + languageValue.value = value + appStore.setLanguage(value) + location.reload() +} + +// function handleChangeTheme(value: Theme) { +// themeValue.value = value +// appStore.setTheme(value) +// // location.reload() +// } </script> <template> @@ -157,6 +178,24 @@ function handleLogout() { </div> </div> + <div class="mt-[10px]"> + <div class="text-slate-500 font-bold"> + {{ $t('common.language') }} + </div> + <div class="max-w-[200px]"> + <NSelect v-model:value="languageValue" :options="languageOptions" @update-value="handleChangeLanuage" /> + </div> + </div> + + <!-- <div class="mt-[10px]"> + <div class="text-slate-500 font-bold"> + {{ $t('apps.userInfo.theme') }} + </div> + <div class="max-w-[200px]"> + <NSelect v-model:value="themeValue" :options="themeOptions" @update-value="handleChangeTheme" /> + </div> + </div> --> + <NDivider style="margin: 10px 0;" dashed /> <div> <NButton size="small" text type="info" @click="updatePasswordModalState.show = !updatePasswordModalState.show"> diff --git a/src/components/common/ItemCard/index.vue b/src/components/common/ItemCard/index.vue new file mode 100644 index 0000000..7685a7f --- /dev/null +++ b/src/components/common/ItemCard/index.vue @@ -0,0 +1,53 @@ +<script setup lang="ts"> +import { ref } from 'vue' +import { PanelPanelConfigStyleEnum } from '@/enums' + +interface Prop { + cardTypeStyle: PanelPanelConfigStyleEnum + class?: string + backgroundColor?: string + iconTextIconHideTitle?: boolean // 隐藏小图标标题 + iconTextColor?: string // 小图标文字颜色 + iconText?: string // 小图标文字 +} + +const props = withDefaults(defineProps<Prop>(), {}) + +const defaultBackground = '#2a2a2a6b' +const propClass = ref(props.class) +</script> + +<template> + <div class="w-full"> + <!-- 详情图标 --> + <div + v-if="cardTypeStyle === PanelPanelConfigStyleEnum.info" + class="w-full rounded-2xl transition-all duration-200 flex" + :class="propClass" + :style="{ backgroundColor: backgroundColor ?? defaultBackground }" + > + <slot name="info" /> + </div> + + <!-- 极简图标(APP) --> + <div + v-if="cardTypeStyle === PanelPanelConfigStyleEnum.icon" + > + <div + class="overflow-hidden rounded-2xl sunpanel w-[70px] h-[70px] mx-auto transition-all duration-200" + :class="propClass" + :style="{ backgroundColor: backgroundColor ?? defaultBackground }" + > + <slot name="small" /> + </div> + + <div + v-if="!iconTextIconHideTitle" + class="text-center app-icon-text-shadow cursor-pointer mt-[2px]" + :style="{ color: iconTextColor }" + > + {{ iconText }} + </div> + </div> + </div> +</template> diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 990d2e8..72ff205 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -8,6 +8,7 @@ import RoundCardModal from './RoundCardModal/index.vue' import SvgIconOnline from './SvgIconOnline/index.vue' import JsonImportExport from './JsonImportExport/index.vue' import AppLoader from './AppLoader/index.vue' +import ItemCard from './ItemCard/index.vue' export { Verification, @@ -20,4 +21,5 @@ export { SvgIconOnline, JsonImportExport, AppLoader, + ItemCard, } diff --git a/src/components/deskModule/Clock/index.vue b/src/components/deskModule/Clock/index.vue index faace66..486cb91 100644 --- a/src/components/deskModule/Clock/index.vue +++ b/src/components/deskModule/Clock/index.vue @@ -1,5 +1,6 @@ <script setup lang="ts"> import { onBeforeUnmount, onMounted, ref } from 'vue' +import { t } from '@/locales' const props = defineProps<{ hideSecond?: boolean @@ -35,7 +36,15 @@ function updateCurrentDate() { const month = now.getMonth() + 1 // 月份从0开始,所以要加1 // const year = now.getFullYear() - const daysOfWeek = ['日', '一', '二', '三', '四', '五', '六'] + const daysOfWeek = [ + t('deskModule.clock.sun'), + t('deskModule.clock.mon'), + t('deskModule.clock.tue'), + t('deskModule.clock.wed'), + t('deskModule.clock.thu'), + t('deskModule.clock.fri'), + t('deskModule.clock.sat'), + ] // const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] currentDate.value.week = daysOfWeek[now.getDay()] currentDate.value.date = `${month}-${day}` @@ -63,11 +72,11 @@ onBeforeUnmount(() => { {{ currentDate.time }} </span> <div class="hidden sm:hidden md:block"> - <span> + <span class="mr-1"> {{ currentDate.date }} </span> <span> - 星期{{ currentDate.week }} + {{ currentDate.week }} </span> </div> </div> diff --git a/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/CPU.vue b/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/CPU.vue new file mode 100644 index 0000000..462fcb0 --- /dev/null +++ b/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/CPU.vue @@ -0,0 +1,59 @@ +<script setup lang="ts"> +import { onMounted, onUnmounted, ref } from 'vue' +import GenericProgress from '../components/GenericProgress/index.vue' +import { getCpuState } from '@/api/system/systemMonitor' +import type { PanelPanelConfigStyleEnum } from '@/enums' + +interface Prop { + cardTypeStyle: PanelPanelConfigStyleEnum + refreshInterval: number + textColor: string + progressColor: string + progressRailColor: string +} + +const props = defineProps<Prop>() +let timer: NodeJS.Timer +const cpuState = ref<SystemMonitor.CPUInfo | null>(null) + +function correctionNumber(v: number, keepNum = 2): number { + return v === 0 ? 0 : Number(v.toFixed(keepNum)) +} + +async function getData() { + try { + const { data, code } = await getCpuState<SystemMonitor.CPUInfo>() + if (code === 0) + cpuState.value = data + } + catch (error) { + + } +} + +onMounted(() => { + getData() + timer = setInterval(() => { + getData() + }, (!props.refreshInterval || props.refreshInterval <= 2000) ? 2000 : props.refreshInterval) +}) + +onUnmounted(() => { + clearInterval(timer) +}) +</script> + +<template> + <div class="w-full"> + <GenericProgress + :progress-color="progressColor" + :progress-rail-color="progressRailColor" + :progress-height="5" + :percentage="correctionNumber(cpuState?.usages[0] || 0)" + :card-type-style="cardTypeStyle" + :info-card-right-text="`${correctionNumber(cpuState?.usages[0] || 0)}%`" + info-card-left-text="CPU" + :text-color="textColor" + /> + </div> +</template> diff --git a/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/Disk.vue b/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/Disk.vue new file mode 100644 index 0000000..41898d2 --- /dev/null +++ b/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/Disk.vue @@ -0,0 +1,69 @@ +<script setup lang="ts"> +import { onMounted, onUnmounted, ref } from 'vue' +import GenericProgress from '../components/GenericProgress/index.vue' +import type { PanelPanelConfigStyleEnum } from '@/enums' +import { bytesToSize } from '@/utils/cmn' +import { getDiskStateByPath } from '@/api/system/systemMonitor' + +interface Prop { + cardTypeStyle: PanelPanelConfigStyleEnum + refreshInterval: number + textColor: string + progressColor: string + progressRailColor: string + path: string +} + +const props = defineProps<Prop>() +let timer: NodeJS.Timer +const diskState = ref<SystemMonitor.DiskInfo | null>(null) + +function correctionNumber(v: number, keepNum = 2): number { + return v === 0 ? 0 : Number(v.toFixed(keepNum)) +} + +function formatdiskSize(v: number): string { + return bytesToSize(v) +} + +function formatdiskToByte(v: number): number { + return v +} + +async function getData() { + try { + const { data, code } = await getDiskStateByPath<SystemMonitor.DiskInfo>(props.path) + if (code === 0) + diskState.value = data + } + catch (error) { + + } +} + +onMounted(() => { + getData() + timer = setInterval(() => { + getData() + }, (!props.refreshInterval || props.refreshInterval <= 2000) ? 2000 : props.refreshInterval) +}) + +onUnmounted(() => { + clearInterval(timer) +}) +</script> + +<template> + <div class="w-full"> + <GenericProgress + :progress-color="progressColor" + :progress-rail-color="progressRailColor" + :progress-height="5" + :percentage="correctionNumber(diskState?.usedPercent || 0)" + :card-type-style="cardTypeStyle" + :info-card-right-text="`${formatdiskSize(formatdiskToByte(diskState?.total || 0))}/${formatdiskSize(formatdiskToByte(diskState?.free || 0))}`" + :info-card-left-text="diskState?.mountpoint" + :text-color="textColor" + /> + </div> +</template> diff --git a/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/Memory.vue b/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/Memory.vue new file mode 100644 index 0000000..1453808 --- /dev/null +++ b/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/Memory.vue @@ -0,0 +1,64 @@ +<script setup lang="ts"> +import { onMounted, onUnmounted, ref } from 'vue' +import GenericProgress from '../components/GenericProgress/index.vue' +import { getMemonyState } from '@/api/system/systemMonitor' +import type { PanelPanelConfigStyleEnum } from '@/enums' +import { bytesToSize } from '@/utils/cmn' + +interface Prop { + cardTypeStyle: PanelPanelConfigStyleEnum + refreshInterval: number + textColor: string + progressColor: string + progressRailColor: string +} + +const props = defineProps<Prop>() +let timer: NodeJS.Timer +const memoryState = ref<SystemMonitor.MemoryInfo | null>(null) + +function correctionNumber(v: number, keepNum = 2): number { + return v === 0 ? 0 : Number(v.toFixed(keepNum)) +} + +function formatMemorySize(v: number): string { + return bytesToSize(v) +} + +async function getData() { + try { + const { data, code } = await getMemonyState<SystemMonitor.MemoryInfo>() + if (code === 0) + memoryState.value = data + } + catch (error) { + + } +} + +onMounted(() => { + getData() + timer = setInterval(() => { + getData() + }, (!props.refreshInterval || props.refreshInterval <= 2000) ? 2000 : props.refreshInterval) +}) + +onUnmounted(() => { + clearInterval(timer) +}) +</script> + +<template> + <div class="w-full"> + <GenericProgress + :progress-color="progressColor" + :progress-rail-color="progressRailColor" + :progress-height="5" + :percentage="correctionNumber(memoryState?.usedPercent || 0)" + :card-type-style="cardTypeStyle" + :info-card-right-text="`${formatMemorySize(memoryState?.total || 0)}/${formatMemorySize(memoryState?.free || 0)}`" + info-card-left-text="RAM" + :text-color="textColor" + /> + </div> +</template> diff --git a/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/index.vue b/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/index.vue new file mode 100644 index 0000000..d02f466 --- /dev/null +++ b/src/components/deskModule/SystemMonitor/AppIconSystemMonitor/index.vue @@ -0,0 +1,118 @@ +<script setup lang="ts"> +import { computed } from 'vue' +import { MonitorType } from '../typings' +import type { CardStyle } from '../typings' +import GenericMonitorCard from '../components/GenericMonitorCard/index.vue' +import CardCPU from './CPU.vue' +import Memory from './Memory.vue' +import Disk from './Disk.vue' +import { SvgIcon } from '@/components/common' +import { PanelPanelConfigStyleEnum } from '@/enums' + +interface Prop { + extendParam?: any + iconTextColor?: string + iconTextIconHideTitle: boolean + cardTypeStyle: PanelPanelConfigStyleEnum + monitorType: string + cardStyle: CardStyle +} + +const props = withDefaults(defineProps<Prop>(), {}) + +const iconText = computed(() => { + switch (props.monitorType) { + case MonitorType.cpu: + return 'CPU' + case MonitorType.disk: + return props.extendParam.path + case MonitorType.memory: + return 'RAM' + } + return '' +}) +const refreshInterval = 5000 +</script> + +<template> + <div class="w-full"> + <GenericMonitorCard + :icon-text-icon-hide-title="false" + :card-type-style="cardTypeStyle" + :icon-text="iconText" + :icon-text-color="iconTextColor" + :background-color="extendParam.backgroundColor" + class="hover:shadow-[0_0_20px_10px_rgba(0,0,0,0.2)]" + > + <template #icon> + <!-- 图标 --> + <div class="w-[60px] h-[70px]"> + <div class="w-[60px] h-full flex items-center justify-center text-white"> + <SvgIcon v-if="monitorType === MonitorType.cpu" icon="solar-cpu-bold" :style="{ color: extendParam.color }" style="width:35px;height:35px" /> + <SvgIcon v-if="monitorType === MonitorType.memory" icon="material-symbols-memory-alt-rounded" :style="{ color: extendParam.color }" style="width:35px;height:35px" /> + <SvgIcon v-if="monitorType === MonitorType.disk" icon="clarity-hard-disk-solid" :style="{ color: extendParam.color }" style="width:35px;height:35px" /> + </div> + </div> + </template> + <template #info> + <!-- 如果为纯白色,将自动根据背景的明暗计算字体的黑白色 --> + <div + class=" w-full text-white flex items-center" + > + <CardCPU + v-if="monitorType === MonitorType.cpu" + :card-type-style="PanelPanelConfigStyleEnum.info" + :progress-color="extendParam?.progressColor" + :progress-rail-color="extendParam?.progressRailColor" + :text-color="extendParam?.color" + :refresh-interval="refreshInterval" + /> + <Memory + v-else-if="monitorType === MonitorType.memory" + :card-type-style="PanelPanelConfigStyleEnum.info" + :progress-color="extendParam?.progressColor" + :progress-rail-color="extendParam?.progressRailColor" + :text-color="extendParam?.color" + :refresh-interval="refreshInterval" + /> + <Disk + v-else-if="monitorType === MonitorType.disk" + :card-type-style="PanelPanelConfigStyleEnum.info" + :progress-color="extendParam?.progressColor" + :progress-rail-color="extendParam?.progressRailColor" + :text-color="extendParam?.color" + :path="extendParam?.path" + :refresh-interval="refreshInterval" + /> + </div> + </template> + <template #small> + <CardCPU + v-if="monitorType === MonitorType.cpu" + :card-type-style="PanelPanelConfigStyleEnum.icon" + :progress-color="extendParam?.progressColor" + :progress-rail-color="extendParam?.progressRailColor" + :text-color="extendParam?.color" + :refresh-interval="refreshInterval" + /> + <Memory + v-else-if="monitorType === MonitorType.memory" + :card-type-style="PanelPanelConfigStyleEnum.icon" + :progress-color="extendParam?.progressColor" + :progress-rail-color="extendParam?.progressRailColor" + :text-color="extendParam?.color" + :refresh-interval="refreshInterval" + /> + <Disk + v-else-if="monitorType === MonitorType.disk" + :card-type-style="PanelPanelConfigStyleEnum.icon" + :progress-color="extendParam?.progressColor" + :progress-rail-color="extendParam?.progressRailColor" + :text-color="extendParam?.color" + :path="extendParam?.path" + :refresh-interval="refreshInterval" + /> + </template> + </GenericMonitorCard> + </div> +</template> diff --git a/src/components/deskModule/SystemMonitor/Edit/DiskEditor/index.vue b/src/components/deskModule/SystemMonitor/Edit/DiskEditor/index.vue new file mode 100644 index 0000000..3d9bd81 --- /dev/null +++ b/src/components/deskModule/SystemMonitor/Edit/DiskEditor/index.vue @@ -0,0 +1,163 @@ +<script setup lang="ts"> +import { computed, onMounted, ref } from 'vue' +import type { FormInst, FormRules } from 'naive-ui' +import { NColorPicker, NForm, NFormItem, NSelect } from 'naive-ui' +import type { DiskExtendParam } from '../../typings' +import GenericMonitorCard from '../../components/GenericMonitorCard/index.vue' +import GenericProgress from '../../components/GenericProgress/index.vue' +import { PanelPanelConfigStyleEnum } from '@/enums' +import { getDiskMountpoints } from '@/api/system/systemMonitor' +import { defautSwatchesBackground } from '@/utils/defaultData' +import { t } from '@/locales' + +interface Emit { + (e: 'update:diskExtendParam', visible: DiskExtendParam): void +} + +const props = defineProps<{ + diskExtendParam: DiskExtendParam +}>() +const emit = defineEmits<Emit>() +const formRef = ref<FormInst | null>(null) +const rules: FormRules = { + path: { + required: true, + message: t('form.required'), + trigger: 'blur', + }, +} + +const mountPointList = ref<{ + label: string + value: string +}[]>([]) + +const extendParam = computed({ + get: () => props.diskExtendParam, + set: (visible) => { + emit('update:diskExtendParam', visible) + }, +}) + +async function getMountPointList() { + try { + const { data } = await getDiskMountpoints<SystemMonitor.Mountpoint[]>() + mountPointList.value = [] + for (let i = 0; i < data.length; i++) { + const element = data[i] + if (i === 0 && !extendParam.value.path) + extendParam.value.path = element.mountpoint + + mountPointList.value.push({ + label: element.mountpoint, + value: element.mountpoint, + }) + } + } + catch (error) { + + } +} + +onMounted(() => { + getMountPointList() +}) + +defineExpose({ + async verification(): Promise<boolean> { + try { + const errors = await formRef.value?.validate() + + if (!errors) { + return Promise.resolve(true) + } + else { + console.log(errors) + return Promise.resolve(false) + } + } + catch (error) { + console.error('An error occurred during validation:', error) + return Promise.resolve(false) + } + }, +}) +</script> + +<template> + <div> + <!-- <div>{{ diskExtendParam }}</div> --> + <div class="flex mb-5 justify-center transparent-grid p-2 rounded-xl border"> + <div class="w-[200px]"> + <GenericMonitorCard + icon-text-icon-hide-title + :card-type-style="PanelPanelConfigStyleEnum.info" + icon="solar-cpu-bold" + :background-color="extendParam.backgroundColor" + :text-color="extendParam.color" + > + <template #info> + <GenericProgress + :progress-color="extendParam.progressColor" + :progress-rail-color="extendParam.progressRailColor" + :percentage="50" + :progress-height="5" + info-card-left-text="DEMO" + info-card-right-text="TEXT" + :text-color="extendParam.color" + :card-type-style="PanelPanelConfigStyleEnum.info" + /> + </template> + </GenericMonitorCard> + </div> + + <div class="w-[80px] ml-2"> + <GenericMonitorCard + icon-text-icon-hide-title + :card-type-style="PanelPanelConfigStyleEnum.icon" + icon="solar-cpu-bold" + :background-color="extendParam.backgroundColor" + :icon-text-color="extendParam.color" + > + <template #small> + <GenericProgress + :progress-color="extendParam.progressColor" + :progress-rail-color="extendParam.progressRailColor" + :percentage="50" + :text-color="extendParam.color" + :card-type-style="PanelPanelConfigStyleEnum.icon" + /> + </template> + </GenericMonitorCard> + </div> + </div> + + <NForm ref="formRef" v-model="extendParam" :model="extendParam" :rules="rules"> + <NFormItem :label="$t('deskModule.systemMonitor.diskMountPoint')" path="path"> + <NSelect v-model:value="extendParam.path" size="small" :options="mountPointList" /> + </NFormItem> + <NFormItem :label="$t('deskModule.systemMonitor.progressColor')"> + <NColorPicker v-model:value="extendParam.progressColor" :swatches="defautSwatchesBackground" :modes="['hex']" size="small" /> + </NFormItem> + <NFormItem :label="$t('deskModule.systemMonitor.progressRailColor')"> + <NColorPicker v-model:value="extendParam.progressRailColor" :swatches="defautSwatchesBackground" :modes="['hex']" size="small" /> + </NFormItem> + <NFormItem :label="$t('common.textColor')"> + <NColorPicker v-model:value="extendParam.color" :swatches="defautSwatchesBackground" :modes="['hex']" size="small" /> + </NFormItem> + <NFormItem :label="$t('common.backgroundColor')"> + <NColorPicker v-model:value="extendParam.backgroundColor" :swatches="defautSwatchesBackground" :modes="['hex']" size="small" /> + </NFormItem> + </NForm> + </div> +</template> + +<style> +.transparent-grid { + background-image: linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%), + linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%); + background-size: 16px 16px; + background-position: 0 0, 8px 8px; + background-color: #e2e8f0; +} +</style> diff --git a/src/components/deskModule/SystemMonitor/Edit/GenericProgressStyleEditor/index.vue b/src/components/deskModule/SystemMonitor/Edit/GenericProgressStyleEditor/index.vue new file mode 100644 index 0000000..7a16673 --- /dev/null +++ b/src/components/deskModule/SystemMonitor/Edit/GenericProgressStyleEditor/index.vue @@ -0,0 +1,101 @@ +<script setup lang="ts"> +import { computed } from 'vue' +import { NColorPicker, NForm, NFormItem } from 'naive-ui' +import type { GenericProgressStyleExtendParam } from '../../typings' +import GenericMonitorCard from '../../components/GenericMonitorCard/index.vue' +import GenericProgress from '../../components/GenericProgress/index.vue' +import { PanelPanelConfigStyleEnum } from '@/enums' +import { defautSwatchesBackground } from '@/utils/defaultData' + +interface Emit { + (e: 'update:genericProgressStyleExtendParam', visible: GenericProgressStyleExtendParam): void +} + +const props = defineProps<{ + genericProgressStyleExtendParam: GenericProgressStyleExtendParam +}>() +const emit = defineEmits<Emit>() + +const data = computed({ + get: () => props.genericProgressStyleExtendParam, + set: (visible) => { + emit('update:genericProgressStyleExtendParam', visible) + }, +}) +</script> + +<template> + <div> + <!-- <div>{{ genericProgressStyleExtendParam }}</div> + <div>{{ data }}</div> --> + <div class="flex mb-5 justify-center transparent-grid p-2 rounded-xl border"> + <div class="w-[200px]"> + <GenericMonitorCard + icon-text-icon-hide-title + :card-type-style="PanelPanelConfigStyleEnum.info" + icon="solar-cpu-bold" + :background-color="data.backgroundColor" + :text-color="data.color" + > + <template #info> + <GenericProgress + :progress-color="data.progressColor" + :progress-rail-color="data.progressRailColor" + :percentage="50" + :progress-height="5" + info-card-left-text="DEMO" + info-card-right-text="TEXT" + :text-color="data.color" + :card-type-style="PanelPanelConfigStyleEnum.info" + /> + </template> + </GenericMonitorCard> + </div> + + <div class="w-[80px] ml-2"> + <GenericMonitorCard + icon-text-icon-hide-title + :card-type-style="PanelPanelConfigStyleEnum.icon" + icon="solar-cpu-bold" + :background-color="data.backgroundColor" + :icon-text-color="data.color" + > + <template #small> + <GenericProgress + :progress-color="data.progressColor" + :progress-rail-color="data.progressRailColor" + :percentage="50" + :text-color="data.color" + :card-type-style="PanelPanelConfigStyleEnum.icon" + /> + </template> + </GenericMonitorCard> + </div> + </div> + + <NForm ref="formRef" v-model="data"> + <NFormItem :label="$t('deskModule.systemMonitor.progressColor')"> + <NColorPicker v-model:value="data.progressColor" :swatches="defautSwatchesBackground" :modes="['hex']" size="small" /> + </NFormItem> + <NFormItem :label="$t('deskModule.systemMonitor.progressRailColor')"> + <NColorPicker v-model:value="data.progressRailColor" :swatches="defautSwatchesBackground" :modes="['hex']" size="small" /> + </NFormItem> + <NFormItem :label="$t('common.textColor')"> + <NColorPicker v-model:value="data.color" :swatches="defautSwatchesBackground" :modes="['hex']" size="small" /> + </NFormItem> + <NFormItem :label="$t('common.backgroundColor')"> + <NColorPicker v-model:value="data.backgroundColor" :swatches="defautSwatchesBackground" :modes="['hex']" size="small" /> + </NFormItem> + </NForm> + </div> +</template> + +<style> +.transparent-grid { + background-image: linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%), + linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%); + background-size: 16px 16px; + background-position: 0 0, 8px 8px; + background-color: #e2e8f0; +} +</style> diff --git a/src/components/deskModule/SystemMonitor/Edit/index.vue b/src/components/deskModule/SystemMonitor/Edit/index.vue new file mode 100644 index 0000000..2012996 --- /dev/null +++ b/src/components/deskModule/SystemMonitor/Edit/index.vue @@ -0,0 +1,148 @@ +<script setup lang="ts"> +import { computed, defineEmits, defineProps, ref, watch } from 'vue' +import { NButton, NModal, NTabPane, NTabs, useMessage } from 'naive-ui' +import { MonitorType } from '../typings' +import type { DiskExtendParam, GenericProgressStyleExtendParam, MonitorData } from '../typings' +import { add, saveByIndex } from '../common' + +import GenericProgressStyleEditor from './GenericProgressStyleEditor/index.vue' +import DiskEditor from './DiskEditor/index.vue' +import { t } from '@/locales' + +interface Props { + visible: boolean + monitorData: MonitorData | null + index: number | null +} + +const props = defineProps<Props>() +const emit = defineEmits<Emit>() +const DiskEditorRef = ref<InstanceType<typeof DiskEditor>>() + +// 默认通用的进度扩展参数 +const defaultGenericProgressStyleExtendParam: GenericProgressStyleExtendParam = { + progressColor: '#fff', + progressRailColor: '#CFCFCFA8', + color: '#fff', + backgroundColor: '#2a2a2a6b', +} + +const defaultDiskExtendParam: DiskExtendParam = { + progressColor: '#fff', + progressRailColor: '#CFCFCFA8', + color: '#fff', + backgroundColor: '#2a2a2a6b', + path: '', +} + +const defaultMonitorData: MonitorData = { + extendParam: defaultGenericProgressStyleExtendParam, + monitorType: MonitorType.cpu, +} + +const active = ref<string>(MonitorType.cpu) +const currentMonitorData = ref<MonitorData>(props.monitorData || { ...defaultMonitorData }) +const currentGenericProgressStyleExtendParam = ref<GenericProgressStyleExtendParam>({ ...defaultGenericProgressStyleExtendParam }) +const currentDiskExtendParam = ref<DiskExtendParam>({ ...defaultDiskExtendParam }) + +const ms = useMessage() +const submitLoading = ref(false) + +interface Emit { + (e: 'update:visible', visible: boolean): void + (e: 'done', item: boolean): void +} + +// 更新值父组件传来的值 +const show = computed({ + get: () => props.visible, + set: (visible: boolean) => { + emit('update:visible', visible) + }, +}) + +watch(() => props.visible, (value) => { + active.value = props.monitorData?.monitorType || MonitorType.cpu + if (props.monitorData?.monitorType === MonitorType.cpu || props.monitorData?.monitorType === MonitorType.memory) + currentGenericProgressStyleExtendParam.value = { ...props.monitorData?.extendParam } + else if (props.monitorData?.monitorType === MonitorType.disk) + currentDiskExtendParam.value = { ...props.monitorData?.extendParam } + + if (!value) + handleResetExtendParam() +}) + +function handleResetExtendParam() { + currentGenericProgressStyleExtendParam.value = { ...defaultGenericProgressStyleExtendParam } + currentDiskExtendParam.value = { ...defaultDiskExtendParam } +} + +// 保存提交 +async function handleSubmit() { + let verificationRes = true + currentMonitorData.value.monitorType = active.value as MonitorType + if (currentMonitorData.value.monitorType === MonitorType.cpu || currentMonitorData.value.monitorType === MonitorType.memory) { + currentMonitorData.value.extendParam = currentGenericProgressStyleExtendParam + } + else if (currentMonitorData.value.monitorType === MonitorType.disk) { + currentMonitorData.value.extendParam = currentDiskExtendParam + const res = await DiskEditorRef.value?.verification() + if (res !== undefined) + verificationRes = res + } + + // console.log('保存', currentMonitorData.value.extendParam) + if (!verificationRes) + return + + if (props.index !== null) { + const res = await saveByIndex(props.index, currentMonitorData.value) + if (res) { + show.value = false + emit('done', true) + } + else { + ms.error(t('common.saveFail')) + } + } + else { + const res = await add(currentMonitorData.value) + if (res) { + show.value = false + emit('done', true) + } + else { + ms.error(t('common.saveFail')) + } + } +} +</script> + +<template> + <NModal v-model:show="show" preset="card" size="small" style="width: 600px;border-radius: 1rem;" :title="monitorData ? t('common.edit') : t('common.add')"> + <!-- 选择监视器 --> + <!-- <div> + {{ JSON.stringify(currentGenericProgressStyleExtendParam) }} + {{ JSON.stringify(currentDiskExtendParam) }} + </div> --> + <NTabs v-model:value="active" type="segment"> + <NTabPane :name="MonitorType.cpu" :tab="$t('deskModule.systemMonitor.cpuState')"> + <GenericProgressStyleEditor v-model:genericProgressStyleExtendParam="currentGenericProgressStyleExtendParam" /> + </NTabPane> + <NTabPane :name="MonitorType.memory" :tab="$t('deskModule.systemMonitor.memoryState')"> + <GenericProgressStyleEditor v-model:genericProgressStyleExtendParam="currentGenericProgressStyleExtendParam" /> + </NTabPane> + <NTabPane :name="MonitorType.disk" :tab="$t('deskModule.systemMonitor.diskState')"> + <DiskEditor ref="DiskEditorRef" v-model:disk-extend-param="currentDiskExtendParam" /> + </NTabPane> + </NTabs> + <NButton @click="handleResetExtendParam"> + {{ t('common.reset') }} + </NButton> + <template #footer> + <NButton type="success" :loading="submitLoading" style="float: right;" @click="handleSubmit"> + {{ t('common.confirm') }} + </NButton> + </template> + </NModal> +</template> diff --git a/src/components/deskModule/SystemMonitor/common.ts b/src/components/deskModule/SystemMonitor/common.ts new file mode 100644 index 0000000..0df0cd4 --- /dev/null +++ b/src/components/deskModule/SystemMonitor/common.ts @@ -0,0 +1,88 @@ +import type { MonitorData } from './typings' +import { useModuleConfig } from '@/store/modules' + +const modelName = 'systemMonitor' +const moduleConfig = useModuleConfig() + +export async function saveAll(value: MonitorData[]) { + return await moduleConfig.saveToCloud(modelName, { list: value }) +} + +export async function getAll(): Promise< MonitorData[]> { + const res = await moduleConfig.getValueByNameFromCloud<{ list: MonitorData[] }>(modelName) + if (res.code === 0 && res.data && res.data.list) + return res.data.list + return [] +} + +export async function add(value: MonitorData): Promise<boolean> { + let success = true + let newData: MonitorData[] = [] + try { + const data = await getAll() + if (data) + newData = data + + newData.push(value) + const res = await saveAll(newData) + if (res.code !== 0) + console.log('save failed', res) + } + catch (error) { + console.error(error) + success = false + } + return success +} + +export async function saveByIndex(index: number | undefined, value: MonitorData): Promise<boolean> { + if (!index) + index = 0 + + let success = true + let newData: MonitorData[] = [] + try { + const data = await getAll() + if (data) + newData = data + + newData[index] = value + const res = await saveAll(newData) + if (res.code !== 0) + console.log('save failed', res) + } + catch (error) { + console.error(error) + success = false + } + return success +} + +export async function getByIndex(index: number): Promise<MonitorData | null> { + try { + const data = await getAll() + if (data[index]) + return data[index] + } + catch (error) { + + } + + return null +} + +export async function deleteByIndex(index: number): Promise<boolean> { + let success = true + try { + const data = await getAll() + if (data[index]) + data.splice(index, 1) + await saveAll(data) + } + catch (error) { + success = false + console.error(error) + } + + return success +} diff --git a/src/components/deskModule/SystemMonitor/components/GenericMonitorCard/index.vue b/src/components/deskModule/SystemMonitor/components/GenericMonitorCard/index.vue new file mode 100644 index 0000000..58cf3ea --- /dev/null +++ b/src/components/deskModule/SystemMonitor/components/GenericMonitorCard/index.vue @@ -0,0 +1,61 @@ +<script setup lang="ts"> +// ------------------- +// 系统监控图标临时使用(与 AppIcon/index.vue 一致) +// 如果确定这种方案将 AppIcon/index.vue 封装成通用组件 +// ------------------- + +import { ref } from 'vue' +import { ItemCard, SvgIcon } from '@/components/common' +import type { PanelPanelConfigStyleEnum } from '@/enums' + +interface Prop { +// size?: number // 默认70 + extendParam?: any + iconTextColor?: string + iconTextIconHideTitle?: boolean + iconText?: string + textColor?: string + cardTypeStyle: PanelPanelConfigStyleEnum + // monitorType: string + icon?: string + class?: string + backgroundColor?: string +} + +const props = withDefaults(defineProps<Prop>(), {}) +const propClass = ref(props.class) +</script> + +<template> + <div class="w-full"> + <ItemCard + :card-type-style="cardTypeStyle" + :icon-text="iconText" + :icon-text-color="iconTextColor" + :class="propClass" + :background-color="backgroundColor" + :icon-text-icon-hide-title="iconTextIconHideTitle" + > + <template #info> + <!-- 图标 --> + <div class="w-[60px] h-[70px]"> + <div class="w-[60px] h-full flex items-center justify-center text-white"> + <slot name="icon"> + <SvgIcon :icon="icon ?? ''" style="width: 35px;height: 35px;" :style="{ color: textColor }" /> + </slot> + </div> + </div> + + <div + class=" w-full text-white flex items-center" + :style=" { maxWidth: 'calc(100% - 80px)' }" + > + <slot name="info" /> + </div> + </template> + <template #small> + <slot name="small" /> + </template> + </ItemCard> + </div> +</template> diff --git a/src/components/deskModule/SystemMonitor/components/GenericProgress/index.vue b/src/components/deskModule/SystemMonitor/components/GenericProgress/index.vue new file mode 100644 index 0000000..f42ec5e --- /dev/null +++ b/src/components/deskModule/SystemMonitor/components/GenericProgress/index.vue @@ -0,0 +1,57 @@ +<script setup lang="ts"> +import { NProgress } from 'naive-ui' +import { PanelPanelConfigStyleEnum } from '@/enums' + +interface Prop { + textColor: string + progressColor: string + progressRailColor: string + progressHeight?: number + percentage: number + cardTypeStyle: PanelPanelConfigStyleEnum + infoCardLeftText?: string + infoCardRightText?: string +} + +defineProps<Prop>() +</script> + +<template> + <div class="w-full"> + <div v-if="cardTypeStyle === PanelPanelConfigStyleEnum.info"> + <div class="mb-1 text-xs" :style="{ color: textColor }"> + <span> + {{ infoCardLeftText }} + </span> + <span class="float-right"> + {{ infoCardRightText }} + </span> + </div> + <NProgress + type="line" + :color="progressColor" + :rail-color="progressRailColor" + :height="progressHeight" + :percentage="percentage" + :show-indicator="false" + :stroke-width="15" + style="max-width: 135px;" + /> + </div> + <div v-else> + <div class="flex justify-center h-full w-full mt-3"> + <NProgress + :color="progressColor" + :rail-color="progressRailColor" + type="dashboard" + :percentage="percentage" :stroke-width="15" + style="width: 50px;" + > + <div class="text-white" style="font-size: 8px;" :style="{ color: textColor }"> + {{ percentage }}% + </div> + </NProgress> + </div> + </div> + </div> +</template> diff --git a/src/components/deskModule/SystemMonitor/index.vue b/src/components/deskModule/SystemMonitor/index.vue new file mode 100644 index 0000000..9824f73 --- /dev/null +++ b/src/components/deskModule/SystemMonitor/index.vue @@ -0,0 +1,308 @@ +<script setup lang="ts"> +import { nextTick, onMounted, ref } from 'vue' +import { VueDraggable } from 'vue-draggable-plus' +import { NButton, NDropdown, useDialog, useMessage } from 'naive-ui' +import AppIconSystemMonitor from './AppIconSystemMonitor/index.vue' +import { type CardStyle, type MonitorData, MonitorType } from './typings' +import Edit from './Edit/index.vue' +import { deleteByIndex, getAll, saveAll } from './common' +import { usePanelState } from '@/store' +import { PanelPanelConfigStyleEnum } from '@/enums' +import { SvgIcon } from '@/components/common' +import { t } from '@/locales' + +interface MonitorGroup extends Panel.ItemIconGroup { + sortStatus?: boolean + hoverStatus: boolean + items?: Panel.ItemInfo[] +} + +const props = defineProps<{ + allowEdit?: boolean + showTitle?: boolean +}>() +const panelState = usePanelState() + +const dialog = useDialog() +const ms = useMessage() + +const dropdownMenuX = ref(0) +const dropdownMenuY = ref(0) +const dropdownShow = ref(false) +const currentRightSelectIndex = ref<number | null>(null) + +const monitorGroup = ref<MonitorGroup>({ + hoverStatus: false, + sortStatus: false, +}) + +const editShowStatus = ref<boolean>(false) +const editData = ref<MonitorData | null>(null) +const editIndex = ref<number | null>(null) + +function handleAddItem() { + editShowStatus.value = true + editData.value = null + editIndex.value = null +} + +function handleSetSortStatus(sortStatus: boolean) { + monitorGroup.value.sortStatus = sortStatus + + // 并未保存排序重新更新数据 + if (!sortStatus) + getData() +} + +function handleSetHoverStatus(hoverStatus: boolean) { + monitorGroup.value.hoverStatus = hoverStatus +} + +const cardStyle: CardStyle = { + background: '#2a2a2a6b', +} + +const monitorDatas = ref<MonitorData[]>([]) + +function handleClick(index: number, item: MonitorData) { + if (!props.allowEdit) + return + editShowStatus.value = true + editData.value = item + editIndex.value = index +} + +async function getData() { + monitorDatas.value = await getAll() + + if (monitorDatas.value.length === 0) { + // 防止空 - 默认数据 + monitorDatas.value.push( + { + extendParam: { + backgroundColor: '#2a2a2a6b', + color: '#fff', + progressColor: '#fff', + progressRailColor: '#CFCFCFA8', + }, + monitorType: MonitorType.cpu, + }, + ) + + // 生成并保存 + saveAll(monitorDatas.value) + } +} + +onMounted(() => { + getData() +}) + +function handleSaveDone() { + getData() +} + +async function handleSaveSort() { + const { code } = await saveAll(monitorDatas.value) + if (code === 0) + monitorGroup.value.sortStatus = false +} + +function handleContextMenu(e: MouseEvent, index: number | null, item: MonitorData) { + if (index !== null) { + e.preventDefault() + currentRightSelectIndex.value = index + } + + nextTick().then(() => { + dropdownShow.value = true + dropdownMenuX.value = e.clientX + dropdownMenuY.value = e.clientY + }) +} + +function getDropdownMenuOptions() { + const dropdownMenuOptions = [ + { + label: t('common.delete'), + key: 'delete', + }, + ] + + return dropdownMenuOptions +} + +function onClickoutside() { + // message.info('clickoutside') + dropdownShow.value = false +} + +async function deleteOneByIndex(index: number) { + const res = await deleteByIndex(index) + if (res) + getData() +} + +function handleRightMenuSelect(key: string | number) { + dropdownShow.value = false + + switch (key) { + case 'delete': + dialog.warning({ + title: t('common.warning'), + content: t('common.deleteConfirm'), + positiveText: t('common.confirm'), + negativeText: t('common.cancel'), + onPositiveClick: () => { + if (monitorDatas.value.length <= 1) { + ms.warning(t('common.leastOne')) + return + } + if (currentRightSelectIndex.value !== null) + deleteOneByIndex(currentRightSelectIndex.value) + }, + }) + + break + default: + break + } +} +</script> + +<template> + <div class="w-full"> + <div + class="mt-[50px]" + :class="monitorGroup.sortStatus ? 'shadow-2xl border shadow-[0_0_30px_10px_rgba(0,0,0,0.3)] p-[10px] rounded-2xl' : ''" + @mouseenter="handleSetHoverStatus(true)" + @mouseleave="handleSetHoverStatus(false)" + > + <!-- 分组标题 --> + <div class="text-white text-xl font-extrabold mb-[20px] ml-[10px] flex items-center"> + <span v-if="showTitle" class="text-shadow"> + {{ $t('deskModule.systemMonitor.systemState') }} + </span> + <div + v-if="allowEdit" + class="ml-2 delay-100 transition-opacity flex" + :class="monitorGroup.hoverStatus ? 'opacity-100' : 'opacity-0'" + > + <span class="mr-2 cursor-pointer" @click="handleAddItem()"> + <SvgIcon class="text-white font-xl" icon="typcn:plus" /> + </span> + <span class="mr-2 cursor-pointer" @click="handleSetSortStatus(!monitorGroup.sortStatus)"> + <SvgIcon class="text-white font-xl" icon="ri:drag-drop-line" /> + </span> + </div> + </div> + + <!-- 详情图标 --> + <div v-if="panelState.panelConfig.iconStyle === PanelPanelConfigStyleEnum.info"> + <VueDraggable + v-model="monitorDatas" item-key="sort" :animation="300" + class="icon-info-box" + filter=".not-drag" + :disabled="!monitorGroup.sortStatus" + > + <div + v-for="item, index in monitorDatas" :key="index" + :title="item.description" + @click="handleClick(index, item)" + @contextmenu="(e) => handleContextMenu(e, index, item)" + > + <AppIconSystemMonitor + :extend-param="item.extendParam" + :icon-text-icon-hide-title="false" + :card-type-style="panelState.panelConfig.iconStyle" + :monitor-type="item.monitorType" + :card-style="cardStyle" + :icon-text-color="panelState.panelConfig.iconTextColor" + /> + </div> + </VueDraggable> + </div> + + <!-- APP图标宫型盒子 --> + <div v-if="panelState.panelConfig.iconStyle === PanelPanelConfigStyleEnum.icon"> + <div v-if="monitorDatas"> + <VueDraggable + v-model="monitorDatas" item-key="sort" :animation="300" + class="icon-small-box" + filter=".not-drag" + :disabled="!monitorGroup.sortStatus" + > + <div + v-for="item, index in monitorDatas" :key="index" + :title="item.description" + @click="handleClick(index, item)" + @contextmenu="(e) => handleContextMenu(e, index, item)" + > + <AppIconSystemMonitor + :extend-param="item.extendParam" + :icon-text-icon-hide-title="false" + :card-type-style="panelState.panelConfig.iconStyle" + :monitor-type="item.monitorType" + :card-style="cardStyle" + :icon-text-color="panelState.panelConfig.iconTextColor" + /> + </div> + </vuedraggable> + </div> + </div> + + <!-- 编辑栏 --> + <div v-if="monitorGroup.sortStatus && allowEdit" class="flex mt-[10px]"> + <div> + <NButton color="#2a2a2a6b" @click="handleSaveSort()"> + <template #icon> + <SvgIcon class="text-white font-xl" icon="material-symbols:save" /> + </template> + <div> + {{ $t('common.saveSort') }} + </div> + </NButton> + </div> + </div> + </div> + + <Edit v-model:visible="editShowStatus" :monitor-data="editData" :index="editIndex" @done="handleSaveDone" /> + + <NDropdown + placement="bottom-start" trigger="manual" :x="dropdownMenuX" :y="dropdownMenuY" + :options="getDropdownMenuOptions()" :show="dropdownShow" :on-clickoutside="onClickoutside" @select="handleRightMenuSelect" + /> + </div> +</template> + +<style scoped> +.text-shadow { + text-shadow: 2px 2px 50px rgb(0, 0, 0); +} + +.app-icon-text-shadow { + text-shadow: 2px 2px 5px rgb(0, 0, 0); +} + +.icon-info-box { + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 18px; + +} + +.icon-small-box { + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(75px, 1fr)); + gap: 18px; + +} + +@media (max-width: 500px) { + .icon-info-box{ + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + } +} +</style> diff --git a/src/components/deskModule/SystemMonitor/typings.ts b/src/components/deskModule/SystemMonitor/typings.ts new file mode 100644 index 0000000..2077144 --- /dev/null +++ b/src/components/deskModule/SystemMonitor/typings.ts @@ -0,0 +1,33 @@ +export enum MonitorType { + 'cpu' = 'cpu', // 图标风格 + 'memory' = 'memory', // 详情风格 + 'disk' = 'disk', +} + +export interface CardStyle { + background: string +} + +export interface MonitorData { + monitorType: MonitorType + extendParam?: { [key: string]: [value:any] } | any + description?: string + // cardStyle: CardStyle +} + +export interface ProgressStyle { + color: string + railColor: string + height: number +} + +export interface GenericProgressStyleExtendParam { + progressColor: string + progressRailColor: string + color: string + backgroundColor: string +} + +export interface DiskExtendParam extends GenericProgressStyleExtendParam { + path: string +} diff --git a/src/components/deskModule/index.ts b/src/components/deskModule/index.ts index 92c8882..5f85ba7 100644 --- a/src/components/deskModule/index.ts +++ b/src/components/deskModule/index.ts @@ -1,4 +1,5 @@ import Clock from './Clock/index.vue' import SearchBox from './SearchBox/index.vue' +import SystemMonitor from './SystemMonitor/index.vue' -export { Clock, SearchBox } +export { Clock, SearchBox, SystemMonitor } diff --git a/src/enums/panel/index.ts b/src/enums/panel/index.ts index 1cd9044..01f847d 100644 --- a/src/enums/panel/index.ts +++ b/src/enums/panel/index.ts @@ -8,4 +8,5 @@ export enum PanelStateNetworkModeEnum { export enum PanelPanelConfigStyleEnum { 'icon' = 1, // 图标风格 'info' = 0, // 详情风格 + 'small' = 1, // 同icon } diff --git a/src/locales/en-US.json b/src/locales/en-US.json new file mode 100644 index 0000000..bda37ae --- /dev/null +++ b/src/locales/en-US.json @@ -0,0 +1,246 @@ +{ + "adminSettingUsers": { + "EditpasswordPlaceholder": "Enter new password, leave blank to keep the current password", + "alertText": "Data between accounts is not shared", + "appName": "User Management", + "currentUseUsername": "Current username", + "deletePromptContent": "Are you sure you want to delete {name} ({username})?", + "formRules": { + "passwordLimit": "6-20 characters", + "roleRequired": "Please select a role", + "usernameRequired": "Enter username with at least 5 characters" + }, + "passwordPlaceholder": "Enter password", + "pblicText": "Public", + "role": "Role", + "setOrUnsetPublicMode": "Set/Unset public access", + "userCountText": "Total {count} users" + }, + "adminSettingUsers.alertText": "Data between accounts are not interoperable", + "api": { + "loginExpires": "Login status has expired, please login again" + }, + "appLauncher": { + "title": "System Applications & Settings" + }, + "apps": { + "about": { + "QQGroup": "QQ Group:", + "QR": "QR Code (Recommended)", + "addQQGroupUrl": "Click to add", + "appName": "About", + "author": "Author:", + "checkUpdate": "Check for new version", + "donate": "🧧 Donate", + "issue": "Feedback:", + "viewUpdateLog": "Click here to view update log" + }, + "baseSettings": { + "appName": "Basic Settings", + "bottomMargin": "Bottom margin", + "clock": "Clock component", + "clockSecondShow": "Show seconds", + "configFailed": "Configuration save failed, {message}", + "configSaved": "Configuration saved", + "contentArea": "Content area", + "customFooter": "Custom footer", + "customImageAddress": "Custom image address", + "detailIcon": "Detail icon", + "hideDescription": "Hide description information", + "hideTitle": "Hide title", + "leftRightMargin": "Left-right margin", + "mask": "Mask", + "maxWidth": "Max width", + "publicVisitModeShow": "Allow public mode display", + "resetWarnText": "Are you sure you want to reset these styles?", + "searchBar": "Search bar component", + "searchBarSearchItem": "Allow search bar to search items", + "searchBarShow": "Configuration saved", + "show": "Show", + "showTitle": "Show title", + "smallIcon": "Small icon", + "systemMonitorStatus": "System status component", + "textContent": "Text content", + "topMargin": "Top margin", + "uploadOrDragText": "Click to upload or drag and drop into the box to replace the image", + "vague": "Blur", + "wallpaper": "Wallpaper" + }, + "exportImport": { + "appName": "Export/Import", + "errorConfigFileFormat": "The configuration file format is incorrect and cannot be imported", + "errorConfigFileLow": "The configuration file version is too low and cannot be compatible", + "export": "Export configuration", + "fileModified": "The file has been modified, import with caution", + "import": "Import configuration", + "moduleIcon": "Icon configuration", + "moduleStyle": "Style configuration", + "selectExportData": "Select the configuration data to export", + "selectImportData": "Select the configuration data to import", + "softwareVersionLow": "The current software version may be too old and may not be compatible with this configuration file. It is recommended to update the software to the latest version before importing again.", + "tip": "Importing icon configuration data will not clear existing icon data", + "transmuteStandard": "Transmute Standard", + "warnConfigFileLow": "The configuration file version is too low but compatible" + }, + "itemGroupManage": { + "appName": "Group Management", + "deleteWarnText": "Are you sure you want to delete this group [{name}]? The application icons in this group will be lost after deletion.", + "groupName": "Group name" + }, + "uploadsFileManager": { + "alertText": "Here you can manage wallpapers and icons you have uploaded.", + "appName": "Uploads File Management", + "copyFailed": "Copy failed", + "copyLink": "Copy link", + "copySuccess": "Link copied successfully. You can paste it in the address bar.", + "deleteWarningText": "Cannot be recovered after deletion. Are you sure you want to continue?", + "fileName": "Original file name", + "infoTitle": "File details", + "nothingText": "You haven't uploaded any images yet.", + "path": "File path", + "setWallpaper": "Set as wallpaper", + "uploadTime": "Upload time" + }, + "userInfo": { + "appName": "My Information", + "theme": "Theme", + "themeStyle": { + "auto": "Follow the system ", + "dark": "Dark ", + "light": "Light " + } + } + }, + "common": { + "action": "Action", + "add": "Add", + "addSuccess": "Added successfully", + "appName": "Sun-Panel", + "backgroundColor": "Background color", + "cancel": "Cancel", + "confirm": "Confirm", + "continue": "Continue", + "delete": "Delete", + "deleteConfirm": "Are you sure you want to delete?", + "deleteConfirmByName": "Are you sure you want to delete {name}?", + "deleteFail": "Delete failed", + "deleteSuccess": "Deleted successfully", + "description": "Description", + "download": "Download", + "edit": "Edit", + "editFail": "Edit failed", + "editSuccess": "Edited successfully", + "export": "Export", + "exportSuccess": "Exported successfully", + "failed": "Operation failed", + "icon": "Icon", + "image": "Image", + "import": "Import", + "importSuccess": "Imported successfully", + "inputPlaceholder": "Please enter", + "inputPlaceholderByText": "Please enter {text}", + "language": "Language", + "leastOne": "Please keep at least one", + "nikeName": "Nickname", + "no": "No", + "noData": "No data available", + "password": "Password", + "refreshPage": "Please refresh the page", + "repeatLater": "Please try again later", + "reset": "Reset", + "role": { + "admin": "Admin", + "regularUser": "Regular" + }, + "save": "Save", + "saveFail": "Save failed", + "saveSort": "Save sort", + "saveSuccess": "Saved successfully", + "serverError": "Server error", + "show": "Show", + "sort": "Sort", + "style": "Style", + "success": "Operation successful", + "text": "Text", + "textColor": "Text color", + "title": "Title", + "unknownError": "Unknown error", + "uploadFail": "Upload failed", + "username": "Username", + "verify": "Verify", + "warning": "Warning", + "yes": "Yes" + }, + "deskModule": { + "clock": { + "fri": "Friday", + "mon": "Monday", + "sat": "Saturday", + "sun": "Sunday", + "thu": "Thursday", + "tue": "Tuesday", + "wed": "Wednesday" + }, + "searchBox": { + "inputPlaceholder": "Enter search content", + "openWithNewOpen": "Open in new window" + }, + "systemMonitor": { + "cpuState": "CPU status", + "diskMountPoint": "Mount point", + "diskState": "Disk status", + "memoryState": "Memory status", + "progressColor": "Main color", + "progressRailColor": "Secondary color", + "systemState": "System status" + } + }, + "form": { + "required": "Required field" + }, + "iconItem": { + "add": "Add item", + "currentPageLayerOpen": "Open in current page as layer", + "currentPageOpen": "Open in current page", + "edit": "Edit item", + "getGroupFail": "Failed to get group information", + "getIcon": "Get icon", + "geticonFail": "Failed to get icon", + "iconGroup": "Group", + "inputIconName": "Enter icon name", + "inputIconUrlOrUpload": "Enter icon URL or upload", + "lanUrl": "LAN URL", + "lanUrlInputPlaceholder": "http(s):// (LAN mode, will redirect to this address)", + "newWindowOpen": "Open in new window", + "onlineIcon": "Online icon", + "onlineIconLibrary": "Online icon library", + "openMethod": "Open method", + "selectUpload": "Local upload", + "url": "URL" + }, + "login": { + "loginButton": "Login", + "passwordPlaceholder": "Password", + "usernamePlaceholder": "Username", + "welcomeMessage": "Welcome back!" + }, + "panelHome": { + "changeToLanModel": "Switch to LAN mode", + "changeToLanModelSuccess": "Switched to LAN mode (mode status saved locally)", + "changeToWanModel": "Switch to WAN mode", + "changeToWanModelSuccess": "Switched to WAN mode (mode status saved locally)", + "goToLogin": "Go to login", + "openLanUrl": "Open LAN URL", + "openWanUrl": "Open WAN URL" + }, + "settingUserInfo": { + "confirmLogoutText": "Are you sure you want to logout?", + "confirmPassword": "Confirm new password", + "confirmPasswordInconsistentMsg": "Passwords do not match", + "logout": "Logout", + "logoutSuccess": "You have safely logged out. Looking forward to seeing you again!", + "newPassword": "New password", + "oldPassword": "Old password", + "updatePassword": "Change password" + } +} diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts deleted file mode 100644 index d033be4..0000000 --- a/src/locales/en-US.ts +++ /dev/null @@ -1,94 +0,0 @@ -export default { - common: { - add: 'Add', - addSuccess: 'Add Success', - edit: 'Edit', - editSuccess: 'Edit Success', - delete: 'Delete', - deleteSuccess: 'Delete Success', - save: 'Save', - saveSuccess: 'Save Success', - reset: 'Reset', - action: 'Action', - export: 'Export', - exportSuccess: 'Export Success', - import: 'Import', - importSuccess: 'Import Success', - clear: 'Clear', - clearSuccess: 'Clear Success', - yes: 'Yes', - no: 'No', - confirm: 'Confirm', - download: 'Download', - noData: 'No Data', - wrong: 'Something went wrong, please try again later.', - success: 'Success', - failed: 'Failed', - verify: 'Verify', - unauthorizedTips: 'Unauthorized, please verify first.', - }, - chat: { - newChatButton: 'New Chat', - placeholder: 'Ask me anything...(Shift + Enter = line break, "/" to trigger prompts)', - placeholderMobile: 'Ask me anything...', - copy: 'Copy', - copied: 'Copied', - copyCode: 'Copy Code', - clearChat: 'Clear Chat', - clearChatConfirm: 'Are you sure to clear this chat?', - exportImage: 'Export Image', - exportImageConfirm: 'Are you sure to export this chat to png?', - exportSuccess: 'Export Success', - exportFailed: 'Export Failed', - usingContext: 'Context Mode', - turnOnContext: 'In the current mode, sending messages will carry previous chat records.', - turnOffContext: 'In the current mode, sending messages will not carry previous chat records.', - deleteMessage: 'Delete Message', - deleteMessageConfirm: 'Are you sure to delete this message?', - deleteHistoryConfirm: 'Are you sure to clear this history?', - clearHistoryConfirm: 'Are you sure to clear chat history?', - preview: 'Preview', - showRawText: 'Show as raw text', - }, - setting: { - setting: 'Setting', - general: 'General', - advanced: 'Advanced', - config: 'Config', - avatarLink: 'Avatar Link', - name: 'Name', - description: 'Description', - role: 'Role', - temperature: 'Temperature', - top_p: 'Top_p', - resetUserInfo: 'Reset UserInfo', - chatHistory: 'ChatHistory', - theme: 'Theme', - language: 'Language', - api: 'API', - reverseProxy: 'Reverse Proxy', - timeout: 'Timeout', - socks: 'Socks', - httpsProxy: 'HTTPS Proxy', - balance: 'API Balance', - monthlyUsage: 'Monthly Usage', - }, - store: { - siderButton: 'Prompt Store', - local: 'Local', - online: 'Online', - title: 'Title', - description: 'Description', - clearStoreConfirm: 'Whether to clear the data?', - importPlaceholder: 'Please paste the JSON data here', - addRepeatTitleTips: 'Title duplicate, please re-enter', - addRepeatContentTips: 'Content duplicate: {msg}, please re-enter', - editRepeatTitleTips: 'Title conflict, please revise', - editRepeatContentTips: 'Content conflict {msg} , please re-modify', - importError: 'Key value mismatch', - importRepeatTitle: 'Title repeatedly skipped: {msg}', - importRepeatContent: 'Content is repeatedly skipped: {msg}', - onlineImportWarning: 'Note: Please check the JSON file source!', - downloadError: 'Please check the network status and JSON file validity', - }, -} diff --git a/src/locales/index.ts b/src/locales/index.ts index ed81036..073406b 100644 --- a/src/locales/index.ts +++ b/src/locales/index.ts @@ -1,20 +1,15 @@ import type { App } from 'vue' import { createI18n } from 'vue-i18n' -import enUS from './en-US' +import enUS from './en-US.json' // import koKR from './ko-KR' -import zhCN from './zh-CN' -// import zhTW from './zh-TW' +import zhCN from './zh-CN.json' // import ruRU from './ru-RU' -import { useAppStoreWithOut } from '@/store/modules/app' -import type { Language } from '@/store/modules/app/helper' -const appStore = useAppStoreWithOut() - -const defaultLocale = appStore.language || 'zh-CN' +const defaultLocale = 'zh-CN' const i18n = createI18n({ locale: defaultLocale, - fallbackLocale: 'en-US', + fallbackLocale: defaultLocale, allowComposition: true, messages: { 'en-US': enUS, @@ -27,7 +22,9 @@ const i18n = createI18n({ export const t = i18n.global.t -export function setLocale(locale: Language) { +// 避免循环依赖appstore(authstore)language此处暂时先使用any +// 后面有时间调整 +export function setLocale(locale: any) { i18n.global.locale = locale } diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json new file mode 100644 index 0000000..c475b0c --- /dev/null +++ b/src/locales/zh-CN.json @@ -0,0 +1,245 @@ +{ + "adminSettingUsers": { + "EditpasswordPlaceholder": "请输入新密码,留空密码不变", + "alertText": "账号之间的数据不互通", + "appName": "用户管理", + "currentUseUsername": "当前账号", + "deletePromptContent": "你确定删除{name}({username})?", + "formRules": { + "passwordLimit": "6-20个字符", + "roleRequired": "请选择角色", + "usernameRequired": "请输入账号且大于5个字符" + }, + "passwordPlaceholder": "请输入密码", + "pblicText": "公开", + "role": "角色", + "setOrUnsetPublicMode": "设置/取消公开访问", + "userCountText": "共{count}位用户" + }, + "api": { + "loginExpires": "登录状态已过期,请重新登录" + }, + "appLauncher": { + "title": "系统应用 & 设置" + }, + "apps": { + "about": { + "QQGroup": "QQ交流群:", + "QR": "二维码(推荐)", + "addQQGroupUrl": "点击添加", + "appName": "关于", + "author": "作者:", + "checkUpdate": "检查新版本", + "donate": "🧧打赏", + "issue": "建议反馈:", + "viewUpdateLog": "点此查看更新说明" + }, + "baseSettings": { + "appName": "基础设置", + "bottomMargin": "下边距", + "clock": "时钟组件", + "clockSecondShow": "显示秒", + "configFailed": "配置保存失败,{message}", + "configSaved": "配置已保存", + "contentArea": "内容区域", + "customFooter": "自定义footer", + "customImageAddress": "自定义图片地址", + "detailIcon": "详情图标", + "hideDescription": "隐藏描述信息", + "hideTitle": "隐藏标题", + "leftRightMargin": "左右边距", + "mask": "遮罩", + "maxWidth": "最大宽度", + "publicVisitModeShow": "公开模式允许显示", + "resetWarnText": "确定要重置这些样式吗?", + "searchBar": "搜索栏组件", + "searchBarSearchItem": "允许搜索栏搜索项目", + "searchBarShow": "配置已保存", + "show": "显示", + "showTitle": "显示标题", + "smallIcon": "小图标", + "systemMonitorStatus": "系统状态组件", + "textContent": "文本内容", + "topMargin": "上边距", + "uploadOrDragText": "点击上传替换图片或拖拽到框内", + "vague": "模糊", + "wallpaper": "壁纸" + }, + "exportImport": { + "appName": "导出导入", + "errorConfigFileFormat": "配置文件格式不正确,无法导入", + "errorConfigFileLow": "配置文件版本过低,无法兼容", + "export": "导出配置", + "fileModified": "文件被修改过,谨慎导入", + "import": "导入配置", + "moduleIcon": "图标配置", + "moduleStyle": "样式配置", + "selectExportData": "请选择要导出的配置数据", + "selectImportData": "请选择要导入的配置数据", + "softwareVersionLow": "当前软件版本可能过旧,很有可能无法兼容该配置文件,请谨慎导入。推荐将软件更新到新版后再次导入", + "tip": "导入图标配置数据不会清空现有图标数据", + "transmuteStandard": "浏览器书签转换工具", + "warnConfigFileLow": "配置文件版本过低,但是兼容" + }, + "itemGroupManage": { + "appName": "分组管理", + "deleteWarnText": "你确定删除此分组[{name}],删除后此分组应用图标将丢失?", + "groupName": "分组名称" + }, + "uploadsFileManager": { + "alertText": "你可以在这里管理你上传过的壁纸和图标", + "appName": "上传文件管理", + "copyFailed": "复制失败", + "copyLink": "复制链接", + "copySuccess": "链接复制成功,可以在图标地址栏", + "deleteWarningText": "删除后无法恢复,你确定要继续吗?", + "fileName": "原文件名", + "infoTitle": "文件详情", + "nothingText": "你还没有上传过任何图片", + "path": "文件路径", + "setWallpaper": "设置为壁纸", + "uploadTime": "上传时间" + }, + "userInfo": { + "appName": "我的信息", + "theme": "主题", + "themeStyle": { + "auto": "跟随系统 ", + "dark": "深色 ", + "light": "浅色 " + } + } + }, + "common": { + "action": "操作", + "add": "添加", + "addSuccess": "添加成功", + "appName": "Sun-Panel", + "backgroundColor": "背景颜色", + "cancel": "取消", + "confirm": "确定", + "continue": "继续", + "delete": "删除", + "deleteConfirm": "你确定要删除吗?", + "deleteConfirmByName": "你确定要删除{name}吗?", + "deleteFail": "删除失败", + "deleteSuccess": "删除成功", + "description": "描述信息", + "download": "下载", + "edit": "编辑", + "editFail": "编辑失败", + "editSuccess": "编辑成功", + "export": "导出", + "exportSuccess": "导出成功", + "failed": "操作失败", + "icon": "图标", + "image": "图片", + "import": "导入", + "importSuccess": "导入成功", + "inputPlaceholder": "请输入", + "inputPlaceholderByText": "请输入{text}", + "language": "语言", + "leastOne": "请至少保留一项", + "nikeName": "昵称", + "no": "否", + "noData": "暂无数据", + "password": "密码", + "refreshPage": "请刷新页面", + "repeatLater": "请稍后重试", + "reset": "重置", + "role": { + "admin": "管理", + "regularUser": "普通" + }, + "save": "保存", + "saveFail": "保存成功", + "saveSort": "保存排序", + "saveSuccess": "保存成功", + "serverError": "服务器错误", + "show": "显示", + "sort": "排序", + "style": "风格", + "success": "操作成功", + "text": "文字", + "textColor": "文字颜色", + "title": "标题", + "unknownError": "未知错误", + "uploadFail": "上传失败", + "username": "账号", + "verify": "验证", + "warning": "警告", + "yes": "是" + }, + "deskModule": { + "clock": { + "fri": "星期五", + "mon": "星期一", + "sat": "星期六", + "sun": "星期日", + "thu": "星期四", + "tue": "星期二", + "wed": "星期三" + }, + "searchBox": { + "inputPlaceholder": "请输入搜索内容", + "openWithNewOpen": "新窗口打开" + }, + "systemMonitor": { + "cpuState": "CPU状态", + "diskMountPoint": "挂载点", + "diskState": "磁盘状态", + "memoryState": "内存状态", + "progressColor": "主色", + "progressRailColor": "副色", + "systemState": "系统状态" + } + }, + "form": { + "required": "必填项" + }, + "iconItem": { + "add": "添加项目", + "currentPageLayerOpen": "当前页面弹窗打开", + "currentPageOpen": "当前页面打开", + "edit": "修改项目", + "getGroupFail": "分组信息获取失败", + "getIcon": "获取图标", + "geticonFail": "图标获取失败", + "iconGroup": "分组", + "inputIconName": "请输入图标名称", + "inputIconUrlOrUpload": "输入图标地址或上传", + "lanUrl": "局域网地址", + "lanUrlInputPlaceholder": "http(s)://(局域网模式,会跳转该地址)", + "newWindowOpen": "新窗口打开", + "onlineIcon": "在线图标", + "onlineIconLibrary": "在线图标库", + "openMethod": "打开方式", + "selectUpload": "本地上传", + "url": "地址" + }, + "login": { + "loginButton": "登录", + "passwordPlaceholder": "密码", + "usernamePlaceholder": "账号", + "welcomeMessage": "欢迎回来!" + }, + "panelHome": { + "changeToLanModel": "切换为内网模式", + "changeToLanModelSuccess": "已经切换到内网模式(模式状态仅保存在本地)", + "changeToWanModel": "切换为公网模式", + "changeToWanModelSuccess": "已经切换到公网模式(模式状态仅保存在本地)", + "goToLogin": "前往登录", + "openLanUrl": "打开局域网地址", + "openWanUrl": "打开公网地址" + }, + "settingUserInfo": { + "confirmLogoutText": "你确定要退出登录吗?", + "confirmPassword": "确认新密码", + "confirmPasswordInconsistentMsg": "两次密码不一致", + "logout": "退出登录", + "logoutSuccess": "您已经安全退出,期待与你再次相见!", + "newPassword": "新密码", + "oldPassword": "旧密码", + "updatePassword": "修改密码" + } +} diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts deleted file mode 100644 index 3988e25..0000000 --- a/src/locales/zh-CN.ts +++ /dev/null @@ -1,121 +0,0 @@ -export default { - common: { - appName: 'Sun-Panel', - add: '添加', - addSuccess: '添加成功', - edit: '编辑', - editSuccess: '编辑成功', - editFail: '编辑失败', - delete: '删除', - deleteSuccess: '删除成功', - save: '保存', - saveSuccess: '保存成功', - reset: '重置', - action: '操作', - export: '导出', - exportSuccess: '导出成功', - import: '导入', - importSuccess: '导入成功', - // clear: '清空', - // clearSuccess: '清空成功', - yes: '是', - no: '否', - confirm: '确定', - cancel: '取消', - warning: '警告', - download: '下载', - noData: '暂无数据', - // wrong: '好像出错了,请稍后再试。', - success: '操作成功', - failed: '操作失败', - verify: '验证', - unauthorizedTips: '未经授权,请先进行验证。', - inputPlaceholder: '请输入', - inputPlaceholderByText: '请输入{text}', - username: '账号', - nikeName: '昵称', - password: '密码', - serverError: '服务器错误', - role: { - regularUser: '普通', - admin: '管理', - }, - }, - setting: { - setting: '设置', - general: '总览', - advanced: '高级', - config: '配置', - avatarLink: '头像链接', - avatar: '头像', - name: '名称', - description: '描述', - role: '角色设定', - temperature: 'Temperature', - top_p: '回答多样性', - resetUserInfo: '重置用户信息', - chatHistory: '聊天记录', - theme: '主题', - language: '语言', - api: 'API', - reverseProxy: '反向代理', - timeout: '超时', - socks: 'Socks', - httpsProxy: 'HTTPS Proxy', - balance: 'API余额', - monthlyUsage: '本月使用量', - }, - login: { - loginButton: '登录', - usernamePlaceholder: '请输入账号', - passwordPlaceholder: '请输入密码', - welcomeMessage: '欢迎回来!', - }, - settingUserInfo: { - updatePassword: '修改密码', - oldPassword: '旧密码', - newPassword: '新密码', - confirmPassword: '确认新密码', - confirmPasswordInconsistentMsg: '两次密码不一致', - logout: '退出登录', - confirmLogoutText: '你确定要退出登录吗?', - logoutSuccess: '您已经安全退出,期待与你再次相见!', - }, - adminSettingUsers: { - passwordPlaceholder: '请输入密码', - EditpasswordPlaceholder: '请输入新密码,留空密码不变', - role: '角色', - formRules: { - usernameRequired: '请输入账号且大于5个字符', - roleRequired: '请选择角色', - passwordLimit: '6-20个字符', - }, - alertText: '账号之间的数据不互通', - userCountText: '共{count}位用户', - deletePromptContent: '你确定删除{name}({username})?', - currentUseUsername: '当前账号', - setOrUnsetPublicMode: '设置/取消公开访问', - pblicText: '公开', - }, - deskModule: { - searchBox: { - openWithNewOpen: '新窗口打开', - inputPlaceholder: '请输入搜索内容', - }, - }, - apps: { - uploadsFileManager: { - copyLink: '复制链接', - infoTitle: '文件详情', - fileName: '原文件名', - path: '文件路径', - uploadTime: '上传时间', - deleteWarningText: '删除后无法恢复,你确定要继续吗?', - copySuccess: '链接复制成功,可以在图标地址栏', - copyFailed: '复制失败', - setWallpaper: '设置为壁纸', - alertText: '你可以在这里管理你上传过的壁纸和图标', - nothingText: '你还没有上传过任何图片', - }, - }, -} diff --git a/src/store/modules/app/helper.ts b/src/store/modules/app/helper.ts index f94b6f6..9fb3e00 100644 --- a/src/store/modules/app/helper.ts +++ b/src/store/modules/app/helper.ts @@ -13,7 +13,12 @@ export interface AppState { } export function defaultSetting(): AppState { - return { siderCollapsed: false, theme: 'light', language: 'zh-CN' } + const lan = (navigator.language).toLowerCase() + let language: Language = 'en-US' + if (lan.includes('zh')) + language = 'zh-CN' + + return { siderCollapsed: false, theme: 'light', language } } export function getLocalSetting(): AppState { @@ -24,3 +29,7 @@ export function getLocalSetting(): AppState { export function setLocalSetting(setting: AppState): void { ss.set(LOCAL_NAME, setting) } + +export function removeLocalState() { + ss.remove(LOCAL_NAME) +} diff --git a/src/store/modules/app/index.ts b/src/store/modules/app/index.ts index 055d1f8..4efbdfd 100644 --- a/src/store/modules/app/index.ts +++ b/src/store/modules/app/index.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia' import type { AppState, Language, Theme } from './helper' -import { getLocalSetting, setLocalSetting } from './helper' +import { defaultSetting, getLocalSetting, removeLocalState, setLocalSetting } from './helper' import { store } from '@/store' export const useAppStore = defineStore('app-store', { @@ -26,6 +26,11 @@ export const useAppStore = defineStore('app-store', { recordState() { setLocalSetting(this.$state) }, + + removeToken() { + this.$state = defaultSetting() + removeLocalState() + }, }, }) diff --git a/src/store/modules/moduleConfig/index.ts b/src/store/modules/moduleConfig/index.ts index 7b24926..cf8bcb8 100644 --- a/src/store/modules/moduleConfig/index.ts +++ b/src/store/modules/moduleConfig/index.ts @@ -33,10 +33,10 @@ export const useModuleConfig = defineStore('module-config-store', { }, // 保存到网络 - saveToCloud(name: string, value: any) { + async saveToCloud(name: string, value: any) { const moduleName = `module-${name}` // 保存至网络 - save(moduleName, value) + return save(moduleName, value) }, // 从网络同步 diff --git a/src/store/modules/panel/helper.ts b/src/store/modules/panel/helper.ts index d660409..934e186 100644 --- a/src/store/modules/panel/helper.ts +++ b/src/store/modules/panel/helper.ts @@ -3,6 +3,8 @@ import { PanelPanelConfigStyleEnum, PanelStateNetworkModeEnum } from '@/enums' import defaultBackground from '@/assets/defaultBackground.webp' const LOCAL_NAME = 'panelStorage' +const defaultFooterHtml = '<div class="flex justify-center text-slate-300" style="margin-top:100px">Powered By <a href="https://github.com/hslr-s/sun-panel" target="_blank" class="ml-[5px]">Sun-Panel</a></div>' + export function defaultStatePanelConfig(): Panel.panelConfig { return { backgroundImageSrc: defaultBackground, @@ -22,6 +24,11 @@ export function defaultStatePanelConfig(): Panel.panelConfig { maxWidth: 1200, maxWidthUnit: 'px', marginX: 5, + footerHtml: defaultFooterHtml, + systemMonitorShow: false, + systemMonitorShowTitle: true, + systemMonitorPublicVisitModeShow: false, + } } diff --git a/src/typings/panel.d.ts b/src/typings/panel.d.ts index d99f4cc..384d76a 100644 --- a/src/typings/panel.d.ts +++ b/src/typings/panel.d.ts @@ -55,6 +55,10 @@ declare namespace Panel { maxWidth?:number maxWidthUnit:string marginX?:number + footerHtml?:string + systemMonitorShow?:boolean + systemMonitorShowTitle?:boolean + systemMonitorPublicVisitModeShow?:boolean } interface userConfig{ diff --git a/src/typings/systemMonitor.d.ts b/src/typings/systemMonitor.d.ts new file mode 100644 index 0000000..7f42e47 --- /dev/null +++ b/src/typings/systemMonitor.d.ts @@ -0,0 +1,43 @@ +declare namespace SystemMonitor { + + interface CPUInfo { + coreCount: number + cpuNum: number + model: string + usages: number[] + } + + interface DiskInfo { + mountpoint: string + total: number + used: number + free: number + usedPercent: number + } + + interface NetIOCountersInfo { + bytesSent: number + bytesRecv: number + name: string + } + + interface MemoryInfo { + total: number + used: number + free: number + usedPercent: number + } + + interface GetAllRes { + cpuInfo: CPUInfo + diskInfo: DiskInfo[] + netIOCountersInfo: NetIOCountersInfo[] + memoryInfo: MemoryInfo + } + + interface Mountpoint{ + device:string + mountpoint:string + fstype:string + } +} \ No newline at end of file diff --git a/src/utils/cmn/index.ts b/src/utils/cmn/index.ts index 2c84b4a..bd6d259 100644 --- a/src/utils/cmn/index.ts +++ b/src/utils/cmn/index.ts @@ -221,3 +221,11 @@ export async function copyToClipboard(text: string): Promise<boolean> { } } } + +export function bytesToSize(bytes: number) { + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'] + if (bytes === 0) + return '0B' + const i = parseInt(String(Math.floor(Math.log(bytes) / Math.log(1024)))) + return `${(bytes / 1024 ** i).toFixed(1)} ${sizes[i]}` +} diff --git a/src/utils/defaultData/index.ts b/src/utils/defaultData/index.ts new file mode 100644 index 0000000..b238f0d --- /dev/null +++ b/src/utils/defaultData/index.ts @@ -0,0 +1,17 @@ +import type { Language } from '@/store/modules/app/helper' + +export const defautSwatchesBackground = [ + '#00000000', + '#000000', + '#ffffff', + '#18A058', + '#2080F0', + '#F0A020', + 'rgba(208, 48, 80, 1)', + '#C418D1FF', +] + +export const languageOptions: { label: string; key: Language; value: Language }[] = [ + { label: 'English', key: 'en-US', value: 'en-US' }, + { label: '简体中文', key: 'zh-CN', value: 'zh-CN' }, +] diff --git a/src/utils/request/index.ts b/src/utils/request/index.ts index 569ea0b..43f2fc7 100644 --- a/src/utils/request/index.ts +++ b/src/utils/request/index.ts @@ -1,11 +1,12 @@ import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios' import { createDiscreteApi } from 'naive-ui' import request from './axios' +import { t } from '@/locales' import { useAuthStore } from '@/store' import { router } from '@/router' const { message } = createDiscreteApi(['message']) - +let loginMessageShow = false export interface HttpOption { url: string data?: any @@ -34,7 +35,17 @@ function http<T = any>( return res.data if (res.data.code === 1001) { - message.warning('登录过期,请重新登录') + // 避免重复弹窗 + if (loginMessageShow === false) { + loginMessageShow = true + message.warning(t('api.loginExpires'), { + // message.warning('登录过期', { + onLeave() { + loginMessageShow = false + }, + }) + } + router.push({ path: '/login' }) authStore.removeToken() return res.data diff --git a/src/views/home/applist/index.vue b/src/views/exception/test/zujian.vue similarity index 100% rename from src/views/home/applist/index.vue rename to src/views/exception/test/zujian.vue diff --git a/src/views/home/components/AppStarter/index.vue b/src/views/home/components/AppStarter/index.vue index 69a3aec..77a6a94 100644 --- a/src/views/home/components/AppStarter/index.vue +++ b/src/views/home/components/AppStarter/index.vue @@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' import { NLayout, NLayoutContent, NLayoutSider, NSpace } from 'naive-ui' import { useAuthStore } from '@/store' import { AppLoader, RoundCardModal, SvgIcon } from '@/components/common' +import { t } from '@/locales' interface App { name: string @@ -23,38 +24,38 @@ const componentName = ref('UserInfo') const collapsed = ref(false) const screenWidth = ref(0) const isSmallScreen = ref(false) -const defaultTitle = '系统应用 & 设置' +const defaultTitle = t('appLauncher.title') const title = ref('') const height = ref('500px') const apps = ref<App[]>([ { - name: '用户信息', + name: t('apps.userInfo.appName'), componentName: 'UserInfo', icon: 'material-symbols-person-edit-outline-rounded', }, { - name: '基本设置', + name: t('apps.baseSettings.appName'), componentName: 'Style', icon: 'ep-setting', }, { - name: '分组管理', + name: t('apps.itemGroupManage.appName'), componentName: 'ItemGroupManage', icon: 'material-symbols-ad-group-outline-rounded', }, { - name: '导入导出', + name: t('apps.exportImport.appName'), componentName: 'ImportExport', icon: 'icon-park-outline-import-and-export', }, { - name: '上传文件管理', + name: t('apps.uploadsFileManager.appName'), componentName: 'UploadFileManager', icon: 'tabler:file-upload', }, { - name: '关于', + name: t('apps.about.appName'), componentName: 'About', icon: 'lucide-info', }, @@ -93,7 +94,7 @@ function handleResize() { onMounted(() => { const adminApp: App = { - name: '用户管理', + name: t('adminSettingUsers.appName'), componentName: 'Users', icon: 'lucide-users', auth: 1, @@ -116,7 +117,6 @@ onUnmounted(() => { <RoundCardModal v-model:show="show" style="max-width: 900px;" - title="应用列表" size="small" > <template #header> diff --git a/src/views/home/components/EditItem/IconEditor.vue b/src/views/home/components/EditItem/IconEditor.vue index 8c7b564..be24f8a 100644 --- a/src/views/home/components/EditItem/IconEditor.vue +++ b/src/views/home/components/EditItem/IconEditor.vue @@ -4,6 +4,7 @@ import type { UploadFileInfo } from 'naive-ui' import { computed, defineProps } from 'vue' import { ItemIcon } from '@/components/common' import { useAuthStore } from '@/store' +import { t } from '@/locales' const props = defineProps<{ itemIcon: Panel.ItemIcon | null @@ -74,7 +75,7 @@ const handleUploadFinish = ({ emit('update:itemIcon', itemIconInfo.value || null) } else { - ms.error(`上传错误:${res.msg}`) + ms.error(`${t('common.uploadFail')}:${res.msg}`) } return file @@ -90,7 +91,7 @@ const handleUploadFinish = ({ name="iconType" @change="handleIconTypeRadioChange(1)" > - 文字 + {{ $t('common.text') }} </NRadio> <NRadio @@ -99,7 +100,7 @@ const handleUploadFinish = ({ name="iconType" @change="handleIconTypeRadioChange(2)" > - 图片/SVG + {{ $t('common.image') }} </NRadio> <NRadio @@ -108,7 +109,7 @@ const handleUploadFinish = ({ name="iconType" @change="handleIconTypeRadioChange(3)" > - 在线图标 + {{ $t('iconItem.onlineIcon') }} </NRadio> </div> @@ -123,22 +124,22 @@ const handleUploadFinish = ({ <div class="ml-[20px]"> <!-- <NImage :src="model.icon" preview-disabled /> --> <div v-if="itemIconInfo.itemType === 1"> - <NInput v-model:value="itemIconInfo.text" class="mb-[5px]" size="small" type="text" placeholder="请输入文字作为图标" @input="handleChange" /> + <NInput v-model:value="itemIconInfo.text" class="mb-[5px]" size="small" type="text" @input="handleChange" /> </div> <div v-if="itemIconInfo.itemType === 3"> <div> - <NInput v-model:value="itemIconInfo.text" class="mb-[5px]" size="small" type="text" placeholder="请输入图标名字" @input="handleChange" /> + <NInput v-model:value="itemIconInfo.text" class="mb-[5px]" size="small" type="text" :placeholder="$t('iconItem.inputIconName')" @input="handleChange" /> <NButton quaternary type="info"> - <a target="_blank" href="https://icon-sets.iconify.design/">在线图标库</a> + <a target="_blank" href="https://icon-sets.iconify.design/">{{ $t('iconItem.onlineIconLibrary') }}</a> </NButton> </div> </div> <!-- 图片 --> <div v-if="itemIconInfo.itemType === 2"> - <NInput v-model:value="itemIconInfo.src" class="mb-[5px] w-full" size="small" type="text" placeholder="输入图标地址或上传" @input="handleChange" /> + <NInput v-model:value="itemIconInfo.src" class="mb-[5px] w-full" size="small" type="text" :placeholder="$t('iconItem.inputIconUrlOrUpload')" @input="handleChange" /> <NUpload action="/api/file/uploadImg" :show-file-list="false" @@ -149,7 +150,7 @@ const handleUploadFinish = ({ @finish="handleUploadFinish" > <NButton size="small"> - 本地上传 + {{ $t('iconItem.selectUpload') }} </NButton> </NUpload> </div> @@ -158,7 +159,7 @@ const handleUploadFinish = ({ <div class="flex items-center mt-[10px]"> <div class="w-auto text-slate-500 mr-[10px]"> - 背景色: + {{ $t('common.backgroundColor') }} </div> <div class="w-[150px] flex items-center mr-[10px]"> <NColorPicker @@ -172,7 +173,7 @@ const handleUploadFinish = ({ </div> <div v-if="itemIconInfo.backgroundColor !== initData.backgroundColor" class="w-auto text-slate-500 mr-[10px] cursor-pointer"> <NButton quaternary type="info" @click="handleResetBackgroundColor"> - 恢复默认 + {{ $t('common.reset') }} </NButton> </div> </div> diff --git a/src/views/home/components/EditItem/index.vue b/src/views/home/components/EditItem/index.vue index 6b962d9..639d6fd 100644 --- a/src/views/home/components/EditItem/index.vue +++ b/src/views/home/components/EditItem/index.vue @@ -5,6 +5,7 @@ import { NButton, NForm, NFormItem, NGrid, NGridItem, NInput, NInputGroup, NModa import IconEditor from './IconEditor.vue' import { edit, getSiteFavicon } from '@/api/panel/itemIcon' import { getList as getGroupList } from '@/api/panel/itemIconGroup' +import { t } from '@/locales' interface Props { visible: boolean @@ -43,33 +44,33 @@ const rules: FormRules = { title: { required: true, trigger: 'blur', - message: '必填项', + message: t('form.required'), }, url: { required: true, trigger: 'blur', type: 'string', - message: '必填项', + message: t('form.required'), }, // itemIconGroupId: { // required: true, // trigger: ['blur', 'change'], - // message: '必填项', + // message: t('form.required'), // }, } const options = [ { default: true, - label: '当前页面打开', + label: t('iconItem.currentPageOpen'), value: 1, }, { - label: '新窗口打开', + label: t('iconItem.newWindowOpen'), value: 2, }, { - label: '当前页面弹窗打开', + label: t('iconItem.currentPageLayerOpen'), value: 3, }, ] @@ -89,16 +90,15 @@ async function editApi() { if (code === 0) { show.value = false model.value = { ...restoreDefault } - console.log('重置完成', model.value) emit('done', data) } else { - ms.error(`保存失败:${msg}`) + ms.error(`${t('common.saveFail')}:${msg}`) } } catch (error) { - ms.error('保存失败') + ms.error(t('common.saveFail')) } submitLoading.value = false } @@ -122,11 +122,11 @@ async function getIconByUrl(url: string, loadingIndex: number) { } } else { - ms.error('图标获取失败') + ms.error(t('iconItem.geticonFail')) } } catch (error) { - ms.error('图标获取失败') + ms.error(t('iconItem.geticonFail')) } getIconLoading.value[loadingIndex] = false } @@ -160,53 +160,53 @@ function getGroupListOptions() { } } else { - ms.error(`分组信息获取失败:${msg}`) + ms.error(`${t('iconItem.getGroupFail')}:${msg}`) } }) } </script> <template> - <NModal v-model:show="show" preset="card" size="small" style="width: 600px;border-radius: 1rem;" :title="itemInfo ? '修改项目' : '添加项目'"> + <NModal v-model:show="show" preset="card" size="small" style="width: 600px;border-radius: 1rem;" :title="itemInfo ? t('iconItem.edit') : t('iconItem.add')"> <div class="h-[600px] overflow-auto p-[5px]"> <NForm ref="formRef" :model="model" :rules="rules"> <NGrid cols="2" :x-gap="10" item-responsive> <NGridItem span="2 500:1"> - <NFormItem path="itemIconGroupId" label="分组"> + <NFormItem path="itemIconGroupId" :label="t('iconItem.iconGroup')"> <NSelect v-model:value="model.itemIconGroupId" :options="itemIconGroupOptions" /> </NFormItem> </NGridItem> <NGridItem span="2 500:1"> - <NFormItem path="title" label="标题"> - <NInput v-model:value="model.title" type="text" show-count :maxlength="20" placeholder="请输入标题" /> + <NFormItem path="title" :label="$t('common.title')"> + <NInput v-model:value="model.title" type="text" show-count :maxlength="20" /> </NFormItem> </NGridItem> </NGrid> - <NFormItem path="icon" label="图标"> + <NFormItem path="icon" :label="$t('common.icon')"> <IconEditor v-model:item-icon="model.icon" /> </NFormItem> - <NFormItem path="url" label="跳转地址"> + <NFormItem path="url" :label="$t('iconItem.url')"> <!-- <NSelect :style="{ width: '100px' }" :options="urlProtocolOptions" /> --> <NInputGroup> <NInput v-model:value="model.url" type="text" :maxlength="1000" placeholder="http(s)://" /> <NButton :disabled="!model.url" :loading="getIconLoading[0]" @click="getIconByUrl(model.url, 0)"> - 获取图标 + {{ $t('iconItem.getIcon') }} </NButton> </NInputGroup> </NFormItem> - <NFormItem path="lanUrl" label="局域网跳转地址"> + <NFormItem path="lanUrl" :label="$t('iconItem.lanUrl')"> <NInputGroup> - <NInput v-model:value="model.lanUrl" type="text" :maxlength="1000" placeholder="http(s)://(可以留空,切换到局域网模式,点击会使用该地址)" /> + <NInput v-model:value="model.lanUrl" type="text" :maxlength="1000" :placeholder="$t('iconItem.lanUrlInputPlaceholder')" /> <NButton :disabled="!model.lanUrl" :loading="getIconLoading[1]" @click="getIconByUrl(model.lanUrl || '', 1)"> - 获取图标 + {{ $t('iconItem.getIcon') }} </NButton> </NInputGroup> </NFormItem> - <NFormItem path="description" label="描述信息"> - <NInput v-model:value="model.description" type="text" show-count :maxlength="100" placeholder="请填写描述信息" /> + <NFormItem path="description" :label="$t('common.description')"> + <NInput v-model:value="model.description" type="text" show-count :maxlength="100" /> </NFormItem> - <NFormItem path="openMethod" label="打开方式"> + <NFormItem path="openMethod" :label="$t('iconItem.openMethod')"> <NSelect v-model:value="model.openMethod" :options="options" /> </NFormItem> </NForm> @@ -214,7 +214,7 @@ function getGroupListOptions() { <template #footer> <NButton type="success" :loading="submitLoading" style="float: right;" @click="handleValidateButtonClick"> - 确定 + {{ $t('common.save') }} </NButton> </template> </NModal> diff --git a/src/views/home/index.vue b/src/views/home/index.vue index 465e2be..a945d36 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -3,7 +3,7 @@ import { VueDraggable } from 'vue-draggable-plus' import { NBackTop, NButton, NButtonGroup, NDropdown, NModal, NSkeleton, NSpin, useDialog, useMessage } from 'naive-ui' import { nextTick, onMounted, ref } from 'vue' import { AppIcon, AppStarter, EditItem } from './components' -import { Clock, SearchBox } from '@/components/deskModule' +import { Clock, SearchBox, SystemMonitor } from '@/components/deskModule' import { SvgIcon } from '@/components/common' import { deletes, getListByGroupId, saveSort } from '@/api/panel/itemIcon' import { getList as getGroupList } from '@/api/panel/itemIconGroup' @@ -13,6 +13,7 @@ import { useAuthStore, usePanelState } from '@/store' import { PanelPanelConfigStyleEnum, PanelStateNetworkModeEnum } from '@/enums' import { VisitMode } from '@/enums/auth' import { router } from '@/router' +import { t } from '@/locales' interface ItemGroup extends Panel.ItemIconGroup { sortStatus?: boolean @@ -134,18 +135,18 @@ function handleRightMenuSelect(key: string | number) { break case 'delete': dialog.warning({ - title: '警告', - content: `你确定要删除图标 ${currentRightSelectItem.value?.title} ?`, - positiveText: '确定', - negativeText: '取消', + title: t('common.warning'), + content: t('common.deleteConfirmByName', { name: currentRightSelectItem.value?.title }), + positiveText: t('common.confirm'), + negativeText: t('common.cancel'), onPositiveClick: () => { deletes([currentRightSelectItem.value?.id as number]).then(({ code, msg }) => { if (code === 0) { - ms.success('已删除') + ms.success(t('common.deleteSuccess')) getList() } else { - ms.error(`删除失败:${msg}`) + ms.error(`${t('common.deleteFail')}:${msg}`) } }) }, @@ -183,10 +184,10 @@ function handleEditSuccess(item: Panel.ItemInfo) { function handleChangeNetwork(mode: PanelStateNetworkModeEnum) { panelState.setNetworkMode(mode) if (mode === PanelStateNetworkModeEnum.lan) - ms.success('已经切换成局域网模式(此配置仅保存在本地)') + ms.success(t('panelHome.changeToLanModelSuccess')) else - ms.success('已经切换成互联网模式(此配置仅保存在本地)') + ms.success(t('panelHome.changeToWanModelSuccess')) } // 结束拖拽 @@ -208,11 +209,11 @@ function handleSaveSort(itemGroup: ItemGroup) { saveSort({ itemIconGroupId: itemGroup.id as number, sortItems: saveItems }).then(({ code, msg }) => { if (code === 0) { - ms.success('保存成功') + ms.success(t('common.saveSuccess')) itemGroup.sortStatus = false } else { - ms.error(`保存失败:${msg}`) + ms.error(`${t('common.saveFail')}:${msg}`) } }) } @@ -221,7 +222,7 @@ function handleSaveSort(itemGroup: ItemGroup) { function getDropdownMenuOptions() { const dropdownMenuOptions = [ { - label: '新窗口打开', + label: t('iconItem.newWindowOpen'), key: 'newWindows', }, @@ -229,24 +230,24 @@ function getDropdownMenuOptions() { if (currentRightSelectItem.value?.lanUrl && panelState.networkMode === PanelStateNetworkModeEnum.wan) { dropdownMenuOptions.push({ - label: '打开局域网地址', + label: t('panelHome.openLanUrl'), key: 'openLanUrl', }) } if (currentRightSelectItem.value?.lanUrl && panelState.networkMode === PanelStateNetworkModeEnum.lan) { dropdownMenuOptions.push({ - label: '打开互联网地址', + label: t('panelHome.openWanUrl'), key: 'openWanUrl', }) } if (authStore.visitMode === VisitMode.VISIT_MODE_LOGIN) { dropdownMenuOptions.push({ - label: '编辑', + label: t('common.edit'), key: 'edit', }, { - label: '删除', + label: t('common.delete'), key: 'delete', }) } @@ -362,7 +363,20 @@ function handleAddItem(itemIconGroupId?: number) { </div> <!-- 应用盒子 --> - <div class="mt-[50px]" :style="{ marginLeft: `${panelState.panelConfig.marginX}px`, marginRight: `${panelState.panelConfig.marginX}px` }"> + <div :style="{ marginLeft: `${panelState.panelConfig.marginX}px`, marginRight: `${panelState.panelConfig.marginX}px` }"> + <!-- 系统监控状态 --> + <div + v-if="panelState.panelConfig.systemMonitorShow + && ((panelState.panelConfig.systemMonitorPublicVisitModeShow && authStore.visitMode === VisitMode.VISIT_MODE_PUBLIC) + || authStore.visitMode === VisitMode.VISIT_MODE_LOGIN)" + class="flex mx-auto" + > + <SystemMonitor + :allow-edit="authStore.visitMode === VisitMode.VISIT_MODE_LOGIN" + :show-title="panelState.panelConfig.systemMonitorShowTitle" + /> + </div> + <!-- 组纵向排列 --> <div v-for="(itemGroup, itemGroupIndex) in filterItems" :key="itemGroupIndex" @@ -381,10 +395,10 @@ function handleAddItem(itemIconGroupId?: number) { class="ml-2 delay-100 transition-opacity flex" :class="itemGroup.hoverStatus ? 'opacity-100' : 'opacity-0'" > - <span class="mr-2 cursor-pointer" title="添加快捷图标" @click="handleAddItem(itemGroup.id)"> + <span class="mr-2 cursor-pointer" :title="t('common.add')" @click="handleAddItem(itemGroup.id)"> <SvgIcon class="text-white font-xl" icon="typcn:plus" /> </span> - <span class="mr-2 cursor-pointer " title="排序组快捷图标" @click="handleSetSortStatus(itemGroupIndex, !itemGroup.sortStatus)"> + <span class="mr-2 cursor-pointer " :title="t('common.sort')" @click="handleSetSortStatus(itemGroupIndex, !itemGroup.sortStatus)"> <SvgIcon class="text-white font-xl" icon="ri:drag-drop-line" /> </span> </div> @@ -414,7 +428,7 @@ function handleAddItem(itemIconGroupId?: number) { <div v-if="itemGroup.items.length === 0" class="not-drag"> <AppIcon :class="itemGroup.sortStatus ? 'cursor-move' : 'cursor-pointer'" - :item-info="{ icon: { itemType: 3, text: 'subway:add' }, title: '添加图标', url: '', openMethod: 0 }" + :item-info="{ icon: { itemType: 3, text: 'subway:add' }, title: t('common.add'), url: '', openMethod: 0 }" :icon-text-color="panelState.panelConfig.iconTextColor" :icon-text-info-hide-description="panelState.panelConfig.iconTextInfoHideDescription || false" :icon-text-icon-hide-title="panelState.panelConfig.iconTextIconHideTitle || false" @@ -430,7 +444,7 @@ function handleAddItem(itemIconGroupId?: number) { <div v-if="panelState.panelConfig.iconStyle === PanelPanelConfigStyleEnum.icon"> <div v-if="itemGroup.items"> <VueDraggable - v-model="itemGroup.items" item-key="id" :animation="300" + v-model="itemGroup.items" item-key="sort" :animation="300" class="icon-small-box" filter=".not-drag" @@ -451,7 +465,7 @@ function handleAddItem(itemIconGroupId?: number) { <div v-if="itemGroup.items.length === 0" class="not-drag"> <AppIcon class="cursor-pointer" - :item-info="{ icon: { itemType: 3, text: 'subway:add' }, title: '添加图标', url: '', openMethod: 0 }" + :item-info="{ icon: { itemType: 3, text: 'subway:add' }, title: $t('common.add'), url: '', openMethod: 0 }" :icon-text-color="panelState.panelConfig.iconTextColor" :icon-text-info-hide-description="!panelState.panelConfig.iconTextInfoHideDescription" :icon-text-icon-hide-title="panelState.panelConfig.iconTextIconHideTitle || false" @@ -471,13 +485,14 @@ function handleAddItem(itemIconGroupId?: number) { <SvgIcon class="text-white font-xl" icon="material-symbols:save" /> </template> <div> - 保存排序 + {{ $t('common.saveSort') }} </div> </NButton> </div> </div> </div> </div> + <div class="mt-5 footer" v-html="panelState.panelConfig.footerHtml" /> </div> </div> @@ -492,7 +507,7 @@ function handleAddItem(itemIconGroupId?: number) { <NButtonGroup vertical> <NButton v-if="panelState.networkMode === PanelStateNetworkModeEnum.lan" color="#2a2a2a6b" - title="当前:局域网模式,点击切换成互联网模式" @click="handleChangeNetwork(PanelStateNetworkModeEnum.wan)" + :title="t('panelHome.changeToWanModel')" @click="handleChangeNetwork(PanelStateNetworkModeEnum.wan)" > <template #icon> <SvgIcon class="text-white font-xl" icon="material-symbols:lan-outline-rounded" /> @@ -501,7 +516,7 @@ function handleAddItem(itemIconGroupId?: number) { <NButton v-if="panelState.networkMode === PanelStateNetworkModeEnum.wan" color="#2a2a2a6b" - title="当前:互联网模式,点击切换成局域网模式" @click="handleChangeNetwork(PanelStateNetworkModeEnum.lan)" + :title="t('panelHome.changeToLanModel')" @click="handleChangeNetwork(PanelStateNetworkModeEnum.lan)" > <template #icon> <SvgIcon class="text-white font-xl" icon="mdi:wan" /> @@ -514,7 +529,7 @@ function handleAddItem(itemIconGroupId?: number) { </template> </NButton> - <NButton v-if="authStore.visitMode === VisitMode.VISIT_MODE_PUBLIC" color="#2a2a2a6b" title="登录" @click="router.push('/login')"> + <NButton v-if="authStore.visitMode === VisitMode.VISIT_MODE_PUBLIC" color="#2a2a2a6b" :title="$t('panelHome.goToLogin')" @click="router.push('/login')"> <template #icon> <SvgIcon class="text-white font-xl" icon="material-symbols:account-circle" /> </template> diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 10d2e9a..9e71269 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -1,16 +1,21 @@ <script setup lang="ts"> -import { NButton, NCard, NForm, NFormItem, NGradientText, NInput, useMessage } from 'naive-ui' +import { NButton, NCard, NForm, NFormItem, NGradientText, NInput, NSelect, useMessage } from 'naive-ui' import { ref } from 'vue' import { login } from '@/api' -import { useAuthStore } from '@/store' +import { useAppStore, useAuthStore } from '@/store' import { SvgIcon } from '@/components/common' import { router } from '@/router' import { t } from '@/locales' +import { languageOptions } from '@/utils/defaultData' +import type { Language } from '@/store/modules/app/helper' // const userStore = useUserStore() const authStore = useAuthStore() +const appStore = useAppStore() const ms = useMessage() const loading = ref(false) +const languageValue = ref<Language>(appStore.language) + // const isShowCaptcha = ref<boolean>(false) // const isShowRegister = ref<boolean>(false) @@ -45,11 +50,25 @@ function handleSubmit() { // 点击登录按钮触发 loginPost() } + +function handleChangeLanuage(value: Language) { + languageValue.value = value + appStore.setLanguage(value) +} </script> <template> <div class="login-container"> <NCard class="login-card" style="border-radius: 20px;"> + <div class="mb-5 flex items-center justify-end"> + <div class="mr-2"> + <SvgIcon icon="ion-language" style="width: 20;height: 20;" /> + </div> + <div class="min-w-[100px]"> + <NSelect v-model:value="languageValue" size="small" :options="languageOptions" @update-value="handleChangeLanuage" /> + </div> + </div> + <div class="login-title "> <NGradientText :size="30" type="success" class="!font-bold"> {{ $t('common.appName') }} diff --git a/sun-panel.code-workspace b/sun-panel.code-workspace new file mode 100644 index 0000000..5e1abd6 --- /dev/null +++ b/sun-panel.code-workspace @@ -0,0 +1,84 @@ +{ + "folders": [ + { + "name": "frontend", + "path": "src" + }, + { + "name": "backend", + "path": "service" + }, + { + "name": "root", + "path": ".", + } + ], + "settings": { + "prettier.enable": false, + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + "html", + "json", + "jsonc", + "json5", + "yaml", + "yml", + "markdown" + ], + "cSpell.words": [ + "axios", + "bumpp", + "commitlint", + "davinci", + "dockerhub", + "esno", + "highlightjs", + "hljs", + "iconify", + "katex", + "katexmath", + "linkify", + "logprobs", + "mdhljs", + "mila", + "nodata", + "pinia", + "Popconfirm", + "rushstack", + "Sider", + "tailwindcss", + "traptitech", + "tsup", + "Typecheck", + "unplugin", + "VITE", + "vueuse", + ], + "i18n-ally.sortKeys": true, + "i18n-ally.enabledParsers": ["ts", "js","json"], + "i18n-ally.enabledFrameworks": [ + "vue" + ], + "i18n-ally.dirStructure": "auto", + "i18n-ally.localesPaths": [ + "locales" + ], + "i18n-ally.sourceLanguage": "zh-CN", + "i18n-ally.keystyle": "nested", + }, + "extensions": { + "recommendations": [ + "Vue.volar", + "dbaeumer.vscode-eslint" + ] + }, + +} \ No newline at end of file From 7ce9191c81daeb6d7d58b7585d6ec76187900c81 Mon Sep 17 00:00:00 2001 From: Sun <95302870@qq.com> Date: Tue, 9 Jan 2024 14:21:49 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++++++++-- doc/donate.md | 9 +++++++-- doc/images/donate/paypal.png | Bin 0 -> 62685 bytes 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 doc/images/donate/paypal.png diff --git a/README.md b/README.md index d134c97..a9ff756 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,12 @@ > 开源开发不易,如果觉得我的项目有帮到你,欢迎给我[打赏](./doc/donate.md)或者请我喝个奶茶☕(如果可以备注下您的昵称或者名字),你的支持就是我的动力,谢谢。 +<a href="https://www.paypal.me/hslrs" target="_blank"> +<div style="height:50px;width:160px;background-image:url(./doc/images/donate/paypal.png);background-size:100% 100%;"></div> +<div>PayPal Click here</div> +</a> + + | | | | ------------ | ------------ | | <img height="300" src="./doc/images/donate/weixin.png"/> | <img height="300" src="./doc/images/donate/alipay.png" /> | @@ -54,10 +60,10 @@ - [x] 增加访客账号 - [x] 帐号解除邮箱限制 - [x] 对上传的文件管理(针对账户增强重复利用,节省空间) +- [x] 服务器监控 +- [x] 多国语言支持 - [ ] 用户自定义搜索框搜索引擎 - [ ] 搜索框样式自定义(背景颜色,文字颜色) -- [ ] 多国语言支持 -- [ ] 服务器监控 - [ ] docker管理器 - [ ] 计划任务 diff --git a/doc/donate.md b/doc/donate.md index d21c6a2..bf043df 100644 --- a/doc/donate.md +++ b/doc/donate.md @@ -1,5 +1,10 @@ -> 开源不易,如果觉得我的项目有帮到你,欢迎给我打赏或者请我喝个奶茶☕(如果可以备注下您的昵称或者名字),打赏不准超过你工资的一半。你的支持就是我的动力,谢谢。 +> Open source development is not easy. If you feel that my project has been helpful to you, please feel free to buy me a cup of coffee ☕ (If possible, leave your nickname, name, or email in the note). Your support is my motivation. Thank you. -| | | +<a href="https://www.paypal.me/hslrs" target="_blank"> +<div style="height:50px;width:160px;background-image:url(./images/donate/paypal.png);background-size:100% 100%;"></div> +<div>PayPal Click here</div> +</a> + +| WeChatPay | AliPay | | ------------ | ------------ | | <img style="max-height:400px;box-shadow: 0 0 10px rgba(0,0,0,0.5);border-radius: 25px;" src="./images/donate/weixin.png"></img> | <img style="max-height:400px;box-shadow: 0 0 10px rgba(0,0,0,0.5);border-radius: 25px;" src="./images/donate/alipay.png"></img> | \ No newline at end of file diff --git a/doc/images/donate/paypal.png b/doc/images/donate/paypal.png new file mode 100644 index 0000000000000000000000000000000000000000..3f4ac6df6534a003919ba4e0c3ceb2922a2ae6b4 GIT binary patch literal 62685 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 OA4rJE3fBti2mXIwvH3Fq literal 0 HcmV?d00001 From 64d37193d5761c6d02cc7bc7d5e625e2183ceaae Mon Sep 17 00:00:00 2001 From: Sun <95302870@qq.com> Date: Tue, 9 Jan 2024 14:36:29 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++--- doc/donate.md | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a9ff756..ddb17f3 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,8 @@ > 开源开发不易,如果觉得我的项目有帮到你,欢迎给我[打赏](./doc/donate.md)或者请我喝个奶茶☕(如果可以备注下您的昵称或者名字),你的支持就是我的动力,谢谢。 -<a href="https://www.paypal.me/hslrs" target="_blank"> -<div style="height:50px;width:160px;background-image:url(./doc/images/donate/paypal.png);background-size:100% 100%;"></div> -<div>PayPal Click here</div> +<a href="https://www.paypal.me/hslrs"> +<img height="60" src="./doc/images/donate/paypal.png" target="_blank"></img> </a> diff --git a/doc/donate.md b/doc/donate.md index bf043df..8f71efe 100644 --- a/doc/donate.md +++ b/doc/donate.md @@ -1,8 +1,7 @@ > Open source development is not easy. If you feel that my project has been helpful to you, please feel free to buy me a cup of coffee ☕ (If possible, leave your nickname, name, or email in the note). Your support is my motivation. Thank you. -<a href="https://www.paypal.me/hslrs" target="_blank"> -<div style="height:50px;width:160px;background-image:url(./images/donate/paypal.png);background-size:100% 100%;"></div> -<div>PayPal Click here</div> +<a href="https://www.paypal.me/hslrs"> +<img height="60" src="./images/donate/paypal.png" target="_blank"></img> </a> | WeChatPay | AliPay | From d6ccd53fd38be135eca846e34bc6cce0a7790f8e Mon Sep 17 00:00:00 2001 From: Sun <95302870@qq.com> Date: Tue, 9 Jan 2024 17:43:36 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=84=9F=E8=B0=A2?= =?UTF-8?q?=E5=90=8D=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ddb17f3..f444759 100644 --- a/README.md +++ b/README.md @@ -155,9 +155,9 @@ hslr/sun-panel ## ❤️ 感谢 -- [Roc](https://github.com/RocCheng)提供自动构建多平台docker镜像[方案](https://github.com/hslr-s/sun-panel/issues/9#issuecomment-1817433439) -- [jackloves111](https://github.com/jackloves111)帮助构建基础文档 - +- [Roc](https://github.com/RocCheng) +- [jackloves111](https://github.com/jackloves111) +- [Rock.L](https://github.com/gitlyp) ## LICENSE [MIT](./LICENSE) \ No newline at end of file