Polish admin list UI for lite

This commit is contained in:
JetSprow
2026-04-30 20:49:03 +10:00
parent d666f1450f
commit 6ee9cf2857
11 changed files with 244 additions and 258 deletions

View File

@@ -1,7 +1,5 @@
import { Network, Tv } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ActiveStatusBadge, StatusBadge } from "@/components/admin/status-badge";
import { DetailItem, DetailList } from "@/components/admin/detail-list";
import {
PlanFormValue,
type StreamingServiceOption,
@@ -65,27 +63,15 @@ function toNumber(value: NumericLike): number | null {
return value == null ? null : Number(value);
}
function money(value: NumericLike): string {
return `¥${Number(value ?? 0).toFixed(2)}`;
}
function remainingStockSummary(plan: PlanListItem, activeCount: number) {
if (plan.totalLimit == null) return { value: "∞", hint: "剩余库存", empty: false };
function renewalSummary(plan: PlanListItem) {
if (!plan.allowRenewal) return "续费关闭";
if (plan.renewalPricingMode === "PER_DAY") {
return `${money(plan.renewalPrice)}/天 · ${plan.renewalMinDays ?? 1}-${plan.renewalMaxDays ?? plan.durationDays}`;
}
return `${money(plan.renewalPrice)} / ${plan.renewalDurationDays ?? plan.durationDays}`;
}
function topupSummary(plan: PlanListItem) {
if (!plan.allowTrafficTopup) return "增流量关闭";
const range = plan.maxTopupGb == null
? `最少 ${plan.minTopupGb ?? 1} GB`
: `${plan.minTopupGb ?? 1}-${plan.maxTopupGb} GB`;
if (plan.topupPricingMode === "FIXED_AMOUNT") {
return `${money(plan.topupFixedPrice)} 固定 · ${range}`;
}
return `${money(plan.topupPricePerGb)}/GB · ${range}`;
const remaining = Math.max(0, plan.totalLimit - activeCount);
return {
value: remaining.toString(),
hint: remaining === 0 ? "已售罄" : "剩余库存",
empty: remaining === 0,
};
}
function buildPlanFormValue(plan: PlanListItem): PlanFormValue {
@@ -127,105 +113,49 @@ function buildPlanFormValue(plan: PlanListItem): PlanFormValue {
}
export function PlanCard({ plan, activeCount, services, batchFormId }: PlanCardProps) {
const remaining = plan.totalLimit == null ? null : Math.max(0, plan.totalLimit - activeCount);
const planFormValue = buildPlanFormValue(plan);
const stock = remainingStockSummary(plan, activeCount);
const Icon = plan.type === "PROXY" ? Network : Tv;
return (
<Card>
<CardHeader className="gap-4">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="flex min-w-0 items-start gap-3">
<input
form={batchFormId}
type="checkbox"
name="planIds"
value={plan.id}
aria-label={`选择套餐 ${plan.name}`}
className="mt-3 size-5 rounded-lg border-border accent-primary shadow-sm"
/>
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg border border-primary/15 bg-primary/10 text-primary">
<Icon className="size-5" />
</span>
<div className="min-w-0 space-y-1.5">
<CardTitle className="text-lg text-balance">{plan.name}</CardTitle>
<p className="text-sm leading-6 text-muted-foreground text-pretty">
{plan.description || "无描述"} · {plan._count.subscriptions}
</p>
</div>
<section className="grid gap-4 px-4 py-4 lg:grid-cols-[minmax(0,1fr)_10rem_auto] lg:items-center">
<div className="flex min-w-0 items-start gap-3">
<input
form={batchFormId}
type="checkbox"
name="planIds"
value={plan.id}
aria-label={`选择套餐 ${plan.name}`}
className="mt-2 size-5 rounded-lg border-border accent-primary shadow-sm"
/>
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg border border-primary/15 bg-primary/10 text-primary">
<Icon className="size-4" />
</span>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h3 className="truncate text-base font-semibold tracking-tight">{plan.name}</h3>
<StatusBadge tone={plan.type === "PROXY" ? "info" : "warning"}>
{plan.type === "PROXY" ? "代理" : "流媒体"}
</StatusBadge>
<ActiveStatusBadge active={plan.isActive} activeLabel="上架中" inactiveLabel="已下架" />
</div>
<PlanActions
isActive={plan.isActive}
services={services}
plan={planFormValue}
/>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<StatusBadge tone={plan.type === "PROXY" ? "info" : "warning"}>
{plan.type === "PROXY" ? "代理套餐" : "流媒体套餐"}
</StatusBadge>
<ActiveStatusBadge active={plan.isActive} activeLabel="上架中" inactiveLabel="已下架" />
<StatusBadge>{plan.durationDays} </StatusBadge>
<StatusBadge>
{plan.type === "PROXY"
? plan.pricingMode === "FIXED_PACKAGE"
? `${money(plan.fixedPrice)} / ${plan.fixedTrafficGb ?? 0}GB`
: `${money(plan.pricePerGb)}/GB`
: money(plan.price)}
</StatusBadge>
</div>
</CardHeader>
<div className="inline-flex w-fit items-center gap-2 rounded-lg border border-border bg-muted/30 px-3 py-2 lg:justify-self-end">
<span className={`text-lg font-semibold tabular-nums ${stock.empty ? "text-destructive" : "text-foreground"}`}>
{stock.value}
</span>
<span className="text-xs font-medium text-muted-foreground">{stock.hint}</span>
</div>
<CardContent>
{plan.type === "PROXY" ? (
<DetailList>
<DetailItem label="节点">{plan.node?.name ?? "未绑定"}</DetailItem>
<DetailItem label="入站">
{plan.inboundOptions.length > 0
? plan.inboundOptions
.map((option) => `${option.inbound.protocol}:${option.inbound.port}`)
.join(" / ")
: plan.inbound
? `${plan.inbound.protocol}:${plan.inbound.port}`
: "未绑定"}
</DetailItem>
<DetailItem label="售卖方式">
{plan.pricingMode === "FIXED_PACKAGE"
? `固定 ${plan.fixedTrafficGb ?? 0} GB · ${money(plan.fixedPrice)}`
: `自选 ${plan.minTrafficGb ?? 0}-${plan.maxTrafficGb ?? 0} GB`}
</DetailItem>
<DetailItem label="流量池">
{plan.totalTrafficGb == null ? "未配置" : `${plan.totalTrafficGb} GB`}
</DetailItem>
<DetailItem label="库存">
{plan.totalLimit == null
? "不限量"
: `${activeCount}/${plan.totalLimit}${remaining === 0 ? " (已满)" : ""}`}
{plan.perUserLimit != null ? ` · 每人限 ${plan.perUserLimit}` : ""}
</DetailItem>
<DetailItem label="续费 / 增流量">
{renewalSummary(plan)} / {topupSummary(plan)}
</DetailItem>
</DetailList>
) : (
<DetailList>
<DetailItem label="绑定服务">{plan.streamingService?.name ?? "未绑定"}</DetailItem>
<DetailItem label="服务占用">
{plan.streamingService
? `${plan.streamingService.usedSlots}/${plan.streamingService.maxSlots}`
: "-"}
</DetailItem>
<DetailItem label="续费">
{renewalSummary(plan)}
</DetailItem>
<DetailItem label="库存">
{plan.totalLimit == null ? "不限量" : `${activeCount}/${plan.totalLimit}`}
{plan.perUserLimit != null ? ` · 每人限 ${plan.perUserLimit}` : ""}
</DetailItem>
</DetailList>
)}
</CardContent>
</Card>
<div className="flex justify-start lg:justify-end">
<PlanActions
isActive={plan.isActive}
services={services}
plan={planFormValue}
/>
</div>
</section>
);
}