mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 17:41:56 +08:00
feat: refactor public note
This commit is contained in:
parent
3d66582a3a
commit
407332c938
@ -34,6 +34,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"country-flag-icons": "^1.5.13",
|
"country-flag-icons": "^1.5.13",
|
||||||
"d3-geo": "^3.1.1",
|
"d3-geo": "^3.1.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"framer-motion": "^12.0.0-alpha.2",
|
"framer-motion": "^12.0.0-alpha.2",
|
||||||
"i18next": "^24.1.0",
|
"i18next": "^24.1.0",
|
||||||
"lucide-react": "^0.460.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 ServerFlag from "@/components/ServerFlag"
|
||||||
import ServerUsageBar from "@/components/ServerUsageBar"
|
import ServerUsageBar from "@/components/ServerUsageBar"
|
||||||
import { formatBytes } from "@/lib/format"
|
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 { NezhaServer } from "@/types/nezha-api"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
|
import PlanInfo from "./PlanInfo"
|
||||||
|
import BillingInfo from "./billingInfo"
|
||||||
import { Badge } from "./ui/badge"
|
import { Badge } from "./ui/badge"
|
||||||
import { Card } from "./ui/card"
|
import { Card } from "./ui/card"
|
||||||
|
|
||||||
@ -37,17 +39,6 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
|
|||||||
|
|
||||||
const parsedData = parsePublicNote(public_note)
|
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 ? (
|
return online ? (
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -74,64 +65,7 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
|
|||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</p>
|
</p>
|
||||||
{parsedData?.billingDataMod &&
|
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||||
(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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@ -188,12 +122,13 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
|
|||||||
</Badge>
|
</Badge>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
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]",
|
showNetTransfer ? "lg:min-h-[91px] min-h-[123px]" : "lg:min-h-[61px] min-h-[93px]",
|
||||||
{
|
{
|
||||||
"bg-card/50": customBackgroundImage,
|
"bg-card/50": customBackgroundImage,
|
||||||
@ -212,69 +147,18 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
|
|||||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex flex-col">
|
<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}
|
{name}
|
||||||
</p>
|
</p>
|
||||||
{parsedData?.billingDataMod &&
|
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||||
(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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,13 @@ import ServerFlag from "@/components/ServerFlag"
|
|||||||
import ServerUsageBar from "@/components/ServerUsageBar"
|
import ServerUsageBar from "@/components/ServerUsageBar"
|
||||||
import { formatBytes } from "@/lib/format"
|
import { formatBytes } from "@/lib/format"
|
||||||
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
|
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 { NezhaServer } from "@/types/nezha-api"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
|
import PlanInfo from "./PlanInfo"
|
||||||
|
import BillingInfo from "./billingInfo"
|
||||||
import { Card } from "./ui/card"
|
import { Card } from "./ui/card"
|
||||||
import { Separator } from "./ui/separator"
|
import { Separator } from "./ui/separator"
|
||||||
|
|
||||||
@ -43,17 +45,6 @@ export default function ServerCardInline({
|
|||||||
|
|
||||||
const parsedData = parsePublicNote(public_note)
|
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 ? (
|
return online ? (
|
||||||
<section>
|
<section>
|
||||||
<Card
|
<Card
|
||||||
@ -87,68 +78,11 @@ export default function ServerCardInline({
|
|||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</p>
|
</p>
|
||||||
{parsedData?.billingDataMod &&
|
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||||
(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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
|
<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")}>
|
<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={"items-center flex flex-row gap-2 whitespace-nowrap"}>
|
||||||
<div className="text-xs font-semibold">
|
<div className="text-xs font-semibold">
|
||||||
@ -221,13 +155,14 @@ export default function ServerCardInline({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
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,
|
"bg-card/50": customBackgroundImage,
|
||||||
},
|
},
|
||||||
@ -235,7 +170,7 @@ export default function ServerCardInline({
|
|||||||
onClick={() => navigate(`/server/${serverInfo.id}`)}
|
onClick={() => navigate(`/server/${serverInfo.id}`)}
|
||||||
>
|
>
|
||||||
<section
|
<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" }}
|
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||||
>
|
>
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
<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}
|
{name}
|
||||||
</p>
|
</p>
|
||||||
{parsedData?.billingDataMod &&
|
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||||
(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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" />
|
||||||
|
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||||
</Card>
|
</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 { NezhaServer } from "@/types/nezha-api"
|
||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import dayjs from "dayjs"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
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 {
|
export function getDaysBetweenDates(date1: string, date2: string): number {
|
||||||
const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
|
const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
|
||||||
const firstDate = new Date(date1)
|
const firstDate = new Date(date1)
|
||||||
@ -137,7 +244,7 @@ interface PlanData {
|
|||||||
extra: string
|
extra: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PublicNoteData {
|
export interface PublicNoteData {
|
||||||
billingDataMod?: BillingData
|
billingDataMod?: BillingData
|
||||||
planDataMod?: PlanData
|
planDataMod?: PlanData
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user