feat: refactor public note

This commit is contained in:
hamster1963 2024-12-19 13:48:13 +08:00
parent 3d66582a3a
commit 407332c938
8 changed files with 285 additions and 262 deletions

BIN
bun.lockb

Binary file not shown.

View File

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

View File

@ -0,0 +1,69 @@
import { PublicNoteData, cn } from "@/lib/utils"
export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData }) {
if (!parsedData || !parsedData.planDataMod) {
return null
}
return (
<section className="flex gap-1 items-center flex-wrap mt-0.5">
{parsedData.planDataMod.bandwidth !== "" && (
<p
className={cn(
"text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.bandwidth}
</p>
)}
{parsedData.planDataMod.trafficVol !== "" && (
<p
className={cn(
"text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.trafficVol}
</p>
)}
{parsedData.planDataMod.IPv4 === "1" && (
<p
className={cn(
"text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
IPv4
</p>
)}
{parsedData.planDataMod.IPv6 === "1" && (
<p
className={cn(
"text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
IPv6
</p>
)}
{parsedData.planDataMod.networkRoute && (
<p
className={cn(
"text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.networkRoute.split(",").map((route) => {
return route
})}
</p>
)}
{parsedData.planDataMod.extra && (
<p
className={cn(
"text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.extra.split(",").map((extra) => {
return extra
})}
</p>
)}
</section>
)
}

View File

@ -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 (
<Progress
aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"}
value={value}
indicatorClassName={value < 30 ? "bg-red-500" : value < 70 ? "bg-orange-400" : "bg-green-500"}
className={cn("h-[3px] rounded-sm w-[70px]", className)}
/>
)
}

View File

@ -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 ? (
<Card
className={cn(
@ -74,64 +65,7 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
>
{name}
</p>
{parsedData?.billingDataMod &&
(daysLeft >= 0 ? (
<>
<p className={cn("text-[10px] text-muted-foreground")}>
: {isNeverExpire ? "永久" : daysLeft + "天"}
</p>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}></p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}></p>
) : null}
</>
) : (
<>
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
: {daysLeft * -1}
</p>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}></p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}></p>
) : null}
</>
))}
{parsedData?.planDataMod && (
<section className="flex gap-1 items-center flex-wrap mt-0.5">
{parsedData.planDataMod.bandwidth !== "" && (
<p
className={cn(
"text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.bandwidth}
</p>
)}
{parsedData.planDataMod.trafficVol !== "" && (
<p
className={cn(
"text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.trafficVol}
</p>
)}
</section>
)}
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
</section>
<div className="flex flex-col gap-2">
@ -188,12 +122,13 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
</Badge>
</section>
)}
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</div>
</Card>
) : (
<Card
className={cn(
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 lg:flex-row cursor-pointer hover:bg-accent/50 transition-colors",
"flex flex-col items-center justify-start gap-3 sm:gap-0 p-3 md:px-5 lg:flex-row cursor-pointer hover:bg-accent/50 transition-colors",
showNetTransfer ? "lg:min-h-[91px] min-h-[123px]" : "lg:min-h-[61px] min-h-[93px]",
{
"bg-card/50": customBackgroundImage,
@ -212,69 +147,18 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative flex flex-col">
<p className={cn("break-all font-bold tracking-tight", showFlag ? "text-xs" : "text-sm")}>
<p
className={cn(
"break-all font-bold tracking-tight max-w-[108px]",
showFlag ? "text-xs" : "text-sm",
)}
>
{name}
</p>
{parsedData?.billingDataMod &&
(daysLeft >= 0 ? (
<>
<p className={cn("text-[10px] text-muted-foreground")}>
: {isNeverExpire ? "永久" : daysLeft + "天"}
</p>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}></p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}></p>
) : null}
</>
) : (
<>
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
: {daysLeft * -1}
</p>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}></p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}></p>
) : null}
</>
))}
{parsedData?.planDataMod && (
<section className="flex gap-1 items-center flex-wrap mt-0.5">
{parsedData.planDataMod.bandwidth !== "" && (
<p
className={cn(
"text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.bandwidth}
</p>
)}
{parsedData.planDataMod.trafficVol !== "" && (
<p
className={cn(
"text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.trafficVol}
</p>
)}
</section>
)}
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
</section>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</Card>
)
}

View File

@ -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 ? (
<section>
<Card
@ -87,68 +78,11 @@ export default function ServerCardInline({
>
{name}
</p>
{parsedData?.billingDataMod &&
(daysLeft >= 0 ? (
<>
<p className={cn("text-[10px] text-muted-foreground")}>
: {isNeverExpire ? "永久" : daysLeft + "天"}
</p>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}></p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}></p>
) : null}
</>
) : (
<>
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
: {daysLeft * -1}
</p>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}></p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}></p>
) : null}
</>
))}
{parsedData?.planDataMod && (
<section className="flex gap-1 items-center flex-wrap mt-0.5">
{parsedData.planDataMod.bandwidth !== "" && (
<p
className={cn(
"text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.bandwidth}
</p>
)}
{parsedData.planDataMod.trafficVol !== "" && (
<p
className={cn(
"text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.trafficVol}
</p>
)}
</section>
)}
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
</section>
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1">
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
<div className={"items-center flex flex-row gap-2 whitespace-nowrap"}>
<div className="text-xs font-semibold">
@ -221,13 +155,14 @@ export default function ServerCardInline({
</div>
</div>
</section>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</div>
</Card>
</section>
) : (
<Card
className={cn(
"flex min-h-[61px] min-w-[900px] items-center justify-start gap-3 p-3 md:px-5 flex-row cursor-pointer hover:bg-accent/50 transition-colors",
"flex min-h-[61px] min-w-[900px] items-center justify-start p-3 md:px-5 flex-row cursor-pointer hover:bg-accent/50 transition-colors",
{
"bg-card/50": customBackgroundImage,
},
@ -235,7 +170,7 @@ export default function ServerCardInline({
onClick={() => navigate(`/server/${serverInfo.id}`)}
>
<section
className={cn("grid items-center gap-2 lg:w-40")}
className={cn("grid items-center gap-2 w-40")}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
@ -253,66 +188,11 @@ export default function ServerCardInline({
>
{name}
</p>
{parsedData?.billingDataMod &&
(daysLeft >= 0 ? (
<>
<p className={cn("text-[10px] text-muted-foreground")}>
: {isNeverExpire ? "永久" : daysLeft + "天"}
</p>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}></p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}></p>
) : null}
</>
) : (
<>
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
: {daysLeft * -1}
</p>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}></p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}></p>
) : null}
</>
))}
{parsedData?.planDataMod && (
<section className="flex gap-1 items-center flex-wrap mt-0.5">
{parsedData.planDataMod.bandwidth !== "" && (
<p
className={cn(
"text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.bandwidth}
</p>
)}
{parsedData.planDataMod.trafficVol !== "" && (
<p
className={cn(
"text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.trafficVol}
</p>
)}
</section>
)}
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
</section>
<Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" />
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</Card>
)
}

View File

@ -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 ? (
<>
<div className={cn("text-[10px] text-muted-foreground")}>
: {isNeverExpire ? "永久" : daysLeftObject.days + "天"}
</div>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}></p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}></p>
) : null}
<RemainPercentBar className="mt-0.5" value={daysLeftObject.remainingPercentage * 100} />
</>
) : (
<>
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
: {daysLeftObject.days * -1}
</p>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}></p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}></p>
) : null}
</>
)
}

View File

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