diff --git a/bun.lockb b/bun.lockb index 9041504..3438f77 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 9d60199..32508b2 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "clsx": "^2.1.1", "country-flag-icons": "^1.5.13", "d3-geo": "^3.1.1", + "dayjs": "^1.11.13", "framer-motion": "^12.0.0-alpha.2", "i18next": "^24.1.0", "lucide-react": "^0.460.0", diff --git a/src/components/PlanInfo.tsx b/src/components/PlanInfo.tsx new file mode 100644 index 0000000..5d5b7c3 --- /dev/null +++ b/src/components/PlanInfo.tsx @@ -0,0 +1,69 @@ +import { PublicNoteData, cn } from "@/lib/utils" + +export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData }) { + if (!parsedData || !parsedData.planDataMod) { + return null + } + return ( +
+ {parsedData.planDataMod.bandwidth !== "" && ( +

+ {parsedData.planDataMod.bandwidth} +

+ )} + {parsedData.planDataMod.trafficVol !== "" && ( +

+ {parsedData.planDataMod.trafficVol} +

+ )} + {parsedData.planDataMod.IPv4 === "1" && ( +

+ IPv4 +

+ )} + {parsedData.planDataMod.IPv6 === "1" && ( +

+ IPv6 +

+ )} + {parsedData.planDataMod.networkRoute && ( +

+ {parsedData.planDataMod.networkRoute.split(",").map((route) => { + return route + })} +

+ )} + {parsedData.planDataMod.extra && ( +

+ {parsedData.planDataMod.extra.split(",").map((extra) => { + return extra + })} +

+ )} +
+ ) +} diff --git a/src/components/RemainPercentBar.tsx b/src/components/RemainPercentBar.tsx new file mode 100644 index 0000000..0cb4d93 --- /dev/null +++ b/src/components/RemainPercentBar.tsx @@ -0,0 +1,21 @@ +import { cn } from "@/lib/utils" + +import { Progress } from "./ui/progress" + +export default function RemainPercentBar({ + value, + className, +}: { + value: number + className?: string +}) { + return ( + + ) +} diff --git a/src/components/ServerCard.tsx b/src/components/ServerCard.tsx index 868ad25..ec4a6ba 100644 --- a/src/components/ServerCard.tsx +++ b/src/components/ServerCard.tsx @@ -1,11 +1,13 @@ import ServerFlag from "@/components/ServerFlag" import ServerUsageBar from "@/components/ServerUsageBar" import { formatBytes } from "@/lib/format" -import { cn, formatNezhaInfo, getDaysBetweenDates, parsePublicNote } from "@/lib/utils" +import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils" import { NezhaServer } from "@/types/nezha-api" import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" +import PlanInfo from "./PlanInfo" +import BillingInfo from "./billingInfo" import { Badge } from "./ui/badge" import { Card } from "./ui/card" @@ -37,17 +39,6 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf const parsedData = parsePublicNote(public_note) - let daysLeft = 0 - let isNeverExpire = false - - if (parsedData?.billingDataMod?.endDate) { - if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) { - isNeverExpire = true - } else { - daysLeft = getDaysBetweenDates(parsedData.billingDataMod.endDate, new Date(now).toISOString()) - } - } - return online ? ( {name}

- {parsedData?.billingDataMod && - (daysLeft >= 0 ? ( - <> -

- 剩余时间: {isNeverExpire ? "永久" : daysLeft + "天"} -

- {parsedData.billingDataMod.amount && - parsedData.billingDataMod.amount !== "0" && - parsedData.billingDataMod.amount !== "-1" ? ( -

- 价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} -

- ) : parsedData.billingDataMod.amount === "0" ? ( -

免费

- ) : parsedData.billingDataMod.amount === "-1" ? ( -

按量收费

- ) : null} - - ) : ( - <> -

- 已过期: {daysLeft * -1} 天 -

- {parsedData.billingDataMod.amount && - parsedData.billingDataMod.amount !== "0" && - parsedData.billingDataMod.amount !== "-1" ? ( -

- 价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} -

- ) : parsedData.billingDataMod.amount === "0" ? ( -

免费

- ) : parsedData.billingDataMod.amount === "-1" ? ( -

按量收费

- ) : null} - - ))} - {parsedData?.planDataMod && ( -
- {parsedData.planDataMod.bandwidth !== "" && ( -

- {parsedData.planDataMod.bandwidth} -

- )} - {parsedData.planDataMod.trafficVol !== "" && ( -

- {parsedData.planDataMod.trafficVol} -

- )} -
- )} + {parsedData?.billingDataMod && }
@@ -188,12 +122,13 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf )} + {parsedData?.planDataMod && }
) : ( : null}
-

+

{name}

- {parsedData?.billingDataMod && - (daysLeft >= 0 ? ( - <> -

- 剩余时间: {isNeverExpire ? "永久" : daysLeft + "天"} -

- {parsedData.billingDataMod.amount && - parsedData.billingDataMod.amount !== "0" && - parsedData.billingDataMod.amount !== "-1" ? ( -

- 价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} -

- ) : parsedData.billingDataMod.amount === "0" ? ( -

免费

- ) : parsedData.billingDataMod.amount === "-1" ? ( -

按量收费

- ) : null} - - ) : ( - <> -

- 已过期: {daysLeft * -1} 天 -

- {parsedData.billingDataMod.amount && - parsedData.billingDataMod.amount !== "0" && - parsedData.billingDataMod.amount !== "-1" ? ( -

- 价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} -

- ) : parsedData.billingDataMod.amount === "0" ? ( -

免费

- ) : parsedData.billingDataMod.amount === "-1" ? ( -

按量收费

- ) : null} - - ))} - {parsedData?.planDataMod && ( -
- {parsedData.planDataMod.bandwidth !== "" && ( -

- {parsedData.planDataMod.bandwidth} -

- )} - {parsedData.planDataMod.trafficVol !== "" && ( -

- {parsedData.planDataMod.trafficVol} -

- )} -
- )} + {parsedData?.billingDataMod && }
+ {parsedData?.planDataMod && }
) } diff --git a/src/components/ServerCardInline.tsx b/src/components/ServerCardInline.tsx index 037d6b1..3a2a8cc 100644 --- a/src/components/ServerCardInline.tsx +++ b/src/components/ServerCardInline.tsx @@ -2,11 +2,13 @@ import ServerFlag from "@/components/ServerFlag" import ServerUsageBar from "@/components/ServerUsageBar" import { formatBytes } from "@/lib/format" import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class" -import { cn, formatNezhaInfo, getDaysBetweenDates, parsePublicNote } from "@/lib/utils" +import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils" import { NezhaServer } from "@/types/nezha-api" import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" +import PlanInfo from "./PlanInfo" +import BillingInfo from "./billingInfo" import { Card } from "./ui/card" import { Separator } from "./ui/separator" @@ -43,17 +45,6 @@ export default function ServerCardInline({ const parsedData = parsePublicNote(public_note) - let daysLeft = 0 - let isNeverExpire = false - - if (parsedData?.billingDataMod?.endDate) { - if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) { - isNeverExpire = true - } else { - daysLeft = getDaysBetweenDates(parsedData.billingDataMod.endDate, new Date(now).toISOString()) - } - } - return online ? (
{name}

- {parsedData?.billingDataMod && - (daysLeft >= 0 ? ( - <> -

- 剩余时间: {isNeverExpire ? "永久" : daysLeft + "天"} -

- {parsedData.billingDataMod.amount && - parsedData.billingDataMod.amount !== "0" && - parsedData.billingDataMod.amount !== "-1" ? ( -

- 价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} -

- ) : parsedData.billingDataMod.amount === "0" ? ( -

免费

- ) : parsedData.billingDataMod.amount === "-1" ? ( -

按量收费

- ) : null} - - ) : ( - <> -

- 已过期: {daysLeft * -1} 天 -

- {parsedData.billingDataMod.amount && - parsedData.billingDataMod.amount !== "0" && - parsedData.billingDataMod.amount !== "-1" ? ( -

- 价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} -

- ) : parsedData.billingDataMod.amount === "0" ? ( -

免费

- ) : parsedData.billingDataMod.amount === "-1" ? ( -

按量收费

- ) : null} - - ))} - {parsedData?.planDataMod && ( -
- {parsedData.planDataMod.bandwidth !== "" && ( -

- {parsedData.planDataMod.bandwidth} -

- )} - {parsedData.planDataMod.trafficVol !== "" && ( -

- {parsedData.planDataMod.trafficVol} -

- )} -
- )} + {parsedData?.billingDataMod && }
-
+
@@ -221,13 +155,14 @@ export default function ServerCardInline({
+ {parsedData?.planDataMod && }
) : ( navigate(`/server/${serverInfo.id}`)} >
@@ -253,66 +188,11 @@ export default function ServerCardInline({ > {name}

- {parsedData?.billingDataMod && - (daysLeft >= 0 ? ( - <> -

- 剩余时间: {isNeverExpire ? "永久" : daysLeft + "天"} -

- {parsedData.billingDataMod.amount && - parsedData.billingDataMod.amount !== "0" && - parsedData.billingDataMod.amount !== "-1" ? ( -

- 价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} -

- ) : parsedData.billingDataMod.amount === "0" ? ( -

免费

- ) : parsedData.billingDataMod.amount === "-1" ? ( -

按量收费

- ) : null} - - ) : ( - <> -

- 已过期: {daysLeft * -1} 天 -

- {parsedData.billingDataMod.amount && - parsedData.billingDataMod.amount !== "0" && - parsedData.billingDataMod.amount !== "-1" ? ( -

- 价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} -

- ) : parsedData.billingDataMod.amount === "0" ? ( -

免费

- ) : parsedData.billingDataMod.amount === "-1" ? ( -

按量收费

- ) : null} - - ))} - {parsedData?.planDataMod && ( -
- {parsedData.planDataMod.bandwidth !== "" && ( -

- {parsedData.planDataMod.bandwidth} -

- )} - {parsedData.planDataMod.trafficVol !== "" && ( -

- {parsedData.planDataMod.trafficVol} -

- )} -
- )} + {parsedData?.billingDataMod && }
+ + {parsedData?.planDataMod && } ) } diff --git a/src/components/billingInfo.tsx b/src/components/billingInfo.tsx new file mode 100644 index 0000000..9a0affa --- /dev/null +++ b/src/components/billingInfo.tsx @@ -0,0 +1,61 @@ +import { PublicNoteData, cn, getDaysBetweenDatesWithAutoRenewal } from "@/lib/utils" + +import RemainPercentBar from "./RemainPercentBar" + +export default function BillingInfo({ parsedData }: { parsedData: PublicNoteData }) { + if (!parsedData || !parsedData.billingDataMod) { + return null + } + + let isNeverExpire = false + let daysLeftObject = { + days: 0, + cycleLabel: "", + remainingPercentage: 0, + } + + if (parsedData?.billingDataMod?.endDate) { + if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) { + isNeverExpire = true + } else { + daysLeftObject = getDaysBetweenDatesWithAutoRenewal(parsedData.billingDataMod) + } + } + + return daysLeftObject.days >= 0 ? ( + <> +
+ 剩余时间: {isNeverExpire ? "永久" : daysLeftObject.days + "天"} +
+ {parsedData.billingDataMod.amount && + parsedData.billingDataMod.amount !== "0" && + parsedData.billingDataMod.amount !== "-1" ? ( +

+ 价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} +

+ ) : parsedData.billingDataMod.amount === "0" ? ( +

免费

+ ) : parsedData.billingDataMod.amount === "-1" ? ( +

按量收费

+ ) : null} + + + ) : ( + <> +

+ 已过期: {daysLeftObject.days * -1} 天 +

+ {parsedData.billingDataMod.amount && + parsedData.billingDataMod.amount !== "0" && + parsedData.billingDataMod.amount !== "-1" ? ( +

+ 价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} +

+ ) : parsedData.billingDataMod.amount === "0" ? ( +

免费

+ ) : parsedData.billingDataMod.amount === "-1" ? ( +

按量收费

+ ) : null} + + ) +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d04c9be..3777f5b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,5 +1,6 @@ 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[]) { @@ -46,6 +47,112 @@ export function formatNezhaInfo(now: number, serverInfo: NezhaServer) { } } +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"), + } + } + + const nextTime = getNextCycleTime(endTime, months, nowTime) + const diff = dayjs(nextTime).diff(dayjs(), "day") + 1 + const remainingPercentage = diff / (30 * months) > 1 ? 1 : diff / (30 * months) + + console.log( + "nextTime", + nextTime, + "diff", + diff, + "month", + months, + "remainingPercentage", + remainingPercentage, + ) + + 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) @@ -137,7 +244,7 @@ interface PlanData { extra: string } -interface PublicNoteData { +export interface PublicNoteData { billingDataMod?: BillingData planDataMod?: PlanData }