feat(ui): 统一组件圆角样式,调整标题和按钮样式

This commit is contained in:
Jeff Gu committed 2026-04-11 21:27:37 +08:00
1 parent 9485366765
commit 822b2fad4c
16 files changed
+63 -61

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>☁️</text></svg>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>One API on Workers - 管理控制台</title>
<title>One API on Workers</title>
</head>
<body>
<div id="root"></div>
@@ -44,17 +44,17 @@ export function BreakdownChartCard({
<CardContent className="space-y-3">
{isLoading ? (
Array.from({ length: 4 }, (_, index) => (
<div key={index} className="rounded-2xl border bg-muted/20 p-3 animate-pulse">
<div className="h-4 w-1/3 rounded bg-muted" />
<div key={index} className="animate-pulse">
<div className="h-4 w-1/3 bg-muted" />
<div className="mt-3 h-2 rounded-full bg-muted" />
</div>
))
) : isError ? (
<div className="rounded-2xl border border-dashed bg-muted/10 px-4 py-8 text-center text-sm text-muted-foreground">
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
排行数据加载失败
</div>
) : visibleItems.length === 0 ? (
<div className="rounded-2xl border border-dashed bg-muted/10 px-4 py-8 text-center text-sm text-muted-foreground">
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
当前时间范围暂无维度排行数据
</div>
) : (
@@ -94,9 +94,9 @@ export function BreakdownChartCard({
</div>
</div>
<div className="mt-2 h-2 bg-muted/60">
<div className="mt-2 h-0.5 bg-muted">
<div
className={cn("h-2 bg-gradient-to-r from-primary to-primary/50", barClassName)}
className={cn("h-2 -translate-y-0.75 bg-gradient-to-r from-primary to-primary/50 -skew-x-12", barClassName)}
style={{ width: `${width}%` }}
/>
</div>
@@ -1,5 +1,6 @@
import { AnalyticsRange, AnalyticsTrendPoint } from "@/types";
import { formatCurrency } from "@/lib/utils";
import { Card } from "../ui/card";
const formatAxisLabel = (value: string, range: AnalyticsRange): string => {
const date = new Date(value);
@@ -10,7 +11,6 @@ const formatAxisLabel = (value: string, range: AnalyticsRange): string => {
if (range === "24h") {
return date.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
});
}
@@ -28,9 +28,6 @@ const formatCompactNumber = (value: number): string => {
};
export function TrendBarChart({ points, range }: { points: AnalyticsTrendPoint[]; range: AnalyticsRange }) {
const totalRequests = points.reduce((sum, point) => sum + point.requests, 0);
const totalFailures = points.reduce((sum, point) => sum + point.failures, 0);
const totalCost = points.reduce((sum, point) => sum + point.totalCost, 0);
const peakRequests = Math.max(...points.map((point) => point.requests), 1);
const minColumnWidth = range === "90d" ? 28 : range === "30d" ? 34 : range === "24h" ? 42 : 56;
const chartMinWidth = Math.max(points.length * minColumnWidth, 720);
@@ -45,7 +42,7 @@ export function TrendBarChart({ points, range }: { points: AnalyticsTrendPoint[]
return (
<div className="space-y-5">
<div className="overflow-x-auto rounded-2xl bg-card p-8 pb-6">
<Card className="overflow-x-auto border-0 p-8 pb-6">
<div className="w-full" style={{ minWidth: `${chartMinWidth}px` }}>
<div className="relative">
<div className="pointer-events-none absolute inset-x-0 top-0 h-56">
@@ -55,7 +52,7 @@ export function TrendBarChart({ points, range }: { points: AnalyticsTrendPoint[]
className="absolute inset-x-0 flex items-center"
style={{ top: tick.top }}
>
<span className="w-10 -translate-y-1/2 pr-3 text-right text-[11px] text-muted-foreground">
<span className="w-10 -translate-y-1/2 pr-1 text-right text-[11px] text-muted-foreground">
{formatCompactNumber(Math.round(tick.value))}
</span>
{/* <div className="h-px flex-1 bg-border/20" /> */}
@@ -64,7 +61,7 @@ export function TrendBarChart({ points, range }: { points: AnalyticsTrendPoint[]
</div>
<div
className="ml-12 grid gap-1"
className="ml-10 grid gap-1"
style={{ gridTemplateColumns: `repeat(${points.length}, minmax(0, 1fr))` }}
>
{points.map((point) => {
@@ -73,24 +70,33 @@ export function TrendBarChart({ points, range }: { points: AnalyticsTrendPoint[]
return (
<div key={`${point.timestamp}-${point.requests}`} className="min-w-0">
<div className="flex h-56 items-end bg-muted/50">
<div
className="group relative flex h-56 items-end bg-muted/50 outline-none hover:bg-muted"
tabIndex={0}
aria-label={`${label},请求数 ${formatCompactNumber(point.requests)},花费 ${formatCurrency(point.totalCost)}`}
>
<div className="pointer-events-none absolute top-3 left-[-100%] right-[-100%] mx-auto z-20 w-24 rounded-md border border-border/50 bg-card px-3 py-2 text-left opacity-0 shadow-lg transition-all duration-200 translate-y-2 group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:translate-y-0 group-focus-visible:opacity-100">
<div className="text-[10px] text-muted-foreground">Requests</div>
<div className="text-xs font-semibold text-foreground">
{formatCompactNumber(point.requests)}
</div>
<div className="text-[10px] text-muted-foreground">Total Cost</div>
<div className="text-xs font-semibold text-foreground">{formatCurrency(point.totalCost)}</div>
</div>
<div className="absolute inset-0 ring-1 ring-sky-400/0 transition-all duration-300 group-focus-visible:ring-sky-400/30" />
<div
className="w-full bg-gradient-to-t from-sky-500 to-cyan-400 transition-all duration-300"
className="relative z-10 w-full bg-gradient-to-t from-sky-500 to-cyan-400 transition-all duration-300 group-hover:from-sky-400 group-hover:to-cyan-300 group-focus-visible:from-sky-400 group-focus-visible:to-cyan-300"
style={{ height: `${barHeight}%` }}
/>
</div>
<div className="mt-4 text-center text-[11px] text-muted-foreground">{label}</div>
<div className="text-center text-sm font-medium">{formatCompactNumber(point.requests)}</div>
<div className="text-center text-[11px] text-muted-foreground">
{formatCurrency(point.totalCost)}
</div>
<div className="mt-1 text-center text-[11px] text-muted-foreground">{label}</div>
</div>
);
})}
</div>
</div>
</div>
</div>
</Card>
</div>
);
}
+6 -4
View File
@@ -91,7 +91,7 @@ export function Sidebar({
onClick={() => onNavigate?.()}
title={collapsed ? item.title : undefined}
className={cn(
"group relative flex items-center gap-3 px-3 py-2 rounded-lg text-[13px] font-medium transition-all duration-150",
"group relative flex items-center gap-3 px-3 py-2 rounded-md text-[13px] font-medium transition-all duration-150",
collapsed && "justify-center px-0 w-10 h-10 mx-auto",
isActive
? "bg-primary text-primary-foreground shadow-sm shadow-primary/20"
@@ -128,7 +128,7 @@ export function Sidebar({
) : (
<>
<div className="w-full flex items-center justify-center gap-2.5">
<span className="font-semibold text-[14px] tracking-tight leading-tight">One API on Workers</span>
<span className="font-semibold text-[14px] tracking-tight leading-tight">One API on Workers</span>
</div>
{onClose && (
<button
@@ -195,7 +195,9 @@ export function Sidebar({
onClick={onToggleCollapse}
className={cn(
"flex items-center justify-center rounded-md transition-colors duration-150",
collapsed ? "w-10 h-9 hover:bg-muted text-muted-foreground hover:text-foreground" : "flex-1 h-8 gap-1.5 px-2.5 hover:bg-muted/70 text-muted-foreground hover:text-foreground text-[11px] font-medium",
collapsed
? "w-10 h-9 hover:bg-muted text-muted-foreground hover:text-foreground"
: "flex-1 h-8 gap-1.5 px-2.5 hover:bg-muted/70 text-muted-foreground hover:text-foreground text-[11px] font-medium",
)}
>
{collapsed ? (
@@ -216,7 +218,7 @@ export function Sidebar({
type="button"
title={collapsed ? "退出登录" : undefined}
className={cn(
"flex items-center justify-center gap-2 w-full rounded-lg text-[13px] font-medium transition-all duration-150",
"flex items-center justify-center gap-2 w-full rounded-md text-[13px] font-medium transition-all duration-150",
collapsed ? "h-9" : "h-9 px-3",
"bg-destructive/8 text-destructive hover:bg-destructive/15",
)}
+1 -1
View File
@@ -11,7 +11,7 @@ const Alert = React.forwardRef<
ref={ref}
role="alert"
className={cn(
"relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&:has(svg)]:pl-11",
"relative w-full rounded-md border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&:has(svg)]:pl-11",
{
"bg-background text-foreground": variant === "default",
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive":
+1 -1
View File
@@ -116,7 +116,7 @@ export const AutoCompleteInput = React.forwardRef<HTMLInputElement, AutoComplete
{isOpen && !disabled && (
<div
className={cn(
'absolute z-20 mt-1 w-full overflow-hidden rounded-lg border bg-popover shadow-lg',
'absolute z-20 mt-1 w-full overflow-hidden rounded-md border bg-popover shadow-lg',
dropdownClassName
)}
>
+3 -3
View File
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:shadow-md disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer active:scale-[0.97] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:shadow-md disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer active:scale-[0.97] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
@@ -21,8 +21,8 @@ const buttonVariants = cva(
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-lg px-3",
lg: "h-11 rounded-lg px-8",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
+1 -1
View File
@@ -8,7 +8,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow-soft transition-shadow duration-200",
"rounded-md border bg-card text-card-foreground shadow-soft transition-shadow duration-200",
className
)}
{...props}
+2 -2
View File
@@ -35,13 +35,13 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-xl",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:opacity-100 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-md opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:opacity-100 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
+1 -1
View File
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-lg border border-input px-3 py-2 text-sm shadow-sm transition-all duration-200 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:border-primary/50 hover:border-muted-foreground/30 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-input px-3 py-2 text-sm shadow-sm transition-all duration-200 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:border-primary/50 hover:border-muted-foreground/30 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
+2 -2
View File
@@ -18,10 +18,10 @@ interface SelectOption {
}
const TRIGGER_BASE_CLASS =
'group relative flex h-10 w-full items-center rounded-lg border border-input bg-transparent px-3 py-2 pr-10 text-sm text-foreground shadow-sm outline-none transition-all duration-200 hover:border-muted-foreground/30 focus-visible:border-primary/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50'
'group relative flex h-10 w-full items-center rounded-md border border-input bg-transparent px-3 py-2 pr-10 text-sm text-foreground shadow-sm outline-none transition-all duration-200 hover:border-muted-foreground/30 focus-visible:border-primary/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50'
const CONTENT_BASE_CLASS =
'absolute z-50 mt-1 w-full overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-lg'
'absolute z-50 mt-1 w-full overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-lg'
const ITEM_BASE_CLASS =
'relative flex w-full select-none items-center px-3 py-2 pr-9 text-left text-sm text-foreground outline-none transition-colors duration-150 hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-45'
+1 -1
View File
@@ -8,7 +8,7 @@ const Textarea = React.forwardRef<
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-lg border border-input px-3 py-2 text-sm shadow-sm transition-all duration-200 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:border-primary/50 hover:border-muted-foreground/30 disabled:cursor-not-allowed disabled:opacity-50",
"flex min-h-[80px] w-full rounded-md border border-input px-3 py-2 text-sm shadow-sm transition-all duration-200 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:border-primary/50 hover:border-muted-foreground/30 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
+1 -1
View File
@@ -10,7 +10,7 @@
--color-popover-foreground: hsl(224 71% 4%);
/* Primary - Vibrant indigo */
--color-primary: hsl(234 89% 60%);
--color-primary: #242424;
--color-primary-foreground: hsl(0 0% 100%);
/* Secondary */
+14 -20
View File
@@ -32,11 +32,11 @@ const BREAKDOWN_CHARTS: Array<{
badgeClassName: string;
}> = [
{
dimension: "token",
title: "令牌排行",
description: "按成本优先排序,识别最活跃的访问凭证。",
barClassName: "from-sky-500 to-cyan-400",
badgeClassName: "bg-sky-500/12 text-sky-600 dark:text-sky-400",
dimension: "model",
title: "模型排行",
description: "看清哪些模型是主要成本与流量入口。",
barClassName: "from-amber-500 to-orange-400",
badgeClassName: "bg-amber-500/12 text-amber-700 dark:text-amber-400",
},
{
dimension: "channel",
@@ -46,11 +46,11 @@ const BREAKDOWN_CHARTS: Array<{
badgeClassName: "bg-emerald-500/12 text-emerald-600 dark:text-emerald-400",
},
{
dimension: "model",
title: "模型排行",
description: "看清哪些模型是主要成本与流量入口。",
barClassName: "from-amber-500 to-orange-400",
badgeClassName: "bg-amber-500/12 text-amber-700 dark:text-amber-400",
dimension: "token",
title: "令牌排行",
description: "按成本优先排序,识别最活跃的访问凭证。",
barClassName: "from-sky-500 to-cyan-400",
badgeClassName: "bg-sky-500/12 text-sky-600 dark:text-sky-400",
},
{
dimension: "provider",
@@ -77,15 +77,6 @@ const formatDuration = (value: number): string => {
return `${Math.round(value)} ms`;
};
const EmptyState = ({ title, description }: { title: string; description: string }) => {
return (
<div className="rounded-xl border border-dashed bg-muted/20 px-4 py-10 text-center">
<p className="text-sm font-medium">{title}</p>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
</div>
);
};
export function Analytics() {
const [range, setRange] = useState<AnalyticsRange>("7d");
@@ -253,7 +244,10 @@ export function Analytics() {
</div>
{!trend || trend.points.length === 0 ? (
<EmptyState title="暂无趋势数据" description="写入第一批使用事件后,这里会展示完整柱状趋势。" />
<Card className="border-0 flex flex-col items-center justify-center py-16">
<p className="text-sm font-medium">暂无趋势数据</p>
<p className="mt-1 text-sm text-muted-foreground">写入第一批使用事件后,这里会展示完整柱状趋势。</p>
</Card>
) : (
<TrendBarChart points={trend.points} range={range} />
)}
+1 -1
View File
@@ -71,7 +71,7 @@ export function Dashboard() {
<Button
size="lg"
onClick={openAuthModal}
className="h-12 px-6 text-[15px] rounded-xl shadow-lg shadow-primary/20"
className="h-12 px-6 text-[15px] shadow-lg shadow-primary/20"
>
管理员登录
<ArrowRight className="h-4 w-4 ml-1" />
+1 -1
View File
@@ -24,7 +24,7 @@
{
"binding": "DB",
"database_name": "one-api-workers",
"database_id": ""
"database_id": "5d429962-32e6-4895-8d40-467237292800"
}
],
"analytics_engine_datasets": [