mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 09:31:55 +08:00
feat: refactor public note
This commit is contained in:
parent
3d66582a3a
commit
407332c938
@ -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",
|
||||
|
69
src/components/PlanInfo.tsx
Normal file
69
src/components/PlanInfo.tsx
Normal 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>
|
||||
)
|
||||
}
|
21
src/components/RemainPercentBar.tsx
Normal file
21
src/components/RemainPercentBar.tsx
Normal 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)}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
61
src/components/billingInfo.tsx
Normal file
61
src/components/billingInfo.tsx
Normal 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}
|
||||
</>
|
||||
)
|
||||
}
|
109
src/lib/utils.ts
109
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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user