2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00

refactor: 仪表盘实时数据接口

This commit is contained in:
耗子
2024-10-16 17:04:34 +08:00
parent 5acf1e6eb7
commit c80c22c133
18 changed files with 195 additions and 111 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
package request
type DashboardCurrent struct {
Nets []string `json:"nets" form:"nets"`
Disks []string `json:"disks" form:"disks"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

@@ -1,12 +0,0 @@
package types
// 定义面板状态常量
const (
StatusNormal = iota
StatusMaintain
StatusClosed
StatusUpgrade
StatusFailed
)
var Status = StatusNormal

25
pkg/types/system.go Normal file
View File

@@ -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"`
}

View File

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

View File

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

31
web/pnpm-lock.yaml generated
View File

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

View File

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