add:适配移动端

This commit is contained in:
lsy committed 2026-02-22 00:47:51 +08:00
1 parent a5be552a67
commit 4ad80d8df9
8 files changed
+536 -294

No files matched your search

+108 -48
View File
@@ -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>
);
};
+122 -40
View File
@@ -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} 页
+130 -119
View File
@@ -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>
+25 -23
View File
@@ -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>
);
+116 -39
View File
@@ -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} 页
+25 -25
View File
@@ -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
+7
View File
@@ -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
### 变更
+3
View File
@@ -21,6 +21,9 @@
- 令牌管理采用列表视图(默认每页 10 条),生成令牌使用弹窗表单,并展示创建时间与前缀
- 列表分页页脚统一使用页码导航 + 每页条数选择,并显示总页数,顶部不显示条数与每页选择
- Token 仅本地保存,不回显
- 移动端使用顶部栏 + 抽屉导航,侧边栏在桌面端保持固定
- 渠道/令牌列表在移动端以卡片形式展示,操作区改为网格按钮排列
- 数据面板、模型广场、使用日志表格在移动端支持横向滚动与字号收紧
## 依赖关系
- `auth` / `channels` / `models` / `tokens` / `usage` / `dashboard` / `settings` 模块