mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-19 09:51:56 +08:00
326 lines
9.6 KiB
TypeScript
326 lines
9.6 KiB
TypeScript
import { NezhaServer } from "@/types/nezha-api"
|
|
import { type ClassValue, clsx } from "clsx"
|
|
import dayjs from "dayjs"
|
|
import { twMerge } from "tailwind-merge"
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs))
|
|
}
|
|
|
|
export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
|
|
const lastActiveTime = serverInfo.last_active.startsWith("000")
|
|
? 0
|
|
: parseISOTimestamp(serverInfo.last_active)
|
|
return {
|
|
...serverInfo,
|
|
cpu: serverInfo.state.cpu || 0,
|
|
gpu: serverInfo.state.gpu || [],
|
|
process: serverInfo.state.process_count || 0,
|
|
up: serverInfo.state.net_out_speed / 1024 / 1024 || 0,
|
|
down: serverInfo.state.net_in_speed / 1024 / 1024 || 0,
|
|
last_active_time_string: lastActiveTime ? new Date(lastActiveTime).toLocaleString() : "",
|
|
online: now - lastActiveTime <= 30000,
|
|
uptime: serverInfo.state.uptime || 0,
|
|
version: serverInfo.host.version || null,
|
|
tcp: serverInfo.state.tcp_conn_count || 0,
|
|
udp: serverInfo.state.udp_conn_count || 0,
|
|
mem: (serverInfo.state.mem_used / serverInfo.host.mem_total) * 100 || 0,
|
|
swap: (serverInfo.state.swap_used / serverInfo.host.swap_total) * 100 || 0,
|
|
disk: (serverInfo.state.disk_used / serverInfo.host.disk_total) * 100 || 0,
|
|
stg: (serverInfo.state.disk_used / serverInfo.host.disk_total) * 100 || 0,
|
|
country_code: serverInfo.country_code,
|
|
platform: serverInfo.host.platform || "",
|
|
net_out_transfer: serverInfo.state.net_out_transfer || 0,
|
|
net_in_transfer: serverInfo.state.net_in_transfer || 0,
|
|
arch: serverInfo.host.arch || "",
|
|
mem_total: serverInfo.host.mem_total || 0,
|
|
swap_total: serverInfo.host.swap_total || 0,
|
|
disk_total: serverInfo.host.disk_total || 0,
|
|
boot_time: serverInfo.host.boot_time || 0,
|
|
platform_version: serverInfo.host.platform_version || "",
|
|
cpu_info: serverInfo.host.cpu || [],
|
|
gpu_info: serverInfo.host.gpu || [],
|
|
load_1: serverInfo.state.load_1?.toFixed(2) || 0.0,
|
|
load_5: serverInfo.state.load_5?.toFixed(2) || 0.0,
|
|
load_15: serverInfo.state.load_15?.toFixed(2) || 0.0,
|
|
public_note: handlePublicNote(serverInfo.id, serverInfo.public_note || ""),
|
|
}
|
|
}
|
|
|
|
export function getDaysBetweenDatesWithAutoRenewal({
|
|
autoRenewal,
|
|
cycle,
|
|
startDate,
|
|
endDate,
|
|
}: BillingData): { days: number; cycleLabel: string; remainingPercentage: number } {
|
|
let months = 1
|
|
// 套餐资费
|
|
let cycleLabel = cycle
|
|
|
|
switch (cycle.toLowerCase()) {
|
|
case "月":
|
|
case "m":
|
|
case "mo":
|
|
case "month":
|
|
case "monthly":
|
|
cycleLabel = "月"
|
|
months = 1
|
|
break
|
|
case "年":
|
|
case "y":
|
|
case "yr":
|
|
case "year":
|
|
case "annual":
|
|
cycleLabel = "年"
|
|
months = 12
|
|
break
|
|
case "季":
|
|
case "quarterly":
|
|
cycleLabel = "季"
|
|
months = 3
|
|
break
|
|
case "半":
|
|
case "半年":
|
|
case "h":
|
|
case "half":
|
|
case "semi-annually":
|
|
cycleLabel = "半年"
|
|
months = 6
|
|
break
|
|
default:
|
|
cycleLabel = cycle
|
|
break
|
|
}
|
|
|
|
const nowTime = new Date().getTime()
|
|
const endTime = dayjs(endDate).valueOf()
|
|
|
|
if (autoRenewal !== "1") {
|
|
return {
|
|
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
|
|
cycleLabel: cycleLabel,
|
|
remainingPercentage:
|
|
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
|
|
dayjs(endDate).diff(startDate, "day") >
|
|
1
|
|
? 1
|
|
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
|
|
dayjs(endDate).diff(startDate, "day"),
|
|
}
|
|
}
|
|
|
|
if (nowTime < endTime) {
|
|
return {
|
|
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
|
|
cycleLabel: cycleLabel,
|
|
remainingPercentage:
|
|
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / (30 * months) > 1
|
|
? 1
|
|
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / (30 * months),
|
|
}
|
|
}
|
|
|
|
const nextTime = getNextCycleTime(endTime, months, nowTime)
|
|
const diff = dayjs(nextTime).diff(dayjs(), "day") + 1
|
|
const remainingPercentage = diff / (30 * months) > 1 ? 1 : diff / (30 * months)
|
|
|
|
return {
|
|
days: diff,
|
|
cycleLabel: cycleLabel,
|
|
remainingPercentage: remainingPercentage,
|
|
}
|
|
}
|
|
|
|
// Thanks to hi2shark for the code
|
|
// https://github.com/hi2shark/nazhua/blob/main/src/utils/date.js#L86
|
|
export function getNextCycleTime(startDate: number, months: number, specifiedDate: number): number {
|
|
const start = dayjs(startDate)
|
|
const checkDate = dayjs(specifiedDate)
|
|
|
|
if (!start.isValid() || months <= 0) {
|
|
throw new Error("参数无效:请检查起始日期、周期月份数和指定日期。")
|
|
}
|
|
|
|
let nextDate = start
|
|
|
|
// 循环增加周期直到大于当前日期
|
|
let whileStatus = true
|
|
while (whileStatus) {
|
|
nextDate = nextDate.add(months, "month")
|
|
whileStatus = nextDate.valueOf() <= checkDate.valueOf()
|
|
}
|
|
|
|
return nextDate.valueOf() // 返回时间毫秒数
|
|
}
|
|
|
|
export function getDaysBetweenDates(date1: string, date2: string): number {
|
|
const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
|
|
const firstDate = new Date(date1)
|
|
const secondDate = new Date(date2)
|
|
|
|
// 计算两个日期之间的天数差异
|
|
return Math.round((firstDate.getTime() - secondDate.getTime()) / oneDay)
|
|
}
|
|
|
|
export const fetcher = (url: string) =>
|
|
fetch(url)
|
|
.then((res) => {
|
|
if (!res.ok) {
|
|
throw new Error(res.statusText)
|
|
}
|
|
return res.json()
|
|
})
|
|
.then((data) => data.data)
|
|
.catch((err) => {
|
|
console.error(err)
|
|
throw err
|
|
})
|
|
|
|
export const nezhaFetcher = async (url: string) => {
|
|
const res = await fetch(url)
|
|
|
|
if (!res.ok) {
|
|
const error = new Error("An error occurred while fetching the data.")
|
|
// @ts-expect-error - res.json() returns a Promise<any>
|
|
error.info = await res.json()
|
|
// @ts-expect-error - res.status is a number
|
|
error.status = res.status
|
|
throw error
|
|
}
|
|
|
|
return res.json()
|
|
}
|
|
|
|
export function parseISOTimestamp(isoString: string): number {
|
|
return new Date(isoString).getTime()
|
|
}
|
|
|
|
export function formatRelativeTime(timestamp: number): string {
|
|
const now = Date.now()
|
|
const diff = now - timestamp
|
|
const hours = Math.floor(diff / (1000 * 60 * 60))
|
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
|
const seconds = Math.floor((diff % (1000 * 60)) / 1000)
|
|
|
|
if (hours > 24) {
|
|
const days = Math.floor(hours / 24)
|
|
return `${days}d`
|
|
} else if (hours > 0) {
|
|
return `${hours}h`
|
|
} else if (minutes > 0) {
|
|
return `${minutes}m`
|
|
} else if (seconds >= 0) {
|
|
return `${seconds}s`
|
|
}
|
|
return "0s"
|
|
}
|
|
|
|
export function formatTime(timestamp: number): string {
|
|
const date = new Date(timestamp)
|
|
const year = date.getFullYear()
|
|
const month = date.getMonth() + 1
|
|
const day = date.getDate()
|
|
const hours = date.getHours().toString().padStart(2, "0")
|
|
const minutes = date.getMinutes().toString().padStart(2, "0")
|
|
const seconds = date.getSeconds().toString().padStart(2, "0")
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
|
}
|
|
|
|
interface BillingData {
|
|
startDate: string
|
|
endDate: string
|
|
autoRenewal: string
|
|
cycle: string
|
|
amount: string
|
|
}
|
|
|
|
interface PlanData {
|
|
bandwidth: string
|
|
trafficVol: string
|
|
trafficType: string
|
|
IPv4: string
|
|
IPv6: string
|
|
networkRoute: string
|
|
extra: string
|
|
}
|
|
|
|
export interface PublicNoteData {
|
|
billingDataMod?: BillingData
|
|
planDataMod?: PlanData
|
|
}
|
|
|
|
export function parsePublicNote(publicNote: string): PublicNoteData | null {
|
|
try {
|
|
if (!publicNote) {
|
|
return null
|
|
}
|
|
const data = JSON.parse(publicNote)
|
|
if (!data.billingDataMod && !data.planDataMod) {
|
|
return null
|
|
}
|
|
if (data.billingDataMod && !data.planDataMod) {
|
|
return {
|
|
billingDataMod: {
|
|
startDate: data.billingDataMod.startDate || "",
|
|
endDate: data.billingDataMod.endDate,
|
|
autoRenewal: data.billingDataMod.autoRenewal || "",
|
|
cycle: data.billingDataMod.cycle || "",
|
|
amount: data.billingDataMod.amount || "",
|
|
},
|
|
}
|
|
}
|
|
if (!data.billingDataMod && data.planDataMod) {
|
|
return {
|
|
planDataMod: {
|
|
bandwidth: data.planDataMod.bandwidth || "",
|
|
trafficVol: data.planDataMod.trafficVol || "",
|
|
trafficType: data.planDataMod.trafficType || "",
|
|
IPv4: data.planDataMod.IPv4 || "",
|
|
IPv6: data.planDataMod.IPv6 || "",
|
|
networkRoute: data.planDataMod.networkRoute || "",
|
|
extra: data.planDataMod.extra || "",
|
|
},
|
|
}
|
|
}
|
|
|
|
return {
|
|
billingDataMod: {
|
|
startDate: data.billingDataMod.startDate || "",
|
|
endDate: data.billingDataMod.endDate,
|
|
autoRenewal: data.billingDataMod.autoRenewal || "",
|
|
cycle: data.billingDataMod.cycle || "",
|
|
amount: data.billingDataMod.amount || "",
|
|
},
|
|
planDataMod: {
|
|
bandwidth: data.planDataMod.bandwidth || "",
|
|
trafficVol: data.planDataMod.trafficVol || "",
|
|
trafficType: data.planDataMod.trafficType || "",
|
|
IPv4: data.planDataMod.IPv4 || "",
|
|
IPv6: data.planDataMod.IPv6 || "",
|
|
networkRoute: data.planDataMod.networkRoute || "",
|
|
extra: data.planDataMod.extra || "",
|
|
},
|
|
}
|
|
} catch (error) {
|
|
console.error("Error parsing public note:", error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
// Function to handle public_note with sessionStorage
|
|
export function handlePublicNote(serverId: number, publicNote: string): string {
|
|
const storageKey = `server_${serverId}_public_note`
|
|
const storedNote = sessionStorage.getItem(storageKey)
|
|
|
|
if (!publicNote && storedNote) {
|
|
return storedNote
|
|
}
|
|
|
|
if (publicNote) {
|
|
sessionStorage.setItem(storageKey, publicNote)
|
|
return publicNote
|
|
}
|
|
|
|
return ""
|
|
}
|