add:适配移动端
This commit is contained in:
1 parent
a5be552a67
commit
4ad80d8df9
8 files changed
+536
-294
No files matched your search
@@ -29,62 +29,122 @@ export const AppLayout = ({
|
||||
onTabChange,
|
||||
onLogout,
|
||||
children,
|
||||
}: AppLayoutProps) => (
|
||||
<div class="grid min-h-screen grid-cols-1 lg:grid-cols-[260px_1fr]">
|
||||
<aside class="border-b border-stone-200 bg-white px-5 py-8 lg:sticky lg:top-0 lg:h-screen lg:border-b-0 lg:border-r">
|
||||
<div class="mb-8 flex flex-col gap-1.5">
|
||||
<h2 class="font-['Space_Grotesk'] text-lg font-semibold tracking-tight text-stone-900">
|
||||
api-workers
|
||||
</h2>
|
||||
<span class="text-xs uppercase tracking-widest text-stone-500">
|
||||
console
|
||||
</span>
|
||||
</div>
|
||||
<nav class="flex flex-col gap-2.5">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
class={`flex h-11 w-full items-center rounded-xl px-3.5 py-2.5 text-left text-sm font-medium transition-all duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white ${
|
||||
activeTab === tab.id
|
||||
? "bg-stone-100 text-stone-900"
|
||||
: "text-stone-500 hover:bg-stone-100 hover:text-stone-900"
|
||||
}`}
|
||||
type="button"
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="px-6 pt-6 pb-16 sm:px-10 sm:pt-8">
|
||||
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="font-['Space_Grotesk'] text-2xl tracking-tight text-stone-900">
|
||||
{activeLabel}
|
||||
</h1>
|
||||
<p class="text-sm text-stone-500">
|
||||
集中管理渠道、模型、令牌与使用情况。
|
||||
</p>
|
||||
</div>
|
||||
}: AppLayoutProps) => {
|
||||
const closeMobileNav = () => {
|
||||
const toggle = document.querySelector<HTMLInputElement>("#app-nav-toggle");
|
||||
if (toggle) {
|
||||
toggle.checked = false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="relative flex min-h-screen flex-col lg:grid lg:grid-cols-[260px_1fr]">
|
||||
<input class="peer hidden" id="app-nav-toggle" type="checkbox" />
|
||||
<header class="flex items-center justify-between border-b border-stone-200 bg-white px-4 py-4 lg:hidden">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="rounded-full bg-stone-100 px-2.5 py-1 text-xs text-stone-500">
|
||||
<label
|
||||
aria-label="打开导航"
|
||||
class="inline-flex h-10 items-center gap-2 rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-700 shadow-sm transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
|
||||
for="app-nav-toggle"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 text-stone-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 12h16" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 18h16" />
|
||||
</svg>
|
||||
菜单
|
||||
</label>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-semibold text-stone-900">
|
||||
api-workers
|
||||
</span>
|
||||
<span class="text-xs text-stone-500">{activeLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full bg-stone-100 px-2.5 py-1 text-[10px] uppercase tracking-widest text-stone-500">
|
||||
{token ? "已登录" : "未登录"}
|
||||
</span>
|
||||
<button
|
||||
class="h-11 rounded-lg border border-stone-200 bg-transparent px-4 py-2.5 text-sm font-semibold text-stone-500 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="h-9 rounded-full border border-stone-200 bg-transparent px-3 text-xs font-semibold text-stone-500 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={onLogout}
|
||||
>
|
||||
退出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{notice && (
|
||||
<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
|
||||
{notice}
|
||||
</header>
|
||||
<aside class="fixed inset-y-0 left-0 z-40 flex w-72 -translate-x-full flex-col overflow-y-auto border-r border-stone-200 bg-white px-5 py-8 shadow-xl transition-transform duration-300 ease-in-out peer-checked:translate-x-0 lg:sticky lg:top-0 lg:z-auto lg:h-screen lg:w-auto lg:translate-x-0 lg:shadow-none">
|
||||
<div class="mb-8 flex flex-col gap-1.5">
|
||||
<h2 class="font-['Space_Grotesk'] text-lg font-semibold tracking-tight text-stone-900">
|
||||
api-workers
|
||||
</h2>
|
||||
<span class="text-xs uppercase tracking-widest text-stone-500">
|
||||
console
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
<nav class="flex flex-col gap-2.5">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
class={`flex h-11 w-full items-center rounded-xl px-3.5 py-2.5 text-left text-sm font-medium transition-all duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white ${
|
||||
activeTab === tab.id
|
||||
? "bg-stone-100 text-stone-900"
|
||||
: "text-stone-500 hover:bg-stone-100 hover:text-stone-900"
|
||||
}`}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onTabChange(tab.id);
|
||||
closeMobileNav();
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="px-4 pt-5 pb-16 sm:px-10 sm:pt-8">
|
||||
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="font-['Space_Grotesk'] text-2xl tracking-tight text-stone-900">
|
||||
{activeLabel}
|
||||
</h1>
|
||||
<p class="text-sm text-stone-500">
|
||||
集中管理渠道、模型、令牌与使用情况。
|
||||
</p>
|
||||
</div>
|
||||
<div class="hidden items-center gap-3 lg:flex">
|
||||
<span class="rounded-full bg-stone-100 px-2.5 py-1 text-xs text-stone-500">
|
||||
{token ? "已登录" : "未登录"}
|
||||
</span>
|
||||
<button
|
||||
class="h-11 rounded-lg border border-stone-200 bg-transparent px-4 py-2.5 text-sm font-semibold text-stone-500 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={onLogout}
|
||||
>
|
||||
退出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{notice && (
|
||||
<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
|
||||
{notice}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</main>
|
||||
<label
|
||||
aria-hidden="true"
|
||||
class="fixed inset-0 z-30 bg-stone-900/40 opacity-0 transition-opacity duration-300 ease-in-out peer-checked:pointer-events-auto peer-checked:opacity-100 lg:hidden pointer-events-none"
|
||||
for="app-nav-toggle"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -75,44 +75,38 @@ export const ChannelsView = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 overflow-hidden rounded-xl border border-stone-200">
|
||||
<div class="grid grid-cols-[minmax(0,1.6fr)_minmax(0,0.7fr)_minmax(0,0.6fr)_minmax(0,1.6fr)] gap-3 bg-stone-50 px-4 py-3 text-xs uppercase tracking-widest text-stone-500">
|
||||
<div>渠道</div>
|
||||
<div>状态</div>
|
||||
<div>权重</div>
|
||||
<div>操作</div>
|
||||
</div>
|
||||
{pagedChannels.length === 0 ? (
|
||||
<div class="px-4 py-10 text-center text-sm text-stone-500">
|
||||
暂无渠道,请先创建。
|
||||
</div>
|
||||
) : (
|
||||
<div class="divide-y divide-stone-100">
|
||||
{pagedChannels.map((channel) => {
|
||||
<div class="mt-4">
|
||||
<div class="space-y-3 md:hidden">
|
||||
{pagedChannels.length === 0 ? (
|
||||
<div class="rounded-xl border border-stone-200 bg-white px-4 py-10 text-center text-sm text-stone-500">
|
||||
暂无渠道,请先创建。
|
||||
</div>
|
||||
) : (
|
||||
pagedChannels.map((channel) => {
|
||||
const isActive = channel.status === "active";
|
||||
return (
|
||||
<div
|
||||
class={`grid grid-cols-[minmax(0,1.6fr)_minmax(0,0.7fr)_minmax(0,0.6fr)_minmax(0,1.6fr)] items-center gap-3 px-4 py-4 text-sm ${
|
||||
class={`rounded-xl border p-4 shadow-sm ${
|
||||
editingChannel?.id === channel.id
|
||||
? "bg-amber-50/60"
|
||||
: "bg-white"
|
||||
? "border-amber-200 bg-amber-50/60"
|
||||
: "border-stone-200 bg-white"
|
||||
}`}
|
||||
key={channel.id}
|
||||
>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<span class="truncate font-semibold text-stone-900">
|
||||
{channel.name}
|
||||
</span>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-stone-900">
|
||||
{channel.name}
|
||||
</p>
|
||||
<p
|
||||
class="truncate text-xs text-stone-500"
|
||||
title={channel.base_url}
|
||||
>
|
||||
{channel.base_url}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="truncate text-xs text-stone-500"
|
||||
title={channel.base_url}
|
||||
>
|
||||
{channel.base_url}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class={`inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold ${
|
||||
class={`inline-flex items-center rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-widest ${
|
||||
isActive
|
||||
? "border-emerald-100 bg-emerald-50 text-emerald-600"
|
||||
: "border-stone-200 bg-stone-100 text-stone-500"
|
||||
@@ -121,33 +115,36 @@ export const ChannelsView = ({
|
||||
{isActive ? "启用" : "禁用"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm font-semibold text-stone-700">
|
||||
{channel.weight}
|
||||
<div class="mt-3 flex items-center justify-between text-xs text-stone-500">
|
||||
<span>权重</span>
|
||||
<span class="font-semibold text-stone-700">
|
||||
{channel.weight}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="mt-3 grid grid-cols-2 gap-2">
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-stone-100 px-3 text-xs font-semibold text-stone-900 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="h-9 w-full rounded-full border border-stone-200 bg-stone-100 px-3 text-xs font-semibold text-stone-900 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onTest(channel.id)}
|
||||
>
|
||||
连通测试
|
||||
</button>
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-600 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="h-9 w-full rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-600 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onToggle(channel.id, channel.status)}
|
||||
>
|
||||
{isActive ? "禁用" : "启用"}
|
||||
</button>
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-600 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="h-9 w-full rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-600 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onEdit(channel)}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-500 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="h-9 w-full rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-500 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onDelete(channel.id)}
|
||||
>
|
||||
@@ -156,11 +153,96 @@ export const ChannelsView = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<div class="hidden overflow-hidden rounded-xl border border-stone-200 md:block">
|
||||
<div class="grid grid-cols-[minmax(0,1.6fr)_minmax(0,0.7fr)_minmax(0,0.6fr)_minmax(0,1.6fr)] gap-3 bg-stone-50 px-4 py-3 text-xs uppercase tracking-widest text-stone-500">
|
||||
<div>渠道</div>
|
||||
<div>状态</div>
|
||||
<div>权重</div>
|
||||
<div>操作</div>
|
||||
</div>
|
||||
)}
|
||||
{pagedChannels.length === 0 ? (
|
||||
<div class="px-4 py-10 text-center text-sm text-stone-500">
|
||||
暂无渠道,请先创建。
|
||||
</div>
|
||||
) : (
|
||||
<div class="divide-y divide-stone-100">
|
||||
{pagedChannels.map((channel) => {
|
||||
const isActive = channel.status === "active";
|
||||
return (
|
||||
<div
|
||||
class={`grid grid-cols-[minmax(0,1.6fr)_minmax(0,0.7fr)_minmax(0,0.6fr)_minmax(0,1.6fr)] items-center gap-3 px-4 py-4 text-sm ${
|
||||
editingChannel?.id === channel.id
|
||||
? "bg-amber-50/60"
|
||||
: "bg-white"
|
||||
}`}
|
||||
key={channel.id}
|
||||
>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<span class="truncate font-semibold text-stone-900">
|
||||
{channel.name}
|
||||
</span>
|
||||
<span
|
||||
class="truncate text-xs text-stone-500"
|
||||
title={channel.base_url}
|
||||
>
|
||||
{channel.base_url}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class={`inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold ${
|
||||
isActive
|
||||
? "border-emerald-100 bg-emerald-50 text-emerald-600"
|
||||
: "border-stone-200 bg-stone-100 text-stone-500"
|
||||
}`}
|
||||
>
|
||||
{isActive ? "启用" : "禁用"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm font-semibold text-stone-700">
|
||||
{channel.weight}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-stone-100 px-3 text-xs font-semibold text-stone-900 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onTest(channel.id)}
|
||||
>
|
||||
连通测试
|
||||
</button>
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-600 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onToggle(channel.id, channel.status)}
|
||||
>
|
||||
{isActive ? "禁用" : "启用"}
|
||||
</button>
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-600 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onEdit(channel)}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-500 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onDelete(channel.id)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-3 text-xs text-stone-500">
|
||||
<div class="mt-4 flex flex-col gap-3 text-xs text-stone-500 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs text-stone-500">
|
||||
共 {channelTotal} 条 · {channelTotalPages} 页
|
||||
|
||||
@@ -69,36 +69,38 @@ export const DashboardView = ({ dashboard }: DashboardViewProps) => {
|
||||
按日趋势
|
||||
</h3>
|
||||
</div>
|
||||
<table class="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
日期
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
请求
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
Tokens
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dashboard.byDay.map((row) => (
|
||||
<tr class="hover:bg-stone-50" key={row.day}>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.day}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.requests}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.tokens}
|
||||
</td>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-[520px] w-full border-collapse text-xs sm:text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
日期
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
请求
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
Tokens
|
||||
</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dashboard.byDay.map((row) => (
|
||||
<tr class="hover:bg-stone-50" key={row.day}>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.day}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.requests}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.tokens}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
@@ -106,36 +108,38 @@ export const DashboardView = ({ dashboard }: DashboardViewProps) => {
|
||||
模型排行
|
||||
</h3>
|
||||
</div>
|
||||
<table class="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
模型
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
请求
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
Tokens
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dashboard.byModel.map((row) => (
|
||||
<tr class="hover:bg-stone-50" key={row.model}>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.model ?? "-"}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.requests}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.tokens}
|
||||
</td>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-[520px] w-full border-collapse text-xs sm:text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
模型
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
请求
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
Tokens
|
||||
</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dashboard.byModel.map((row) => (
|
||||
<tr class="hover:bg-stone-50" key={row.model}>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.model ?? "-"}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.requests}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.tokens}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-5 lg:grid-cols-2">
|
||||
@@ -145,39 +149,41 @@ export const DashboardView = ({ dashboard }: DashboardViewProps) => {
|
||||
渠道贡献
|
||||
</h3>
|
||||
</div>
|
||||
<table class="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
渠道
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
请求
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
Tokens
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dashboard.byChannel.map((row) => (
|
||||
<tr
|
||||
class="hover:bg-stone-50"
|
||||
key={row.channel_name ?? "unknown"}
|
||||
>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.channel_name ?? "-"}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.requests}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.tokens}
|
||||
</td>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-[520px] w-full border-collapse text-xs sm:text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
渠道
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
请求
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
Tokens
|
||||
</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dashboard.byChannel.map((row) => (
|
||||
<tr
|
||||
class="hover:bg-stone-50"
|
||||
key={row.channel_name ?? "unknown"}
|
||||
>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.channel_name ?? "-"}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.requests}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.tokens}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-stone-200 bg-white p-5 shadow-lg">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
@@ -185,36 +191,41 @@ export const DashboardView = ({ dashboard }: DashboardViewProps) => {
|
||||
令牌贡献
|
||||
</h3>
|
||||
</div>
|
||||
<table class="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
令牌
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
请求
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
Tokens
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dashboard.byToken.map((row) => (
|
||||
<tr class="hover:bg-stone-50" key={row.token_name ?? "unknown"}>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.token_name ?? "-"}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.requests}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{row.tokens}
|
||||
</td>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-[520px] w-full border-collapse text-xs sm:text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
令牌
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
请求
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
Tokens
|
||||
</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dashboard.byToken.map((row) => (
|
||||
<tr
|
||||
class="hover:bg-stone-50"
|
||||
key={row.token_name ?? "unknown"}
|
||||
>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.token_name ?? "-"}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.requests}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{row.tokens}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,29 +23,31 @@ export const ModelsView = ({ models }: ModelsViewProps) => (
|
||||
{models.length} 个模型
|
||||
</span>
|
||||
</div>
|
||||
<table class="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
模型
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
渠道
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{models.map((model) => (
|
||||
<tr class="hover:bg-stone-50" key={model.id}>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{model.id}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
{model.channels.map((channel) => channel.name).join(" / ")}
|
||||
</td>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-[420px] w-full border-collapse text-xs sm:text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
模型
|
||||
</th>
|
||||
<th class="border-b border-stone-200 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
渠道
|
||||
</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{models.map((model) => (
|
||||
<tr class="hover:bg-stone-50" key={model.id}>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{model.id}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{model.channels.map((channel) => channel.name).join(" / ")}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -68,36 +68,31 @@ export const TokensView = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 overflow-hidden rounded-xl border border-stone-200">
|
||||
<div class="grid grid-cols-[minmax(0,1.2fr)_minmax(0,0.6fr)_minmax(0,0.9fr)_minmax(0,0.6fr)_minmax(0,1fr)_minmax(0,1.2fr)] gap-3 bg-stone-50 px-4 py-3 text-xs uppercase tracking-widest text-stone-500">
|
||||
<div>名称</div>
|
||||
<div>状态</div>
|
||||
<div>已用/额度</div>
|
||||
<div>前缀</div>
|
||||
<div>创建时间</div>
|
||||
<div>操作</div>
|
||||
</div>
|
||||
{pagedTokens.length === 0 ? (
|
||||
<div class="px-4 py-10 text-center text-sm text-stone-500">
|
||||
暂无令牌,请先创建。
|
||||
</div>
|
||||
) : (
|
||||
<div class="divide-y divide-stone-100">
|
||||
{pagedTokens.map((tokenItem) => {
|
||||
<div class="mt-4">
|
||||
<div class="space-y-3 md:hidden">
|
||||
{pagedTokens.length === 0 ? (
|
||||
<div class="rounded-xl border border-stone-200 bg-white px-4 py-10 text-center text-sm text-stone-500">
|
||||
暂无令牌,请先创建。
|
||||
</div>
|
||||
) : (
|
||||
pagedTokens.map((tokenItem) => {
|
||||
const isActive = tokenItem.status === "active";
|
||||
return (
|
||||
<div
|
||||
class="grid grid-cols-[minmax(0,1.2fr)_minmax(0,0.6fr)_minmax(0,0.9fr)_minmax(0,0.6fr)_minmax(0,1fr)_minmax(0,1.2fr)] items-center gap-3 px-4 py-4 text-sm"
|
||||
class="rounded-xl border border-stone-200 bg-white p-4 shadow-sm"
|
||||
key={tokenItem.id}
|
||||
>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<span class="truncate font-semibold text-stone-900">
|
||||
{tokenItem.name}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-stone-900">
|
||||
{tokenItem.name}
|
||||
</p>
|
||||
<p class="text-xs text-stone-500">
|
||||
前缀 {tokenItem.key_prefix ?? "-"}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class={`inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold ${
|
||||
class={`inline-flex items-center rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-widest ${
|
||||
isActive
|
||||
? "border-emerald-100 bg-emerald-50 text-emerald-600"
|
||||
: "border-stone-200 bg-stone-100 text-stone-500"
|
||||
@@ -106,32 +101,38 @@ export const TokensView = ({
|
||||
{isActive ? "启用" : "禁用"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm font-semibold text-stone-700">
|
||||
{tokenItem.quota_used} / {tokenItem.quota_total ?? "∞"}
|
||||
<div class="mt-3 grid grid-cols-2 gap-2 text-xs text-stone-500">
|
||||
<div class="rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
|
||||
<p>已用/额度</p>
|
||||
<p class="mt-1 font-semibold text-stone-700">
|
||||
{tokenItem.quota_used} /{" "}
|
||||
{tokenItem.quota_total ?? "∞"}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
|
||||
<p>创建时间</p>
|
||||
<p class="mt-1 font-semibold text-stone-700">
|
||||
{formatDateTime(tokenItem.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-stone-700">
|
||||
{tokenItem.key_prefix ?? "-"}
|
||||
</div>
|
||||
<div class="text-sm text-stone-700">
|
||||
{formatDateTime(tokenItem.created_at)}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="mt-3 grid grid-cols-2 gap-2">
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-stone-100 px-3 text-xs font-semibold text-stone-900 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="h-9 w-full rounded-full border border-stone-200 bg-stone-100 px-3 text-xs font-semibold text-stone-900 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onReveal(tokenItem.id)}
|
||||
>
|
||||
查看
|
||||
</button>
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-600 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="h-9 w-full rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-600 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onToggle(tokenItem.id, tokenItem.status)}
|
||||
>
|
||||
切换
|
||||
</button>
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-500 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="col-span-2 h-9 w-full rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-500 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onDelete(tokenItem.id)}
|
||||
>
|
||||
@@ -140,11 +141,87 @@ export const TokensView = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<div class="hidden overflow-hidden rounded-xl border border-stone-200 md:block">
|
||||
<div class="grid grid-cols-[minmax(0,1.2fr)_minmax(0,0.6fr)_minmax(0,0.9fr)_minmax(0,0.6fr)_minmax(0,1fr)_minmax(0,1.2fr)] gap-3 bg-stone-50 px-4 py-3 text-xs uppercase tracking-widest text-stone-500">
|
||||
<div>名称</div>
|
||||
<div>状态</div>
|
||||
<div>已用/额度</div>
|
||||
<div>前缀</div>
|
||||
<div>创建时间</div>
|
||||
<div>操作</div>
|
||||
</div>
|
||||
)}
|
||||
{pagedTokens.length === 0 ? (
|
||||
<div class="px-4 py-10 text-center text-sm text-stone-500">
|
||||
暂无令牌,请先创建。
|
||||
</div>
|
||||
) : (
|
||||
<div class="divide-y divide-stone-100">
|
||||
{pagedTokens.map((tokenItem) => {
|
||||
const isActive = tokenItem.status === "active";
|
||||
return (
|
||||
<div
|
||||
class="grid grid-cols-[minmax(0,1.2fr)_minmax(0,0.6fr)_minmax(0,0.9fr)_minmax(0,0.6fr)_minmax(0,1fr)_minmax(0,1.2fr)] items-center gap-3 px-4 py-4 text-sm"
|
||||
key={tokenItem.id}
|
||||
>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<span class="truncate font-semibold text-stone-900">
|
||||
{tokenItem.name}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class={`inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold ${
|
||||
isActive
|
||||
? "border-emerald-100 bg-emerald-50 text-emerald-600"
|
||||
: "border-stone-200 bg-stone-100 text-stone-500"
|
||||
}`}
|
||||
>
|
||||
{isActive ? "启用" : "禁用"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm font-semibold text-stone-700">
|
||||
{tokenItem.quota_used} / {tokenItem.quota_total ?? "∞"}
|
||||
</div>
|
||||
<div class="text-sm text-stone-700">
|
||||
{tokenItem.key_prefix ?? "-"}
|
||||
</div>
|
||||
<div class="text-sm text-stone-700">
|
||||
{formatDateTime(tokenItem.created_at)}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-stone-100 px-3 text-xs font-semibold text-stone-900 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onReveal(tokenItem.id)}
|
||||
>
|
||||
查看
|
||||
</button>
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-600 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onToggle(tokenItem.id, tokenItem.status)}
|
||||
>
|
||||
切换
|
||||
</button>
|
||||
<button
|
||||
class="h-9 rounded-full border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-500 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:text-stone-900 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
type="button"
|
||||
onClick={() => onDelete(tokenItem.id)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-3 text-xs text-stone-500">
|
||||
<div class="mt-4 flex flex-col gap-3 text-xs text-stone-500 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs text-stone-500">
|
||||
共 {tokenTotal} 条 · {tokenTotalPages} 页
|
||||
|
||||
@@ -86,41 +86,41 @@ export const UsageView = ({ usage, onRefresh }: UsageViewProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 overflow-hidden rounded-xl border border-stone-200">
|
||||
<div class="h-[520px] overflow-auto">
|
||||
<table class="min-w-[1100px] w-full border-collapse text-sm">
|
||||
<div class="h-[420px] overflow-auto sm:h-[520px]">
|
||||
<table class="min-w-[960px] w-full border-collapse text-xs sm:text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
时间
|
||||
</th>
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
模型
|
||||
</th>
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
渠道
|
||||
</th>
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
令牌
|
||||
</th>
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
输入 Tokens
|
||||
</th>
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
输出 Tokens
|
||||
</th>
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
用时
|
||||
</th>
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
首 token 延迟
|
||||
</th>
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
流式
|
||||
</th>
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
推理强度
|
||||
</th>
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-xs uppercase tracking-widest text-stone-500">
|
||||
<th class="sticky top-0 border-b border-stone-200 bg-stone-50 px-3 py-2.5 text-left text-[10px] uppercase tracking-widest text-stone-500 sm:text-xs">
|
||||
状态
|
||||
</th>
|
||||
</tr>
|
||||
@@ -138,37 +138,37 @@ export const UsageView = ({ usage, onRefresh }: UsageViewProps) => {
|
||||
) : (
|
||||
pagedUsage.map((log) => (
|
||||
<tr class="hover:bg-stone-50" key={log.id}>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{formatDateTime(log.created_at)}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{log.model ?? "-"}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{log.channel_name ?? log.channel_id ?? "-"}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{log.token_name ?? log.token_id ?? "-"}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{formatTokens(log.prompt_tokens)}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{formatTokens(log.completion_tokens)}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{formatSeconds(log.latency_ms)}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{formatSeconds(log.first_token_latency_ms)}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{formatStream(log.stream)}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{log.reasoning_effort ?? "-"}
|
||||
</td>
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-sm text-stone-700">
|
||||
<td class="border-b border-stone-200 px-3 py-2.5 text-left text-xs text-stone-700 sm:text-sm">
|
||||
{log.status}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -178,7 +178,7 @@ export const UsageView = ({ usage, onRefresh }: UsageViewProps) => {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-3 text-xs text-stone-500">
|
||||
<div class="mt-4 flex flex-col gap-3 text-xs text-stone-500 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs text-stone-500">共 {totalPages} 页</span>
|
||||
<button
|
||||
|
||||
@@ -86,6 +86,13 @@
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/worker/src/routes/proxy.ts
|
||||
|
||||
## [0.4.11] - 2026-02-22
|
||||
|
||||
### 变更
|
||||
|
||||
- **[admin-ui]**: 管理台移动端适配(导航抽屉、列表卡片、表格横向滚动)
|
||||
- 方案: [202602220036_mobile-ui](archive/2026-02/202602220036_mobile-ui/)
|
||||
|
||||
## [0.4.10] - 2026-02-21
|
||||
|
||||
### 变更
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
- 令牌管理采用列表视图(默认每页 10 条),生成令牌使用弹窗表单,并展示创建时间与前缀
|
||||
- 列表分页页脚统一使用页码导航 + 每页条数选择,并显示总页数,顶部不显示条数与每页选择
|
||||
- Token 仅本地保存,不回显
|
||||
- 移动端使用顶部栏 + 抽屉导航,侧边栏在桌面端保持固定
|
||||
- 渠道/令牌列表在移动端以卡片形式展示,操作区改为网格按钮排列
|
||||
- 数据面板、模型广场、使用日志表格在移动端支持横向滚动与字号收紧
|
||||
|
||||
## 依赖关系
|
||||
- `auth` / `channels` / `models` / `tokens` / `usage` / `dashboard` / `settings` 模块
|
||||
|
||||
Reference in new issue
Block a user