fix:将ui改成扁平化ui,优化ui错误
This commit is contained in:
1 parent
d87610151c
commit
d6aa20e85b
36 files changed
+4023
-2973
No files matched your search
+30
-35
@@ -485,29 +485,29 @@ const App = () => {
|
||||
|
||||
const handleDashboardApply = useCallback(
|
||||
async (override?: DashboardQuery) => {
|
||||
const actionKey = buildActionKey("dashboard:filter");
|
||||
if (isActionPending(actionKey)) {
|
||||
return;
|
||||
}
|
||||
const nextQuery = override ?? dashboardQuery;
|
||||
startAction(actionKey);
|
||||
try {
|
||||
await loadDashboard(nextQuery);
|
||||
pushNotice("success", "筛选已更新");
|
||||
} catch (error) {
|
||||
pushNotice("error", (error as Error).message);
|
||||
} finally {
|
||||
endAction(actionKey);
|
||||
}
|
||||
},
|
||||
[
|
||||
dashboardQuery,
|
||||
endAction,
|
||||
isActionPending,
|
||||
loadDashboard,
|
||||
pushNotice,
|
||||
startAction,
|
||||
],
|
||||
const actionKey = buildActionKey("dashboard:filter");
|
||||
if (isActionPending(actionKey)) {
|
||||
return;
|
||||
}
|
||||
const nextQuery = override ?? dashboardQuery;
|
||||
startAction(actionKey);
|
||||
try {
|
||||
await loadDashboard(nextQuery);
|
||||
pushNotice("success", "筛选已更新");
|
||||
} catch (error) {
|
||||
pushNotice("error", (error as Error).message);
|
||||
} finally {
|
||||
endAction(actionKey);
|
||||
}
|
||||
},
|
||||
[
|
||||
dashboardQuery,
|
||||
endAction,
|
||||
isActionPending,
|
||||
loadDashboard,
|
||||
pushNotice,
|
||||
startAction,
|
||||
],
|
||||
);
|
||||
|
||||
const loadSites = useCallback(async () => {
|
||||
@@ -667,16 +667,14 @@ const App = () => {
|
||||
proxy_upstream_timeout_ms: String(
|
||||
runtimeSettings?.upstream_timeout_ms ?? 30000,
|
||||
),
|
||||
proxy_stream_usage_mode:
|
||||
runtimeSettings?.stream_usage_mode ?? "full",
|
||||
proxy_stream_usage_mode: runtimeSettings?.stream_usage_mode ?? "full",
|
||||
proxy_stream_usage_max_bytes: String(
|
||||
runtimeSettings?.stream_usage_max_bytes ?? 0,
|
||||
),
|
||||
proxy_stream_usage_max_parsers: String(
|
||||
runtimeSettings?.stream_usage_max_parsers ?? 0,
|
||||
),
|
||||
proxy_usage_queue_enabled:
|
||||
runtimeSettings?.usage_queue_enabled ?? true,
|
||||
proxy_usage_queue_enabled: runtimeSettings?.usage_queue_enabled ?? true,
|
||||
usage_queue_daily_limit: String(
|
||||
runtimeSettings?.usage_queue_daily_limit ?? 10000,
|
||||
),
|
||||
@@ -1303,17 +1301,16 @@ const App = () => {
|
||||
settingsForm.model_failure_cooldown_minutes,
|
||||
);
|
||||
const upstreamTimeoutMs = Number(settingsForm.proxy_upstream_timeout_ms);
|
||||
const streamUsageMode =
|
||||
settingsForm.proxy_stream_usage_mode.trim().toLowerCase();
|
||||
const streamUsageMode = settingsForm.proxy_stream_usage_mode
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const streamUsageMaxBytes = Number(
|
||||
settingsForm.proxy_stream_usage_max_bytes,
|
||||
);
|
||||
const streamUsageMaxParsers = Number(
|
||||
settingsForm.proxy_stream_usage_max_parsers,
|
||||
);
|
||||
const usageQueueDailyLimit = Number(
|
||||
settingsForm.usage_queue_daily_limit,
|
||||
);
|
||||
const usageQueueDailyLimit = Number(settingsForm.usage_queue_daily_limit);
|
||||
const usageQueueDirectRatio = Number(
|
||||
settingsForm.usage_queue_direct_write_ratio,
|
||||
);
|
||||
@@ -1327,9 +1324,7 @@ const App = () => {
|
||||
const cacheCallTokensTtl = Number(
|
||||
settingsForm.cache_ttl_call_tokens_seconds,
|
||||
);
|
||||
const cacheSettingsTtl = Number(
|
||||
settingsForm.cache_ttl_settings_seconds,
|
||||
);
|
||||
const cacheSettingsTtl = Number(settingsForm.cache_ttl_settings_seconds);
|
||||
if (
|
||||
Number.isNaN(retention) ||
|
||||
retention < 1 ||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "hono/jsx/dom";
|
||||
import { cx, type ClassName } from "./utils";
|
||||
import { type ClassName, cx } from "./utils";
|
||||
|
||||
export type AreaChartPoint = {
|
||||
label: string;
|
||||
@@ -111,7 +111,10 @@ export const AreaChart = ({
|
||||
}
|
||||
const maxValue = Math.max(...data.map((item) => item.value), 1);
|
||||
const drawableWidth = Math.max(1, viewBoxWidth - paddingX * 2);
|
||||
const drawableHeight = Math.max(1, viewBoxHeight - paddingTop - paddingBottom);
|
||||
const drawableHeight = Math.max(
|
||||
1,
|
||||
viewBoxHeight - paddingTop - paddingBottom,
|
||||
);
|
||||
return data.map((item, index) => {
|
||||
const x =
|
||||
data.length === 1
|
||||
@@ -143,7 +146,8 @@ export const AreaChart = ({
|
||||
const horizontalGridLines = useMemo(
|
||||
() =>
|
||||
[0.2, 0.4, 0.6, 0.8].map(
|
||||
(step) => paddingTop + step * (viewBoxHeight - paddingTop - paddingBottom),
|
||||
(step) =>
|
||||
paddingTop + step * (viewBoxHeight - paddingTop - paddingBottom),
|
||||
),
|
||||
[paddingBottom, paddingTop, viewBoxHeight],
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "hono/jsx/dom";
|
||||
import { Button } from "./button";
|
||||
import { Popover, PopoverContent } from "./popover";
|
||||
import { cx, type ClassName } from "./utils";
|
||||
import { type ClassName, cx } from "./utils";
|
||||
|
||||
export type ColumnOption = {
|
||||
id: string;
|
||||
@@ -77,7 +77,10 @@ export const ColumnPicker = ({
|
||||
{label}
|
||||
</Button>
|
||||
<Popover open={open}>
|
||||
<PopoverContent class="right-0 w-64 p-3 app-popover-content--spaced">
|
||||
<PopoverContent
|
||||
class="right-0 p-3 app-popover-content--spaced app-column-picker-popover"
|
||||
style="width:176px;min-width:176px;max-width:176px;"
|
||||
>
|
||||
<div class="text-xs font-semibold uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
显示列
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { JSX } from "hono/jsx";
|
||||
import { createPortal } from "hono/jsx/dom";
|
||||
import { cx } from "./utils";
|
||||
|
||||
export type DialogProps = {
|
||||
@@ -7,22 +8,32 @@ export type DialogProps = {
|
||||
children?: unknown;
|
||||
};
|
||||
|
||||
export const Dialog = ({ open, onClose, children }: DialogProps) => {
|
||||
export const Dialog = ({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
}: DialogProps): JSX.Element | null => {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
if (typeof document === "undefined") {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div class="fixed inset-0 z-50">
|
||||
<button
|
||||
aria-label="关闭弹窗"
|
||||
class="absolute inset-0 bg-slate-950/60"
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div class="relative z-10 flex min-h-screen items-center justify-center px-4 py-8">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{createPortal(
|
||||
<div class="app-dialog-root">
|
||||
<button
|
||||
aria-label="关闭弹窗"
|
||||
class="app-dialog-backdrop"
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div class="app-dialog-frame">{children}</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,7 +43,7 @@ export const DialogContent = ({
|
||||
}: JSX.IntrinsicElements["div"]) => (
|
||||
<div
|
||||
{...props}
|
||||
class={cx("app-card w-full max-w-xl p-6", className)}
|
||||
class={cx("app-card app-dialog-content w-full max-w-xl p-6", className)}
|
||||
role="dialog"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "hono/jsx/dom";
|
||||
import { Input } from "./input";
|
||||
import { PopoverContent } from "./popover";
|
||||
import { cx, type ClassName } from "./utils";
|
||||
import { type ClassName, cx } from "./utils";
|
||||
|
||||
export type MultiSelectOption = {
|
||||
value: string;
|
||||
@@ -124,7 +124,10 @@ export const MultiSelect = ({
|
||||
>
|
||||
<button
|
||||
aria-expanded={open}
|
||||
class={cx("app-multi-select app-focus", open && "app-multi-select--open")}
|
||||
class={cx(
|
||||
"app-multi-select app-focus",
|
||||
open && "app-multi-select--open",
|
||||
)}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from "./button";
|
||||
import { cx, type ClassName } from "./utils";
|
||||
import { type ClassName, cx } from "./utils";
|
||||
|
||||
export type PaginationItem = number | "ellipsis";
|
||||
|
||||
|
||||
@@ -12,5 +12,10 @@ export const Select = ({
|
||||
}: SelectProps) => {
|
||||
const variantClass =
|
||||
variant === "pill" ? "app-input app-input--pill" : "app-input";
|
||||
return <select {...props} class={cx(variantClass, "app-focus", className)} />;
|
||||
return (
|
||||
<select
|
||||
{...props}
|
||||
class={cx(variantClass, "app-select app-focus", className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -3,5 +3,7 @@ type ClassInput = ClassName | Promise<string>;
|
||||
|
||||
export const cx = (...classes: ClassInput[]) =>
|
||||
classes
|
||||
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
||||
.filter(
|
||||
(value): value is string => typeof value === "string" && value.length > 0,
|
||||
)
|
||||
.join(" ");
|
||||
@@ -67,7 +67,7 @@ export const AppLayout = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="relative flex min-h-screen flex-col lg:grid lg:grid-cols-[260px_1fr]">
|
||||
<div class="relative flex min-h-screen flex-col lg:grid lg:grid-cols-[300px_1fr]">
|
||||
<input class="peer hidden" id="app-nav-toggle" type="checkbox" />
|
||||
<header class="app-bar flex items-center justify-between px-4 py-4 lg:hidden">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -129,12 +129,14 @@ export const AppLayout = ({
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<aside class="app-sidebar fixed inset-y-0 left-0 z-40 flex w-72 -translate-x-full flex-col overflow-y-auto 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="app-title text-lg">api-workers</h2>
|
||||
<span class="text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
console
|
||||
</span>
|
||||
<aside class="app-sidebar fixed inset-y-0 left-0 z-40 flex w-[18.5rem] max-w-[86vw] -translate-x-full flex-col overflow-y-auto rounded-r-[28px] px-5 py-6 shadow-xl transition-transform duration-300 ease-in-out peer-checked:translate-x-0 lg:sticky lg:top-5 lg:z-10 lg:mx-5 lg:my-5 lg:h-[calc(100vh-40px)] lg:w-auto lg:translate-x-0 lg:rounded-[30px] lg:shadow-none">
|
||||
<div class="mb-8 flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<h2 class="app-title text-[21px]">api-workers</h2>
|
||||
<span class="text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
control center
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex flex-col gap-2.5">
|
||||
{tabs.map((tab) => (
|
||||
@@ -152,7 +154,7 @@ export const AppLayout = ({
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div class="mt-auto hidden w-full items-center justify-between gap-2 rounded-2xl border border-[color:var(--app-border)] bg-white/70 px-3 py-2 text-xs text-[color:var(--app-ink-muted)] lg:flex">
|
||||
<div class="mt-auto hidden w-full items-center justify-between gap-2 rounded-[18px] border border-[color:var(--app-border)] bg-[color:var(--app-surface-muted)] px-3 py-2 text-xs text-[color:var(--app-ink-muted)] lg:flex">
|
||||
<span>{token ? "已登录" : "未登录"}</span>
|
||||
<Button
|
||||
class="h-8 px-3 text-[11px]"
|
||||
@@ -165,8 +167,8 @@ export const AppLayout = ({
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="px-4 pt-5 pb-16 sm:px-10 sm:pt-8">
|
||||
{children}
|
||||
<main class="px-4 pt-5 pb-16 sm:px-10 sm:pt-8 lg:pt-5 lg:pl-0 lg:pr-8">
|
||||
<div class="animate-fade-up">{children}</div>
|
||||
</main>
|
||||
{notices.length > 0 && (
|
||||
<ToastViewport aria-live="polite">
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2,9 +2,9 @@ import { Button, Card, Input } from "../components/ui";
|
||||
import type { NoticeMessage } from "../core/types";
|
||||
|
||||
type LoginViewProps = {
|
||||
notice: NoticeMessage | null;
|
||||
isSubmitting: boolean;
|
||||
onSubmit: (event: Event) => void;
|
||||
notice: NoticeMessage | null;
|
||||
isSubmitting: boolean;
|
||||
onSubmit: (event: Event) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -17,55 +17,53 @@ type LoginViewProps = {
|
||||
* Login JSX element.
|
||||
*/
|
||||
export const LoginView = ({
|
||||
notice,
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
notice,
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
}: LoginViewProps) => {
|
||||
const toneStyles: Record<NoticeMessage["tone"], string> = {
|
||||
success: "app-notice app-notice--success",
|
||||
warning: "app-notice app-notice--warning",
|
||||
error: "app-notice app-notice--error",
|
||||
info: "app-notice app-notice--info",
|
||||
};
|
||||
return (
|
||||
<Card class="animate-fade-up mx-auto mt-24 max-w-md p-8">
|
||||
<h1 class="app-title mb-2 text-2xl">api-workers</h1>
|
||||
<p class="text-sm text-[color:var(--app-ink-muted)]">
|
||||
请输入管理员密码登录管理台。
|
||||
</p>
|
||||
<form
|
||||
class="mt-6 grid gap-4"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]"
|
||||
for="password"
|
||||
>
|
||||
管理员密码
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "登录中..." : "登录"}
|
||||
</Button>
|
||||
</form>
|
||||
<p class="mt-3 text-xs text-[color:var(--app-ink-muted)]">
|
||||
按回车键可快速提交。
|
||||
</p>
|
||||
{notice && (
|
||||
<div class={`mt-4 ${toneStyles[notice.tone]}`}>{notice.message}</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
const toneStyles: Record<NoticeMessage["tone"], string> = {
|
||||
success: "app-notice app-notice--success",
|
||||
warning: "app-notice app-notice--warning",
|
||||
error: "app-notice app-notice--error",
|
||||
info: "app-notice app-notice--info",
|
||||
};
|
||||
return (
|
||||
<div class="flex min-h-screen items-center justify-center px-4 py-10">
|
||||
<Card class="app-login-card animate-fade-up p-8 sm:p-10">
|
||||
<div class="app-login-badge mb-4 w-max">
|
||||
<span aria-hidden="true" class="app-login-badge__dot" />
|
||||
SwiftUI Style
|
||||
</div>
|
||||
<h1 class="app-title mb-2 text-3xl">api-workers</h1>
|
||||
<p class="text-sm text-[color:var(--app-ink-muted)]">
|
||||
欢迎回来,输入管理员密码进入控制台。
|
||||
</p>
|
||||
<form class="mt-7 grid gap-4" onSubmit={onSubmit}>
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]"
|
||||
for="password"
|
||||
>
|
||||
管理员密码
|
||||
</label>
|
||||
<Input id="password" name="password" type="password" required />
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "登录中..." : "登录"}
|
||||
</Button>
|
||||
</form>
|
||||
<p class="mt-4 text-xs text-[color:var(--app-ink-muted)]">
|
||||
支持回车快捷提交。
|
||||
</p>
|
||||
{notice && (
|
||||
<div class={`mt-4 ${toneStyles[notice.tone]}`}>{notice.message}</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
ColumnPicker,
|
||||
MultiSelect,
|
||||
Pagination,
|
||||
Select,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -87,8 +86,7 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
}, [models]);
|
||||
const filteredModels = useMemo(() => {
|
||||
const modelSet =
|
||||
modelFilters.length > 0 ? new Set(modelFilters) : null;
|
||||
const modelSet = modelFilters.length > 0 ? new Set(modelFilters) : null;
|
||||
const channelSet =
|
||||
channelFilters.length > 0 ? new Set(channelFilters) : null;
|
||||
return models.filter((model) => {
|
||||
@@ -134,9 +132,9 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
|
||||
<Card variant="compact" class="app-layer-raised space-y-3 p-4">
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
模型
|
||||
</label>
|
||||
</p>
|
||||
<MultiSelect
|
||||
class="w-full"
|
||||
options={modelOptions}
|
||||
@@ -148,9 +146,9 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
渠道
|
||||
</label>
|
||||
</p>
|
||||
<MultiSelect
|
||||
class="w-full"
|
||||
options={channelOptions}
|
||||
@@ -190,18 +188,18 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
|
||||
</TableRow>
|
||||
) : (
|
||||
pagedModels.map((model) => (
|
||||
<TableRow key={model.id}>
|
||||
{visibleColumnSet.has("model") && (
|
||||
<TableCell>{model.id}</TableCell>
|
||||
)}
|
||||
{visibleColumnSet.has("channels") && (
|
||||
<TableCell>
|
||||
{model.channels
|
||||
.map((channel) => channel.name)
|
||||
.join(" / ")}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
<TableRow key={model.id}>
|
||||
{visibleColumnSet.has("model") && (
|
||||
<TableCell>{model.id}</TableCell>
|
||||
)}
|
||||
{visibleColumnSet.has("channels") && (
|
||||
<TableCell>
|
||||
{model.channels
|
||||
.map((channel) => channel.name)
|
||||
.join(" / ")}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
@@ -209,7 +207,7 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
|
||||
</div>
|
||||
)}
|
||||
{models.length > 0 && (
|
||||
<div class="flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="app-pagination-bar flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs text-[color:var(--app-ink-muted)]">
|
||||
共 {filteredModels.length} 条 · {totalPages} 页
|
||||
@@ -221,28 +219,26 @@ export const ModelsView = ({ models }: ModelsViewProps) => {
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
<label class="app-page-size" for="model-page-size">
|
||||
<span>每页条数</span>
|
||||
<Select
|
||||
variant="pill"
|
||||
class="w-auto text-xs app-page-size__select"
|
||||
id="model-page-size"
|
||||
value={String(pageSize)}
|
||||
onChange={(event) => {
|
||||
const next = Number(
|
||||
(event.currentTarget as HTMLSelectElement).value,
|
||||
);
|
||||
persistPageSizePref("pageSize:models", next);
|
||||
setPageSize(next);
|
||||
}}
|
||||
>
|
||||
<div class="app-page-size-control">
|
||||
<span class="app-page-size-control__label">每页</span>
|
||||
<div class="app-page-size-control__chips">
|
||||
{pageSizeOptions.map((size) => (
|
||||
<option key={size} value={String(size)}>
|
||||
<button
|
||||
class={`app-page-size-chip ${
|
||||
pageSize === size ? "app-page-size-chip--active" : ""
|
||||
}`}
|
||||
key={size}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
persistPageSizePref("pageSize:models", size);
|
||||
setPageSize(size);
|
||||
}}
|
||||
>
|
||||
{size}
|
||||
</option>
|
||||
</button>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+1022
-1014
File diff suppressed because it is too large.
Load diff
@@ -157,7 +157,7 @@ export const TokensView = ({
|
||||
}, [isTokenModalOpen, onCloseModal]);
|
||||
return (
|
||||
<div class="space-y-5">
|
||||
<div class="animate-fade-up space-y-4">
|
||||
<div class="app-panel animate-fade-up space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="app-title text-lg">令牌列表</h3>
|
||||
@@ -181,7 +181,7 @@ export const TokensView = ({
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="space-y-3 md:hidden">
|
||||
<div class="app-mobile-stack space-y-3 md:hidden">
|
||||
{pagedTokens.length === 0 ? (
|
||||
<Card class="text-center text-sm text-[color:var(--app-ink-muted)]">
|
||||
<p>暂无令牌,请先创建。</p>
|
||||
@@ -300,9 +300,9 @@ export const TokensView = ({
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<div class="app-surface hidden overflow-hidden md:block">
|
||||
<div class="app-surface app-list-shell hidden overflow-hidden md:block">
|
||||
<div
|
||||
class="grid gap-3 bg-white/60 px-4 py-3 text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]"
|
||||
class="app-list-header grid gap-3 px-4 py-3 text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]"
|
||||
style={`grid-template-columns: ${tokenGridTemplate};`}
|
||||
>
|
||||
{visibleColumnSet.has("name") && <div>名称</div>}
|
||||
@@ -315,7 +315,7 @@ export const TokensView = ({
|
||||
{visibleColumnSet.has("actions") && <div>操作</div>}
|
||||
</div>
|
||||
{pagedTokens.length === 0 ? (
|
||||
<div class="px-4 py-10 text-center text-sm text-[color:var(--app-ink-muted)]">
|
||||
<div class="app-list-empty px-4 py-10 text-center text-sm text-[color:var(--app-ink-muted)]">
|
||||
<p>暂无令牌,请先创建。</p>
|
||||
<Button
|
||||
class="mt-4 h-9 px-4 text-xs"
|
||||
@@ -328,7 +328,7 @@ export const TokensView = ({
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div class="divide-y divide-white/60">
|
||||
<div class="app-list-body divide-y divide-white/60">
|
||||
{pagedTokens.map((tokenItem) => {
|
||||
const isActive = tokenItem.status === "active";
|
||||
const revealPending = isActionPending(
|
||||
@@ -342,7 +342,7 @@ export const TokensView = ({
|
||||
);
|
||||
return (
|
||||
<div
|
||||
class="grid items-center gap-3 px-4 py-4 text-sm"
|
||||
class="app-list-row grid items-center gap-3 px-4 py-4 text-sm"
|
||||
key={tokenItem.id}
|
||||
style={`grid-template-columns: ${tokenGridTemplate};`}
|
||||
>
|
||||
@@ -443,7 +443,7 @@ export const TokensView = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="app-pagination-bar flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs text-[color:var(--app-ink-muted)]">
|
||||
共 {tokenTotal} 条 · {displayPages} 页
|
||||
@@ -455,26 +455,23 @@ export const TokensView = ({
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
<label class="app-page-size" for="token-page-size">
|
||||
<span>每页条数</span>
|
||||
<Select
|
||||
variant="pill"
|
||||
class="w-auto text-xs app-page-size__select"
|
||||
id="token-page-size"
|
||||
value={String(tokenPageSize)}
|
||||
onChange={(event) => {
|
||||
onPageSizeChange(
|
||||
Number((event.currentTarget as HTMLSelectElement).value),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div class="app-page-size-control">
|
||||
<span class="app-page-size-control__label">每页</span>
|
||||
<div class="app-page-size-control__chips">
|
||||
{pageSizeOptions.map((size) => (
|
||||
<option key={size} value={String(size)}>
|
||||
<button
|
||||
class={`app-page-size-chip ${
|
||||
tokenPageSize === size ? "app-page-size-chip--active" : ""
|
||||
}`}
|
||||
key={size}
|
||||
type="button"
|
||||
onClick={() => onPageSizeChange(size)}
|
||||
>
|
||||
{size}
|
||||
</option>
|
||||
</button>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isTokenModalOpen && (
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Input,
|
||||
MultiSelect,
|
||||
Pagination,
|
||||
Select,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
} from "../components/ui";
|
||||
@@ -203,7 +202,7 @@ export const UsageView = ({
|
||||
|
||||
return (
|
||||
<div class="space-y-5">
|
||||
<div class="animate-fade-up space-y-4">
|
||||
<div class="app-panel animate-fade-up space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="app-title text-lg">使用日志</h3>
|
||||
@@ -221,7 +220,10 @@ export const UsageView = ({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Card variant="compact" class="app-layer-raised space-y-3 p-4">
|
||||
<Card
|
||||
variant="compact"
|
||||
class="app-layer-raised app-toolbar-card space-y-3 p-4"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="text-xs font-semibold uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
筛选模块
|
||||
@@ -270,9 +272,9 @@ export const UsageView = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
渠道
|
||||
</label>
|
||||
</p>
|
||||
<MultiSelect
|
||||
class="w-full"
|
||||
options={channelOptions}
|
||||
@@ -286,9 +288,9 @@ export const UsageView = ({
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
令牌
|
||||
</label>
|
||||
</p>
|
||||
<MultiSelect
|
||||
class="w-full"
|
||||
options={tokenOptions}
|
||||
@@ -300,9 +302,9 @@ export const UsageView = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
模型
|
||||
</label>
|
||||
</p>
|
||||
<MultiSelect
|
||||
class="w-full"
|
||||
options={modelOptions}
|
||||
@@ -314,9 +316,9 @@ export const UsageView = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
<p class="mb-1.5 block text-xs uppercase tracking-widest text-[color:var(--app-ink-muted)]">
|
||||
状态
|
||||
</label>
|
||||
</p>
|
||||
<MultiSelect
|
||||
class="w-full"
|
||||
options={statusOptions}
|
||||
@@ -351,51 +353,69 @@ export const UsageView = ({
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div class="app-surface overflow-hidden">
|
||||
<div class="app-surface app-data-shell overflow-hidden">
|
||||
<div class="h-[360px] overflow-auto sm:h-[440px]">
|
||||
<table class="app-table min-w-[960px] w-full text-xs sm:text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
{visibleColumnSet.has("time") && (
|
||||
<th class="sticky top-0 bg-slate-100/90">时间</th>
|
||||
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
|
||||
时间
|
||||
</th>
|
||||
)}
|
||||
{visibleColumnSet.has("model") && (
|
||||
<th class="sticky top-0 bg-slate-100/90">模型</th>
|
||||
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
|
||||
模型
|
||||
</th>
|
||||
)}
|
||||
{visibleColumnSet.has("channel") && (
|
||||
<th class="sticky top-0 bg-slate-100/90">渠道</th>
|
||||
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
|
||||
渠道
|
||||
</th>
|
||||
)}
|
||||
{visibleColumnSet.has("token") && (
|
||||
<th class="sticky top-0 bg-slate-100/90">令牌</th>
|
||||
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
|
||||
令牌
|
||||
</th>
|
||||
)}
|
||||
{visibleColumnSet.has("prompt_tokens") && (
|
||||
<th class="sticky top-0 bg-slate-100/90">输入 Tokens</th>
|
||||
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
|
||||
输入 Tokens
|
||||
</th>
|
||||
)}
|
||||
{visibleColumnSet.has("completion_tokens") && (
|
||||
<th class="sticky top-0 bg-slate-100/90">输出 Tokens</th>
|
||||
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
|
||||
输出 Tokens
|
||||
</th>
|
||||
)}
|
||||
{visibleColumnSet.has("latency") && (
|
||||
<th class="sticky top-0 bg-slate-100/90">用时 (s)</th>
|
||||
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
|
||||
用时 (s)
|
||||
</th>
|
||||
)}
|
||||
{visibleColumnSet.has("first_token") && (
|
||||
<th class="sticky top-0 bg-slate-100/90">
|
||||
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
|
||||
<Tooltip content="首个 token 返回的等待时间。">
|
||||
<span>首 token 延迟 (s)</span>
|
||||
</Tooltip>
|
||||
</th>
|
||||
)}
|
||||
{visibleColumnSet.has("stream") && (
|
||||
<th class="sticky top-0 bg-slate-100/90">流式</th>
|
||||
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
|
||||
流式
|
||||
</th>
|
||||
)}
|
||||
{visibleColumnSet.has("reasoning") && (
|
||||
<th class="sticky top-0 bg-slate-100/90">
|
||||
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
|
||||
<Tooltip content="模型推理强度等级。">
|
||||
<span>推理强度</span>
|
||||
</Tooltip>
|
||||
</th>
|
||||
)}
|
||||
{visibleColumnSet.has("status") && (
|
||||
<th class="sticky top-0 bg-slate-100/90">状态码</th>
|
||||
<th class="sticky top-0 bg-[color:var(--app-surface-strong)]/95">
|
||||
状态码
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -489,7 +509,7 @@ export const UsageView = ({
|
||||
</td>
|
||||
)}
|
||||
{visibleColumnSet.has("status") && (
|
||||
<td class="px-3 py-2.5 text-left text-xs text-[color:var(--app-ink)] sm:text-sm">
|
||||
<td class="px-3 py-2.5 text-left text-xs text-[color:var(--app-ink)] sm:text-sm">
|
||||
<button
|
||||
class="app-focus inline-flex items-center border-0 bg-transparent p-0"
|
||||
type="button"
|
||||
@@ -498,7 +518,9 @@ export const UsageView = ({
|
||||
<Chip
|
||||
class="text-[10px]"
|
||||
variant={
|
||||
log.upstream_status === 200 ? "success" : "danger"
|
||||
log.upstream_status === 200
|
||||
? "success"
|
||||
: "danger"
|
||||
}
|
||||
>
|
||||
{statusDetail.label}
|
||||
@@ -514,7 +536,7 @@ export const UsageView = ({
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="app-pagination-bar flex flex-col gap-3 text-xs text-[color:var(--app-ink-muted)] sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs text-[color:var(--app-ink-muted)]">
|
||||
共 {displayPages} 页 · {total} 条
|
||||
@@ -527,27 +549,24 @@ export const UsageView = ({
|
||||
disabled={isRefreshing}
|
||||
/>
|
||||
</div>
|
||||
<label class="app-page-size" for="usage-page-size">
|
||||
<span>每页条数</span>
|
||||
<Select
|
||||
variant="pill"
|
||||
class="w-auto text-xs app-page-size__select"
|
||||
id="usage-page-size"
|
||||
value={String(pageSize)}
|
||||
disabled={isRefreshing}
|
||||
onChange={(event) =>
|
||||
onPageSizeChange(
|
||||
Number((event.currentTarget as HTMLSelectElement).value),
|
||||
)
|
||||
}
|
||||
>
|
||||
<div class="app-page-size-control">
|
||||
<span class="app-page-size-control__label">每页</span>
|
||||
<div class="app-page-size-control__chips">
|
||||
{pageSizeOptions.map((size) => (
|
||||
<option key={size} value={String(size)}>
|
||||
<button
|
||||
class={`app-page-size-chip ${
|
||||
pageSize === size ? "app-page-size-chip--active" : ""
|
||||
}`}
|
||||
key={size}
|
||||
type="button"
|
||||
disabled={isRefreshing}
|
||||
onClick={() => onPageSizeChange(size)}
|
||||
>
|
||||
{size}
|
||||
</option>
|
||||
</button>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{activeErrorLog ? (
|
||||
|
||||
+1187
-116
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user