mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
polish: clarify record detail actions
This commit is contained in:
@@ -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>
|
||||
</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,
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { ShoppingBag } from "lucide-react";
|
||||
import { CreditCard, ShoppingBag } from "lucide-react";
|
||||
import { DataTableShell } from "@/components/shared/data-table-shell";
|
||||
import {
|
||||
DataTable,
|
||||
@@ -66,12 +66,13 @@ export function UserOrdersTable({ orders }: UserOrdersTableProps) {
|
||||
{formatDateShort(order.createdAt)}
|
||||
</DataTableCell>
|
||||
<DataTableCell>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
{order.status === "PENDING" && (
|
||||
<Link
|
||||
href={`/pay/${order.id}`}
|
||||
className="text-sm font-medium text-primary hover:underline"
|
||||
className={buttonVariants({ size: "sm" })}
|
||||
>
|
||||
<CreditCard className="size-3.5" />
|
||||
去支付
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { format } from "date-fns";
|
||||
import { zhCN } from "date-fns/locale";
|
||||
import { ArrowUpRight, CalendarClock, Database, Radio, Server, Tv } from "lucide-react";
|
||||
import { CalendarClock, Database, Radio, Server, Tv } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { StatusBadge } from "@/components/shared/status-badge";
|
||||
@@ -104,14 +103,8 @@ export function ActiveSubscriptionCard({ sub, poolMap }: ActiveSubscriptionCardP
|
||||
{sub.plan.type === "PROXY" ? <Radio className="size-4" /> : <Tv className="size-4" />}
|
||||
</span>
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<CardTitle className="text-base">
|
||||
<Link
|
||||
href={`/subscriptions/${sub.id}`}
|
||||
className="group/link inline-flex max-w-full items-center gap-1.5 hover:text-primary"
|
||||
>
|
||||
<span className="truncate">{sub.plan.name}</span>
|
||||
<ArrowUpRight className="size-3.5 opacity-45 transition-transform duration-300 group-hover/link:-translate-y-0.5 group-hover/link:translate-x-0.5 group-hover/link:opacity-100" />
|
||||
</Link>
|
||||
<CardTitle className="truncate text-base">
|
||||
{sub.plan.name}
|
||||
</CardTitle>
|
||||
<p className="inline-flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<CalendarClock className="size-3.5" />
|
||||
|
||||
@@ -50,9 +50,7 @@ export function UserSupportTicketTable({ tickets }: UserSupportTicketTableProps)
|
||||
{tickets.map((ticket) => (
|
||||
<DataTableRow key={ticket.id}>
|
||||
<DataTableCell>
|
||||
<Link href={`/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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user