polish: clarify record detail actions

This commit is contained in:
JetSprow
2026-04-30 22:29:25 +10:00
parent 157f3841f6
commit b8a7cab1af
9 changed files with 108 additions and 59 deletions

View File

@@ -1,9 +1,10 @@
import { KeyRound, Server, UserRound, Waypoints } from "lucide-react";
import { Eye, KeyRound, Server, UserRound, Waypoints } from "lucide-react";
import Link from "next/link";
import { batchTestNodeConnections } from "@/actions/admin/nodes";
import { BatchActionBar, BatchActionButton } from "@/components/admin/batch-action-bar";
import { EmptyState } from "@/components/shared/page-shell";
import { StatusBadge } from "@/components/shared/status-badge";
import { buttonVariants } from "@/components/ui/button";
import { getNodeStatusLabel } from "@/lib/domain-labels";
import { NodeActions } from "../node-actions";
import { NodeForm } from "../node-form";
@@ -43,7 +44,6 @@ function InboundPreview({ node }: { node: NodeServerRow }) {
<div className="space-y-2">
<div className="flex min-h-6 items-center justify-between gap-3">
<p className="text-xs font-medium text-muted-foreground"></p>
<StatusBadge tone="neutral">{node._count.inbounds} </StatusBadge>
</div>
<div className="flex flex-wrap gap-2">
{preview.map((inbound) => (
@@ -58,7 +58,7 @@ function InboundPreview({ node }: { node: NodeServerRow }) {
))}
{hiddenCount > 0 && (
<span className="inline-flex min-h-8 items-center rounded-lg border border-border bg-muted/25 px-2.5 text-xs font-medium text-muted-foreground">
+{hiddenCount}
</span>
)}
</div>
@@ -84,9 +84,7 @@ function NodeCard({ node, siteUrl }: { node: NodeServerRow; siteUrl: string | nu
<div className="min-w-0">
<div className="flex min-h-6 flex-wrap items-center gap-2">
<h3 className="min-w-0 truncate text-base font-semibold leading-6 tracking-tight">
<Link href={`/admin/nodes/${node.id}`} className="hover:underline">
{node.name}
</Link>
{node.name}
</h3>
<StatusBadge tone={node.status === "active" ? "success" : "neutral"}>
{getNodeStatusLabel(node.status)}
@@ -99,6 +97,13 @@ function NodeCard({ node, siteUrl }: { node: NodeServerRow; siteUrl: string | nu
<InboundPreview node={node} />
<div className="flex flex-wrap items-center gap-2 xl:justify-end">
<Link
href={`/admin/nodes/${node.id}`}
className={buttonVariants({ variant: "outline", size: "sm" })}
>
<Eye className="size-3.5" />
</Link>
<NodeForm
node={{
id: node.id,

View File

@@ -1,6 +1,6 @@
import Link from "next/link";
import type { SubscriptionRiskEvent } from "@prisma/client";
import { ChevronDown } from "lucide-react";
import { ChevronDown, Eye } from "lucide-react";
import { LogDeleteButton } from "@/components/admin/log-delete-button";
import {
SubscriptionStatusBadge,
@@ -10,6 +10,7 @@ import {
import { EmptyState } from "@/components/shared/page-shell";
import { StatusBadge, type StatusTone } from "@/components/shared/status-badge";
import { SubscriptionRiskReviewActions } from "@/components/subscriptions/subscription-risk-review-actions";
import { buttonVariants } from "@/components/ui/button";
import { formatDate, formatDateShort } from "@/lib/utils";
import { SubscriptionRiskGeoDetails } from "./subscription-risk-geo-details";
import type { SubscriptionRiskEventRow } from "../risk-data";
@@ -114,14 +115,19 @@ function EventScope({ event }: { event: SubscriptionRiskEventRow }) {
return (
<div className="space-y-2">
<Link href={"/admin/subscriptions/" + event.subscription.id} className="break-words font-medium hover:underline">
{event.subscription.plan.name}
</Link>
<p className="break-words font-medium">{event.subscription.plan.name}</p>
<div className="flex flex-wrap items-center gap-2">
<SubscriptionTypeBadge type={event.subscription.plan.type} />
<SubscriptionStatusBadge status={event.subscription.status} />
</div>
<p className="text-xs text-muted-foreground">{formatDateShort(event.subscription.endDate)}</p>
<Link
href={"/admin/subscriptions/" + event.subscription.id}
className={buttonVariants({ variant: "outline", size: "xs" })}
>
<Eye className="size-3" />
</Link>
</div>
);
}
@@ -133,11 +139,18 @@ function UserBlock({ event }: { event: SubscriptionRiskEventRow }) {
return (
<div className="space-y-2">
<Link href={"/admin/users/" + event.user.id} className="block break-all font-medium hover:underline">
{event.user.email}
</Link>
<p className="break-all font-medium">{event.user.email}</p>
<p className="break-words text-xs text-muted-foreground">{event.user.name || "未设置昵称"}</p>
<UserStatusBadge status={event.user.status} />
<div className="flex flex-wrap items-center gap-2">
<UserStatusBadge status={event.user.status} />
<Link
href={"/admin/users/" + event.user.id}
className={buttonVariants({ variant: "outline", size: "xs" })}
>
<Eye className="size-3" />
</Link>
</div>
</div>
);
}

View File

@@ -6,7 +6,7 @@ import type {
SubscriptionType,
UserStatus,
} from "@prisma/client";
import { AlertTriangle, ShieldCheck, UserRound } from "lucide-react";
import { AlertTriangle, Eye, ShieldCheck, UserRound } from "lucide-react";
import { DataTableShell } from "@/components/admin/data-table-shell";
import {
DataTable,
@@ -137,8 +137,15 @@ export function SubscriptionAccessRiskSection({
</div>
<div className="space-y-1 text-sm">
<div className="flex flex-wrap items-center gap-2">
<Link href={"/admin/users/" + owner.id} className="break-all font-medium hover:underline">{owner.email}</Link>
<span className="break-all font-medium">{owner.email}</span>
<UserStatusBadge status={owner.status} />
<Link
href={"/admin/users/" + owner.id}
className={buttonVariants({ variant: "outline", size: "xs" })}
>
<Eye className="size-3" />
</Link>
</div>
<p className="text-muted-foreground">{owner.name || "未设置昵称"}</p>
<p className="break-all font-mono text-xs text-muted-foreground">{owner.id}</p>

View File

@@ -1,4 +1,5 @@
import Link from "next/link";
import { Eye } from "lucide-react";
import { batchSubscriptionOperation } from "@/actions/admin/subscriptions";
import { BatchActionBar, BatchActionButton } from "@/components/admin/batch-action-bar";
import { DataTableShell } from "@/components/admin/data-table-shell";
@@ -15,6 +16,7 @@ import {
SubscriptionStatusBadge,
SubscriptionTypeBadge,
} from "@/components/shared/domain-badges";
import { buttonVariants } from "@/components/ui/button";
import { formatBytes, formatDateShort } from "@/lib/utils";
import { AdminSubscriptionActions } from "../subscription-actions";
import type { StreamingServiceOption } from "../streaming-slot-dialog";
@@ -92,12 +94,7 @@ export function SubscriptionsTable({
className="mt-1 size-4 rounded border-border accent-primary"
/>
<div className="min-w-0 flex-1">
<Link
href={`/admin/subscriptions/${subscription.id}`}
className="break-words text-sm font-semibold hover:underline"
>
{subscription.plan.name}
</Link>
<p className="break-words text-sm font-semibold">{subscription.plan.name}</p>
<p className="mt-1 break-all text-xs text-muted-foreground">{subscription.user.email}</p>
</div>
<SubscriptionStatusBadge status={subscription.status} />
@@ -118,7 +115,14 @@ export function SubscriptionsTable({
</div>
</div>
</div>
<div className="flex justify-end">
<div className="flex flex-wrap justify-end gap-2">
<Link
href={`/admin/subscriptions/${subscription.id}`}
className={buttonVariants({ variant: "outline", size: "sm" })}
>
<Eye className="size-3.5" />
</Link>
<AdminSubscriptionActions
subscriptionId={subscription.id}
status={subscription.status}
@@ -162,12 +166,7 @@ export function SubscriptionsTable({
</p>
</DataTableCell>
<DataTableCell className="max-w-52 whitespace-normal break-words">
<Link
href={`/admin/subscriptions/${subscription.id}`}
className="font-medium hover:underline"
>
{subscription.plan.name}
</Link>
<p className="font-medium">{subscription.plan.name}</p>
</DataTableCell>
<DataTableCell>
<SubscriptionTypeBadge type={subscription.plan.type} />
@@ -188,7 +187,14 @@ export function SubscriptionsTable({
<SubscriptionStatusBadge status={subscription.status} />
</DataTableCell>
<DataTableCell>
<div className="flex justify-end">
<div className="flex flex-wrap justify-end gap-2">
<Link
href={`/admin/subscriptions/${subscription.id}`}
className={buttonVariants({ variant: "outline", size: "sm" })}
>
<Eye className="size-3.5" />
</Link>
<AdminSubscriptionActions
subscriptionId={subscription.id}
status={subscription.status}

View File

@@ -32,9 +32,7 @@ export function AdminSupportTable({ tickets }: AdminSupportTableProps) {
mobileCards={tickets.map((ticket) => (
<article key={ticket.id} className="space-y-3 p-4">
<div className="min-w-0">
<Link href={`/admin/support/${ticket.id}`} className="break-words text-sm font-semibold hover:underline">
{ticket.subject}
</Link>
<p className="break-words text-sm font-semibold">{ticket.subject}</p>
<p className="mt-1 break-all text-xs text-muted-foreground">{ticket.user.email}</p>
</div>
<div className="flex flex-wrap items-center gap-2">
@@ -72,9 +70,7 @@ export function AdminSupportTable({ tickets }: AdminSupportTableProps) {
{tickets.map((ticket) => (
<DataTableRow key={ticket.id}>
<DataTableCell className="max-w-64 whitespace-normal break-words">
<Link href={`/admin/support/${ticket.id}`} className="font-medium hover:underline">
{ticket.subject}
</Link>
<p className="font-medium">{ticket.subject}</p>
{ticket.category && (
<p className="mt-1 text-xs text-muted-foreground">{ticket.category}</p>
)}

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import type { ReactNode } from "react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Eye } from "lucide-react";
import { PageHeader, PageShell, SectionHeader } from "@/components/shared/page-shell";
import { DataTableShell } from "@/components/admin/data-table-shell";
import {
@@ -22,6 +23,7 @@ import {
orderKindLabels,
} from "@/components/shared/domain-badges";
import { StatusBadge, type StatusTone } from "@/components/shared/status-badge";
import { buttonVariants } from "@/components/ui/button";
import {
SupportTicketPriorityBadge,
SupportTicketStatusBadge,
@@ -102,7 +104,15 @@ export default async function AdminUserDetailPage({
<SectionHeader
title="账号资料"
description="用于风控判断时快速确认用户基础信息。"
actions={<Link href={"/admin/subscription-risk?q=" + encodeURIComponent(user.email)} className="text-sm font-medium text-primary hover:underline"></Link>}
actions={
<Link
href={"/admin/subscription-risk?q=" + encodeURIComponent(user.email)}
className={buttonVariants({ variant: "outline", size: "sm" })}
>
<Eye className="size-3.5" />
</Link>
}
/>
<div className="mt-4 grid gap-3 md:grid-cols-2">
<div>
@@ -136,15 +146,14 @@ export default async function AdminUserDetailPage({
<DataTableHeadCell></DataTableHeadCell>
<DataTableHeadCell></DataTableHeadCell>
<DataTableHeadCell></DataTableHeadCell>
<DataTableHeadCell className="text-right"></DataTableHeadCell>
</DataTableHeaderRow>
</DataTableHead>
<DataTableBody>
{subscriptions.map((subscription) => (
<DataTableRow key={subscription.id}>
<DataTableCell>
<Link href={"/admin/subscriptions/" + subscription.id} className="font-medium hover:underline">
{subscription.plan.name}
</Link>
<p className="font-medium">{subscription.plan.name}</p>
</DataTableCell>
<DataTableCell><SubscriptionTypeBadge type={subscription.plan.type} /></DataTableCell>
<DataTableCell><SubscriptionStatusBadge status={subscription.status} /></DataTableCell>
@@ -153,6 +162,17 @@ export default async function AdminUserDetailPage({
</DataTableCell>
<DataTableCell className="whitespace-nowrap text-muted-foreground">{formatDateShort(subscription.endDate)}</DataTableCell>
<DataTableCell className="whitespace-nowrap text-muted-foreground">{formatDateShort(subscription.createdAt)}</DataTableCell>
<DataTableCell>
<div className="flex justify-end">
<Link
href={"/admin/subscriptions/" + subscription.id}
className={buttonVariants({ variant: "outline", size: "sm" })}
>
<Eye className="size-3.5" />
</Link>
</div>
</DataTableCell>
</DataTableRow>
))}
</DataTableBody>
@@ -235,19 +255,29 @@ export default async function AdminUserDetailPage({
<DataTableHeadCell></DataTableHeadCell>
<DataTableHeadCell></DataTableHeadCell>
<DataTableHeadCell></DataTableHeadCell>
<DataTableHeadCell className="text-right"></DataTableHeadCell>
</DataTableHeaderRow>
</DataTableHead>
<DataTableBody>
{supportTickets.map((ticket) => (
<DataTableRow key={ticket.id}>
<DataTableCell>
<Link href={"/admin/support/" + ticket.id} className="max-w-72 truncate font-medium hover:underline">
{ticket.subject}
</Link>
<p className="max-w-72 truncate font-medium">{ticket.subject}</p>
</DataTableCell>
<DataTableCell><SupportTicketStatusBadge status={ticket.status} /></DataTableCell>
<DataTableCell><SupportTicketPriorityBadge priority={ticket.priority} /></DataTableCell>
<DataTableCell className="whitespace-nowrap text-muted-foreground">{formatDateShort(ticket.updatedAt)}</DataTableCell>
<DataTableCell>
<div className="flex justify-end">
<Link
href={"/admin/support/" + ticket.id}
className={buttonVariants({ variant: "outline", size: "sm" })}
>
<Eye className="size-3.5" />
</Link>
</div>
</DataTableCell>
</DataTableRow>
))}
</DataTableBody>