diff --git a/internal/app/global.go b/internal/app/global.go index 2377a647..daeda103 100644 --- a/internal/app/global.go +++ b/internal/app/global.go @@ -25,10 +25,20 @@ var ( Logger *zap.Logger ) +// 定义面板状态常量 +const ( + StatusNormal = iota + StatusMaintain + StatusClosed + StatusUpgrade + StatusFailed +) + // 面板全局变量 var ( Root string Version string Locale string IsCli bool + Status = StatusNormal ) diff --git a/internal/biz/monitor.go b/internal/biz/monitor.go index dff495c6..336b06bb 100644 --- a/internal/biz/monitor.go +++ b/internal/biz/monitor.go @@ -4,14 +4,14 @@ import ( "time" "github.com/TheTNB/panel/internal/http/request" - "github.com/TheTNB/panel/pkg/tools" + "github.com/TheTNB/panel/pkg/types" ) type Monitor struct { - ID uint `gorm:"primaryKey" json:"id"` - Info tools.MonitoringInfo `gorm:"not null;serializer:json" json:"info"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primaryKey" json:"id"` + Info types.CurrentInfo `gorm:"not null;serializer:json" json:"info"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type MonitorRepo interface { diff --git a/internal/http/middleware/status.go b/internal/http/middleware/status.go index 8d98c39f..f366f5a8 100644 --- a/internal/http/middleware/status.go +++ b/internal/http/middleware/status.go @@ -3,37 +3,36 @@ package middleware import ( "net/http" + "github.com/TheTNB/panel/internal/app" "github.com/go-rat/chix" - - "github.com/TheTNB/panel/pkg/types" ) // Status 检查程序状态 func Status(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch types.Status { - case types.StatusUpgrade: + switch app.Status { + case app.StatusUpgrade: render := chix.NewRender(w) render.Status(http.StatusServiceUnavailable) render.JSON(chix.M{ "message": "面板升级中,请稍后刷新", }) return - case types.StatusMaintain: + case app.StatusMaintain: render := chix.NewRender(w) render.Status(http.StatusServiceUnavailable) render.JSON(chix.M{ "message": "面板正在运行维护任务,请稍后刷新", }) return - case types.StatusClosed: + case app.StatusClosed: render := chix.NewRender(w) render.Status(http.StatusForbidden) render.JSON(chix.M{ "message": "面板已关闭", }) return - case types.StatusFailed: + case app.StatusFailed: render := chix.NewRender(w) render.Status(http.StatusInternalServerError) render.JSON(chix.M{ diff --git a/internal/http/request/dashboard.go b/internal/http/request/dashboard.go new file mode 100644 index 00000000..62e62230 --- /dev/null +++ b/internal/http/request/dashboard.go @@ -0,0 +1,6 @@ +package request + +type DashboardCurrent struct { + Nets []string `json:"nets" form:"nets"` + Disks []string `json:"disks" form:"disks"` +} diff --git a/internal/job/cert_renew.go b/internal/job/cert_renew.go index f57f7979..18433664 100644 --- a/internal/job/cert_renew.go +++ b/internal/job/cert_renew.go @@ -9,7 +9,6 @@ import ( "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/data" pkgcert "github.com/TheTNB/panel/pkg/cert" - "github.com/TheTNB/panel/pkg/types" ) // CertRenew 证书续签 @@ -24,7 +23,7 @@ func NewCertRenew() *CertRenew { } func (r *CertRenew) Run() { - if types.Status != types.StatusNormal { + if app.Status != app.StatusNormal { return } diff --git a/internal/job/monitoring.go b/internal/job/monitoring.go index 09d4830d..ca3c0ba1 100644 --- a/internal/job/monitoring.go +++ b/internal/job/monitoring.go @@ -10,7 +10,6 @@ import ( "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/data" "github.com/TheTNB/panel/pkg/tools" - "github.com/TheTNB/panel/pkg/types" ) // Monitoring 系统监控 @@ -25,7 +24,7 @@ func NewMonitoring() *Monitoring { } func (r *Monitoring) Run() { - if types.Status != types.StatusNormal { + if app.Status != app.StatusNormal { return } @@ -38,13 +37,13 @@ func (r *Monitoring) Run() { return } - info := tools.GetMonitoringInfo() + info := tools.CurrentInfo(nil, nil) // 去除部分数据以减少数据库存储 info.Disk = nil info.Cpus = nil - if types.Status != types.StatusNormal { + if app.Status != app.StatusNormal { return } @@ -59,7 +58,7 @@ func (r *Monitoring) Run() { return } day := cast.ToInt(dayStr) - if day <= 0 || types.Status != types.StatusNormal { + if day <= 0 || app.Status != app.StatusNormal { return } if err = app.Orm.Where("created_at < ?", time.Now().AddDate(0, 0, -day).Format("2006-01-02 15:04:05")).Delete(&biz.Monitor{}).Error; err != nil { diff --git a/internal/job/panel_task.go b/internal/job/panel_task.go index eda528ca..dbb4894d 100644 --- a/internal/job/panel_task.go +++ b/internal/job/panel_task.go @@ -9,7 +9,6 @@ import ( "github.com/TheTNB/panel/internal/app" "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/data" - "github.com/TheTNB/panel/pkg/types" ) // PanelTask 面板每日任务 @@ -28,15 +27,15 @@ func NewPanelTask() *PanelTask { } func (r *PanelTask) Run() { - types.Status = types.StatusMaintain + app.Status = app.StatusMaintain // 优化数据库 if err := app.Orm.Exec("VACUUM").Error; err != nil { - types.Status = types.StatusFailed + app.Status = app.StatusFailed app.Logger.Error("优化面板数据库失败", zap.Error(err)) } if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil { - types.Status = types.StatusFailed + app.Status = app.StatusFailed app.Logger.Error("优化面板数据库失败", zap.Error(err)) } @@ -64,5 +63,5 @@ func (r *PanelTask) Run() { runtime.GC() debug.FreeOSMemory() - types.Status = types.StatusNormal + app.Status = app.StatusNormal } diff --git a/internal/route/http.go b/internal/route/http.go index 0b6bdfa5..5dfcaca5 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -23,18 +23,18 @@ func Http(r chi.Router) { r.With(middleware.MustLogin).Get("/info", user.Info) }) - r.Route("/info", func(r chi.Router) { - info := service.NewInfoService() - r.Get("/panel", info.Panel) - r.With(middleware.MustLogin).Get("/homeApps", info.HomeApps) - r.With(middleware.MustLogin).Get("/realtime", info.Realtime) - r.With(middleware.MustLogin).Get("/systemInfo", info.SystemInfo) - r.With(middleware.MustLogin).Get("/countInfo", info.CountInfo) - r.With(middleware.MustLogin).Get("/installedDbAndPhp", info.InstalledDbAndPhp) - r.With(middleware.MustLogin).Get("/checkUpdate", info.CheckUpdate) - r.With(middleware.MustLogin).Get("/updateInfo", info.UpdateInfo) - r.With(middleware.MustLogin).Post("/update", info.Update) - r.With(middleware.MustLogin).Post("/restart", info.Restart) + r.Route("/dashboard", func(r chi.Router) { + dashboard := service.NewDashboardService() + r.Get("/panel", dashboard.Panel) + r.With(middleware.MustLogin).Get("/homeApps", dashboard.HomeApps) + r.With(middleware.MustLogin).Post("/current", dashboard.Current) + r.With(middleware.MustLogin).Get("/systemInfo", dashboard.SystemInfo) + r.With(middleware.MustLogin).Get("/countInfo", dashboard.CountInfo) + r.With(middleware.MustLogin).Get("/installedDbAndPhp", dashboard.InstalledDbAndPhp) + r.With(middleware.MustLogin).Get("/checkUpdate", dashboard.CheckUpdate) + r.With(middleware.MustLogin).Get("/updateInfo", dashboard.UpdateInfo) + r.With(middleware.MustLogin).Post("/update", dashboard.Update) + r.With(middleware.MustLogin).Post("/restart", dashboard.Restart) }) r.Route("/task", func(r chi.Router) { diff --git a/internal/service/info.go b/internal/service/dashboard.go similarity index 80% rename from internal/service/info.go rename to internal/service/dashboard.go index b48da222..2e6fecc2 100644 --- a/internal/service/info.go +++ b/internal/service/dashboard.go @@ -8,11 +8,13 @@ import ( "github.com/go-rat/chix" "github.com/hashicorp/go-version" + "github.com/shirou/gopsutil/host" "github.com/spf13/cast" "github.com/TheTNB/panel/internal/app" "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/api" "github.com/TheTNB/panel/pkg/db" "github.com/TheTNB/panel/pkg/shell" @@ -21,7 +23,7 @@ import ( "github.com/TheTNB/panel/pkg/types" ) -type InfoService struct { +type DashboardService struct { api *api.API taskRepo biz.TaskRepo websiteRepo biz.WebsiteRepo @@ -30,8 +32,8 @@ type InfoService struct { cronRepo biz.CronRepo } -func NewInfoService() *InfoService { - return &InfoService{ +func NewDashboardService() *DashboardService { + return &DashboardService{ api: api.NewAPI(app.Version), taskRepo: data.NewTaskRepo(), websiteRepo: data.NewWebsiteRepo(), @@ -41,7 +43,7 @@ func NewInfoService() *InfoService { } } -func (s *InfoService) Panel(w http.ResponseWriter, r *http.Request) { +func (s *DashboardService) Panel(w http.ResponseWriter, r *http.Request) { name, _ := s.settingRepo.Get(biz.SettingKeyName) if name == "" { name = "耗子面板" @@ -53,31 +55,46 @@ func (s *InfoService) Panel(w http.ResponseWriter, r *http.Request) { }) } -func (s *InfoService) HomeApps(w http.ResponseWriter, r *http.Request) { +func (s *DashboardService) HomeApps(w http.ResponseWriter, r *http.Request) { apps, err := s.appRepo.GetHomeShow() if err != nil { - Error(w, http.StatusInternalServerError, "获取首页应用失败") + Error(w, http.StatusInternalServerError, "获取首页应用失败: %v", err) return } Success(w, apps) } -func (s *InfoService) Realtime(w http.ResponseWriter, r *http.Request) { - Success(w, tools.GetMonitoringInfo()) +func (s *DashboardService) Current(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.DashboardCurrent](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + Success(w, tools.CurrentInfo(req.Nets, req.Disks)) } -func (s *InfoService) SystemInfo(w http.ResponseWriter, r *http.Request) { - monitorInfo := tools.GetMonitoringInfo() +func (s *DashboardService) SystemInfo(w http.ResponseWriter, r *http.Request) { + hostInfo, err := host.Info() + if err != nil { + Error(w, http.StatusInternalServerError, "获取系统信息失败") + return + } Success(w, chix.M{ - "os_name": monitorInfo.Host.Platform + " " + monitorInfo.Host.PlatformVersion, - "uptime": fmt.Sprintf("%.2f", float64(monitorInfo.Host.Uptime)/86400), - "panel_version": app.Version, + "procs": hostInfo.Procs, + "hostname": hostInfo.Hostname, + "kernel_arch": hostInfo.KernelArch, + "kernel_version": hostInfo.KernelVersion, + "os_name": hostInfo.Platform + " " + hostInfo.PlatformVersion, + "boot_time": hostInfo.BootTime, + "uptime": fmt.Sprintf("%.2f", float64(hostInfo.Uptime)/86400), + "panel_version": app.Version, }) } -func (s *InfoService) CountInfo(w http.ResponseWriter, r *http.Request) { +func (s *DashboardService) CountInfo(w http.ResponseWriter, r *http.Request) { websiteCount, err := s.websiteRepo.Count() if err != nil { Error(w, http.StatusInternalServerError, "获取网站数量失败") @@ -173,7 +190,7 @@ func (s *InfoService) CountInfo(w http.ResponseWriter, r *http.Request) { }) } -func (s *InfoService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Request) { +func (s *DashboardService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Request) { mysqlInstalled, _ := s.appRepo.IsInstalled("slug like ?", "mysql%") postgresqlInstalled, _ := s.appRepo.IsInstalled("slug like ?", "postgresql%") php, _ := s.appRepo.GetInstalledAll("slug like ?", "php%") @@ -206,7 +223,7 @@ func (s *InfoService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Request) }) } -func (s *InfoService) CheckUpdate(w http.ResponseWriter, r *http.Request) { +func (s *DashboardService) CheckUpdate(w http.ResponseWriter, r *http.Request) { if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline { Error(w, http.StatusForbidden, "离线模式下无法检查更新") return @@ -241,7 +258,7 @@ func (s *InfoService) CheckUpdate(w http.ResponseWriter, r *http.Request) { }) } -func (s *InfoService) UpdateInfo(w http.ResponseWriter, r *http.Request) { +func (s *DashboardService) UpdateInfo(w http.ResponseWriter, r *http.Request) { if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline { Error(w, http.StatusForbidden, "离线模式下无法检查更新") return @@ -278,7 +295,7 @@ func (s *InfoService) UpdateInfo(w http.ResponseWriter, r *http.Request) { Success(w, versions) } -func (s *InfoService) Update(w http.ResponseWriter, r *http.Request) { +func (s *DashboardService) Update(w http.ResponseWriter, r *http.Request) { if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline { Error(w, http.StatusForbidden, "离线模式下无法升级") return @@ -302,19 +319,19 @@ func (s *InfoService) Update(w http.ResponseWriter, r *http.Request) { } ver, url, checksum := panel.Version, download.URL, download.Checksum - types.Status = types.StatusUpgrade + app.Status = app.StatusUpgrade if err = s.settingRepo.UpdatePanel(ver, url, checksum); err != nil { - types.Status = types.StatusFailed + app.Status = app.StatusFailed Error(w, http.StatusInternalServerError, "%v", err) return } - types.Status = types.StatusNormal + app.Status = app.StatusNormal Success(w, nil) tools.RestartPanel() } -func (s *InfoService) Restart(w http.ResponseWriter, r *http.Request) { +func (s *DashboardService) Restart(w http.ResponseWriter, r *http.Request) { if s.taskRepo.HasRunningTask() { Error(w, http.StatusInternalServerError, "后台任务正在运行,禁止重启,请稍后再试") return diff --git a/internal/service/monitor.go b/internal/service/monitor.go index 3c4e1d36..c7ee3b0b 100644 --- a/internal/service/monitor.go +++ b/internal/service/monitor.go @@ -101,7 +101,7 @@ func (s *MonitorService) List(w http.ResponseWriter, r *http.Request) { list.Load.Load1 = append(list.Load.Load1, monitor.Info.Load.Load1) list.Load.Load5 = append(list.Load.Load5, monitor.Info.Load.Load5) list.Load.Load15 = append(list.Load.Load15, monitor.Info.Load.Load15) - list.CPU.Percent = append(list.CPU.Percent, fmt.Sprintf("%.2f", monitor.Info.Percent[0])) + list.CPU.Percent = append(list.CPU.Percent, fmt.Sprintf("%.2f", monitor.Info.Percent)) list.Mem.Available = append(list.Mem.Available, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Available)/1024/1024)) list.Mem.Used = append(list.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024)) list.SWAP.Used = append(list.SWAP.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024)) diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go index e862f00d..0104c186 100644 --- a/pkg/tools/tools.go +++ b/pkg/tools/tools.go @@ -3,6 +3,7 @@ package tools import ( "errors" + "slices" "strings" "time" @@ -15,50 +16,64 @@ import ( "github.com/shirou/gopsutil/net" "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/types" ) -// MonitoringInfo 监控信息 -type MonitoringInfo struct { - Cpus []cpu.InfoStat `json:"cpus"` - Percent []float64 `json:"percent"` - Load *load.AvgStat `json:"load"` - Host *host.InfoStat `json:"host"` - Mem *mem.VirtualMemoryStat `json:"mem"` - Swap *mem.SwapMemoryStat `json:"swap"` - Net []net.IOCountersStat `json:"net"` - DiskIO []disk.IOCountersStat `json:"disk_io"` - Disk []disk.PartitionStat `json:"disk"` - DiskUsage []disk.UsageStat `json:"disk_usage"` -} - -// GetMonitoringInfo 获取监控数据 -func GetMonitoringInfo() MonitoringInfo { - var res MonitoringInfo +// CurrentInfo 获取监控数据 +func CurrentInfo(nets, disks []string) types.CurrentInfo { + var res types.CurrentInfo res.Cpus, _ = cpu.Info() - res.Percent, _ = cpu.Percent(time.Second, false) + res.Percents, _ = cpu.Percent(100*time.Millisecond, true) + percent, _ := cpu.Percent(100*time.Millisecond, false) + if len(percent) > 0 { + res.Percent = percent[0] + } res.Load, _ = load.Avg() res.Host, _ = host.Info() res.Mem, _ = mem.VirtualMemory() res.Swap, _ = mem.SwapMemory() - res.Net, _ = net.IOCounters(true) res.Disk, _ = disk.Partitions(true) - ioCounters, _ := disk.IOCounters() + ioCounters, _ := disk.IOCounters(disks...) for _, info := range ioCounters { res.DiskIO = append(res.DiskIO, info) } + var excludes = []string{"/dev", "/boot", "/sys", "/dev", "/run", "/proc", "/usr", "/var", "/snap"} + excludes = append(excludes, "/mnt/cdrom") // CDROM + excludes = append(excludes, "/mnt/wsl") // Windows WSL for _, partition := range res.Disk { - if strings.HasPrefix(partition.Mountpoint, "/dev") || strings.HasPrefix(partition.Mountpoint, "/sys") || strings.HasPrefix(partition.Mountpoint, "/proc") || strings.HasPrefix(partition.Mountpoint, "/run") || strings.HasPrefix(partition.Mountpoint, "/boot") || strings.HasPrefix(partition.Mountpoint, "/usr") || strings.HasPrefix(partition.Mountpoint, "/var") { + for _, exclude := range excludes { + if strings.HasPrefix(partition.Mountpoint, exclude) { + continue + } + } + // 去除内存盘和overlay容器盘 + if slices.Contains([]string{"tmpfs", "overlay"}, partition.Fstype) { continue } usage, _ := disk.Usage(partition.Mountpoint) res.DiskUsage = append(res.DiskUsage, *usage) } + if len(nets) == 0 { + netInfo, _ := net.IOCounters(false) + res.Net = netInfo + } else { + var netStats []net.IOCountersStat + netInfo, _ := net.IOCounters(true) + for _, state := range netInfo { + if slices.Contains(nets, state.Name) { + netStats = append(netStats, state) + } + } + res.Net = netStats + } + return res } +// RestartPanel 重启面板 func RestartPanel() { _ = shell.ExecfAsync("sleep 1 && systemctl restart panel") } diff --git a/pkg/tools/tools_test.go b/pkg/tools/tools_test.go index 6d9fe30c..0082aa23 100644 --- a/pkg/tools/tools_test.go +++ b/pkg/tools/tools_test.go @@ -15,7 +15,7 @@ func TestHelperTestSuite(t *testing.T) { } func (s *HelperTestSuite) TestGetMonitoringInfo() { - s.NotNil(GetMonitoringInfo()) + s.NotNil(CurrentInfo(nil, nil)) } func (s *HelperTestSuite) TestGetPublicIP() { diff --git a/pkg/types/status.go b/pkg/types/status.go deleted file mode 100644 index 51e3e67c..00000000 --- a/pkg/types/status.go +++ /dev/null @@ -1,12 +0,0 @@ -package types - -// 定义面板状态常量 -const ( - StatusNormal = iota - StatusMaintain - StatusClosed - StatusUpgrade - StatusFailed -) - -var Status = StatusNormal diff --git a/pkg/types/system.go b/pkg/types/system.go new file mode 100644 index 00000000..b853b10c --- /dev/null +++ b/pkg/types/system.go @@ -0,0 +1,25 @@ +package types + +import ( + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/host" + "github.com/shirou/gopsutil/load" + "github.com/shirou/gopsutil/mem" + "github.com/shirou/gopsutil/net" +) + +// CurrentInfo 监控信息 +type CurrentInfo struct { + Cpus []cpu.InfoStat `json:"cpus"` + Percent float64 `json:"percent"` // 总使用率 + Percents []float64 `json:"percents"` // 每个核心使用率 + Load *load.AvgStat `json:"load"` + Host *host.InfoStat `json:"host"` + Mem *mem.VirtualMemoryStat `json:"mem"` + Swap *mem.SwapMemoryStat `json:"swap"` + Net []net.IOCountersStat `json:"net"` + DiskIO []disk.IOCountersStat `json:"disk_io"` + Disk []disk.PartitionStat `json:"disk"` + DiskUsage []disk.UsageStat `json:"disk_usage"` +} diff --git a/web/build/config/define.ts b/web/build/config/define.ts index 9f5d81b7..0a937442 100644 --- a/web/build/config/define.ts +++ b/web/build/config/define.ts @@ -1,12 +1,12 @@ -import dayjs from 'dayjs' +import { DateTime } from 'luxon' /** - * * 此处定义的是全局常量,启动或打包后将添加到window中 + * * 此处定义的是全局常量,启动或打包后将添加到 window 中 * https://vitejs.cn/config/#define */ // 项目构建时间 -const _BUILD_TIME_ = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss')) +const _BUILD_TIME_ = JSON.stringify(DateTime.now().toFormat('yyyy-MM-dd HH:mm:ss')) export const viteDefine = { _BUILD_TIME_ diff --git a/web/package.json b/web/package.json index 6d3b6950..87a93bf1 100644 --- a/web/package.json +++ b/web/package.json @@ -26,10 +26,10 @@ "@xterm/xterm": "^5.5.0", "axios": "^1.7.7", "crypto-js": "^4.2.0", - "dayjs": "^1.11.13", "echarts": "^5.5.1", "install": "^0.13.0", "lodash-es": "^4.17.21", + "luxon": "^3.5.0", "marked": "^14.1.2", "pinia": "^2.2.4", "remove": "^0.1.5", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 44c2a727..63ba506e 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: crypto-js: specifier: ^4.2.0 version: 4.2.0 - dayjs: - specifier: ^1.11.13 - version: 1.11.13 echarts: specifier: ^5.5.1 version: 5.5.1 @@ -38,6 +35,9 @@ importers: lodash-es: specifier: ^4.17.21 version: 4.17.21 + luxon: + specifier: ^3.5.0 + version: 3.5.0 marked: specifier: ^14.1.2 version: 14.1.3 @@ -761,30 +761,35 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-glibc@2.4.1': resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.4.1': resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.4.1': resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.4.1': resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.4.1': resolution: {integrity: sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==} @@ -852,46 +857,55 @@ packages: resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.24.0': resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.24.0': resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.24.0': resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.24.0': resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.24.0': resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.24.0': resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.24.0': resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.24.0': resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==} @@ -1464,9 +1478,6 @@ packages: date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} - dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} @@ -2058,6 +2069,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} @@ -4355,8 +4370,6 @@ snapshots: date-fns@3.6.0: {} - dayjs@1.11.13: {} - de-indent@1.0.2: {} debug@2.6.9: @@ -5060,6 +5073,8 @@ snapshots: lru-cache@10.4.3: {} + luxon@3.5.0: {} + magic-string@0.30.12: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 diff --git a/web/src/utils/common/common.ts b/web/src/utils/common/common.ts index 256303c1..498107b0 100644 --- a/web/src/utils/common/common.ts +++ b/web/src/utils/common/common.ts @@ -1,17 +1,29 @@ -import dayjs from 'dayjs' +import { DateTime, Duration } from 'luxon' type Time = undefined | string | Date -/** 格式化时间,默认格式:YYYY-MM-DD HH:mm:ss */ -export function formatDateTime(time: Time, format = 'YYYY-MM-DD HH:mm:ss'): string { - return dayjs(time).format(format) +/** 格式化时间,默认格式:yyyy-MM-dd HH:mm:ss */ +export function formatDateTime(time: Time, format = 'yyyy-MM-dd HH:mm:ss'): string { + const dateTime = time ? DateTime.fromJSDate(new Date(time)) : DateTime.now() + return dateTime.toFormat(format) } -/** 格式化日期,默认格式:YYYY-MM-DD */ -export function formatDate(date: Time = undefined, format = 'YYYY-MM-DD') { +/** 格式化日期,默认格式:yyyy-MM-dd */ +export function formatDate(date: Time = undefined, format = 'yyyy-MM-dd') { return formatDateTime(date, format) } +/** 格式化持续时间,转为 x天x小时x分钟x秒 */ +export function formatDuration(seconds: number) { + const duration = Duration.fromObject({ seconds }) + const days = Math.floor(duration.as('days')) + const hours = duration.hours + const minutes = duration.minutes + const secs = duration.seconds + + return `${days}天${hours}时${minutes}分${secs}秒` +} + /** 生成随机字符串 */ export function generateRandomString(length: number) { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'